diff --git a/Projects/3_Adversarial Search/.env.example b/Projects/3_Adversarial Search/.env.example
new file mode 100644
index 00000000..7018c4c3
--- /dev/null
+++ b/Projects/3_Adversarial Search/.env.example
@@ -0,0 +1,8 @@
+# Copy to .env for local overrides. Keep this application on a trusted machine.
+AGENT_ARENA_DATABASE_URL=sqlite:///var/agent_arena.db
+AGENT_ARENA_SOURCE_ROOT=var/sources
+AGENT_ARENA_TEMP_ROOT=var/tmp
+AGENT_ARENA_RUNNER_IMAGE=agent-arena-runner:py312-v1
+AGENT_ARENA_MOVE_BUDGET_MS=150
+AGENT_ARENA_MINIMUM_RANKED_FIXTURES=10
+AGENT_ARENA_DEMO_OWNER_NAME=oualid
diff --git a/Projects/3_Adversarial Search/.gitignore b/Projects/3_Adversarial Search/.gitignore
new file mode 100644
index 00000000..05c8f58e
--- /dev/null
+++ b/Projects/3_Adversarial Search/.gitignore
@@ -0,0 +1,14 @@
+.env
+.venv/
+__pycache__/
+*.py[cod]
+*.egg-info/
+.pytest_cache/
+.ruff_cache/
+.coverage
+htmlcov/
+apps/web/node_modules/
+apps/web/dist/
+apps/web/*.tsbuildinfo
+docs/
+var/
diff --git a/Projects/3_Adversarial Search/.nvmrc b/Projects/3_Adversarial Search/.nvmrc
new file mode 100644
index 00000000..fc37597b
--- /dev/null
+++ b/Projects/3_Adversarial Search/.nvmrc
@@ -0,0 +1 @@
+22.17.0
diff --git a/Projects/3_Adversarial Search/Makefile b/Projects/3_Adversarial Search/Makefile
new file mode 100644
index 00000000..c1e8ab2d
--- /dev/null
+++ b/Projects/3_Adversarial Search/Makefile
@@ -0,0 +1,42 @@
+PYTHON ?= python3.12
+VENV ?= .venv
+
+.PHONY: setup dev benchmark test seed run-round runner-build format lint clean-start
+
+setup:
+ $(PYTHON) -m venv $(VENV)
+ $(VENV)/bin/python -m pip install --upgrade pip
+ $(VENV)/bin/python -m pip install -e ".[dev]"
+ cd apps/web && npm ci
+ $(VENV)/bin/alembic upgrade head
+ $(VENV)/bin/agent-arena seed
+
+dev:
+ $(VENV)/bin/python scripts/dev.py
+
+benchmark:
+ $(VENV)/bin/python scripts/benchmark.py
+
+test:
+ $(VENV)/bin/ruff format --check apps packages scripts tests
+ $(VENV)/bin/ruff check apps packages scripts tests
+ $(VENV)/bin/pytest
+ cd apps/web && npm run lint && npm run typecheck && npm test -- --run && npm run build
+
+seed:
+ $(VENV)/bin/agent-arena seed
+
+run-round:
+ $(VENV)/bin/agent-arena run-round
+
+runner-build:
+ docker build -f packages/agent_runner/Dockerfile -t agent-arena-runner:py312-v1 .
+
+format:
+ $(VENV)/bin/ruff format apps packages tests
+
+lint:
+ $(VENV)/bin/ruff check apps packages tests
+
+clean-start:
+ $(VENV)/bin/python scripts/startup_check.py
diff --git a/Projects/3_Adversarial Search/README.md b/Projects/3_Adversarial Search/README.md
index e21dff4f..1d010f76 100644
--- a/Projects/3_Adversarial Search/README.md
+++ b/Projects/3_Adversarial Search/README.md
@@ -1,159 +1,172 @@
+# Agent Arena
-# Build an Adversarial Game Playing Agent
+Agent Arena is a local browser-based beta for one permanent Knight Isolation competition.
-
+> Submit once. Your Knight Isolation agent keeps competing.
-## Synopsis
+The beta deliberately uses a small architecture: React calls FastAPI over HTTP; FastAPI uses SQLAlchemy with one SQLite file; the authoritative Python engine runs games; and submitted source is imported only inside a Docker container. Rankings are calculated directly from completed games instead of being cached in a separate standings table.
-In this project, you will experiment with adversarial search techniques by building an agent to play knights Isolation. Unlike the examples in lecture where the players control tokens that move like chess queens, this version of Isolation gives each agent control over a single token that moves in L-shaped movements--like a knight in chess.
+## What the product does
-### Isolation
+- Edit and practice one `agent.py` against Random, Greedy, or Minimax.
+- Validate syntax, the public contract, representative legal actions, and one Greedy scrimmage.
+- Store only accepted immutable submissions.
+- Automatically make the newest accepted submission active.
+- Run one mirrored fixture for every pair of active submissions.
+- Award 3/1/0 fixture points and rank qualified agents by points per fixture.
+- Replay both games in a completed fixture.
+- Open a subordinate Agent page for immutable submissions and complete fixture history.
-In the game Isolation, two players each control their own single token and alternate taking turns moving the token from one cell to another on a rectangular grid. Whenever a token occupies a cell, that cell becomes blocked for the remainder of the game. An open cell available for a token to move into is called a "liberty". The first player with no remaining liberties for their token loses the game, and their opponent is declared the winner.
+There are five browser views: Overview, Code & Play, Leaderboard, My Agents, and Submit Agent. There is no rating system, calendar, season, event system, payment system, or production authentication.
-In knights Isolation, tokens can move to any open cell that is 2-rows and 1-column or 2-columns and 1-row away from their current position on the board. On a blank board, this means that tokens have at most eight liberties surrounding their current location. Token movement is blocked at the edges of the board (the board does not wrap around the edges), however, tokens can "jump" blocked or occupied spaces (just like a knight in chess).
+The leaderboard marks an active submission as provisional until it has played 10 fixtures by default. Qualified entries are ordered by points per fixture (PPF), then raw points, fixture wins, and agent name. The threshold can be changed with `AGENT_ARENA_MINIMUM_RANKED_FIXTURES`; the formula and qualification state are returned by the API.
-Finally, agents have a fixed time limit (150 milliseconds by default) to search for the best move and respond. The search will be automatically cut off after the time limit expires, and the active agent will forfeit the game if it has not chosen a move.
+## Prerequisites
-**You can find more information (including implementation details) about the in the Isolation library readme [here](/isolation/README.md).**
+- Python 3.12.x
+- Node.js 22.17.0 and npm
+- Docker Engine or Docker Desktop for submitted-source execution
+- GNU Make on Linux/macOS; PowerShell commands are provided for Windows
+Docker is optional for browsing seeded results and running trusted baseline rounds. It is mandatory for practice or competition with submitted source. The API never falls back to importing submitted source.
-## Getting Started (Workspaces)
+## Database reset after the simplification refactor
-The easiest way to complete the project is to use the Udacity Workspace in your classroom. The environment has already been configured with the required files and libraries to support the project. If you decide to use the Workspace, then you do NOT need to perform any of the setup steps for this project. Skip to the section with instructions for completing the project.
+The simplified schema intentionally replaces the earlier local-beta schema. Existing data is not deleted automatically.
+If `var/agent_arena.db` was created by the earlier architecture, rename it before setup:
-## Getting Started (Local Environment)
-
-If you would prefer to complete the exercise in your own local environment, then follow the steps below:
-
-- Open your terminal and activate the aind conda environment (OS X or Unix/Linux users use the command shown; Windows users only run `activate aind`)
-```
-$ source activate aind
-```
-
-- Download a copy of the project files from GitHub and navigate to the project folder. (Note: if you've previously downloaded the repository for another project then you can skip the clone command. However, you should run `git pull` to receive any project updates since you cloned the repository.)
-```
-(aind) $ git clone https://github.com/udacity/artificial-intelligence
-(aind) $ cd "artificial-intelligence/Projects/3_Adversarial Search"
+```powershell
+Move-Item var\agent_arena.db var\agent_arena_before_simplification.db
```
+On Linux/macOS:
-## Instructions
-
-You must implement an agent in the `CustomPlayer` class defined in the `my_custom_player.py` file. The interface definition for game agents only requires you to implement the `.get_action()` method, but you can add any other methods to the class that you deem necessary. You can build a basic agent by combining minimax search with alpha-beta pruning and iterative deepening from lecture.
-
-**NOTE:** Your agent will **not** be evaluated in an environment suitable for running machine learning or deep learning agents (like AlphaGo); visit an office hours sessions **after** completing the project if you would like guidance on incorporating machine learning in your agent.
-
-#### The get_action() Method
-This function is called once per turn for each player. The calling function handles the time limit and
+```bash
+mv var/agent_arena.db var/agent_arena_before_simplification.db
```
-def get_action(self, state):
- import random
- self.queue.put(random.choice(state.actions()))
-```
-
-- **DO NOT** use multithreading/multiprocessing (the isolation library already uses them, so using them in your agent may cause conflicts)
-
-#### Initialization Data
-Your agent will automatically read the contents of a file named `data.pickle` if it exists in the same folder as `my_custom_player.py`. The serialized object from the pickle file will be assigned to `self.data`. Your agent should not write to or modify the contents of the pickle file during search.
-The log file will record a warning message if there is no data file, however a data file is NOT required unless you need it for your opening book. (You are allowed to use the data file to provide _any_ initialization information to your agent; it is not limited to an opening book.)
+The old database remains available as a backup but is not read by the simplified application.
+## Linux/macOS quick start
-#### Saving Information Between Turns
-The `CustomPlayer` class can pass internal state by assigning the data to the attribute `self.context`. An instance of your agent class will carry the context between each turn of a single game, but the contents will be reset at the start of any new game.
-```
-def get_action(...):
- action = self.mcts()
- self.queue.put(action)
- self.context = object_you_want_to_save # self.context will contain this object on the next turn
+```bash
+cp .env.example .env
+make setup
+make runner-build
+make dev
```
-## Choose an Experiment
+Open:
-Select at least one of the following to implement and evaluate in your report. (There is no upper limit on the techniques you incorporate into your agent.)
+- Web app: http://127.0.0.1:5173
+- API health: http://127.0.0.1:8000/api/health
+- OpenAPI: http://127.0.0.1:8000/docs
-### Option 1: Develop a custom heuristic (must not be one of the heuristics from lectures, and cannot only be a combination of the number of liberties available to each agent)
+`make setup` creates the virtual environment, installs pinned dependencies, applies the one Alembic migration, and seeds four trusted demo agents plus completed fixtures.
-- Create a performance baseline using `run_search.py` (with the `fair_matches` flag enabled) to evaluate the effectiveness of your agent using the #my_moves - #opponent_moves heuristic from lecture
-- Use the same process to evaluate the effectiveness of your agent using your own custom heuristic
+## Windows PowerShell
-**Hints:**
-- Research other games (chess, go, connect4, etc.) to get ideas for developing good heuristics
-- If the results of your tests are very close, try increasing the number of matches (e.g., >100) to increase your confidence in the results
-- Experiment with adding more search time--does adding time confer any advantage to your agent over the baseline?
-- Augment the code to count the nubmer of nodes your agent searches--is it better to search more or fewer nodes? How does your heuristic compare to the baseline heuristic you chose?
+```powershell
+Copy-Item .env.example .env
+py -3.12 -m venv .venv
+.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
+npm.cmd ci --prefix apps\web
+.\.venv\Scripts\alembic.exe upgrade head
+.\.venv\Scripts\agent-arena.exe seed
+docker build -f packages\agent_runner\Dockerfile -t agent-arena-runner:py312-v1 .
+.\.venv\Scripts\python.exe scripts\dev.py
+```
+This checkout may be inspected with another Python version, but the supported application and runner runtime is Python 3.12.
-### Option 2: Develop an opening book (must span at least depth 4 of the search tree)
+## Agent contract
-- Write your own code to develop an opening book of the best moves for every possible game state from an empty board to at least a depth of 4 plies
-- Create a performance baseline using `run_search.py` (with the `fair_matches` flag _disabled_) to evaluate the effectiveness of your agent using randomly chosen opening moves. (You can use any heuristic function, but you should use the same heuristic on your agent for all experiments.)
-- Use the same procedure to evaluate the effectiveness of your agent when early moves are selected from your opening book
+Submit one file named `agent.py`:
-**Hints:**
-- Developing an opening book can require long run-times to simulate games and accumulate outcome statistics
-- If the results are very close, try increasing the number of matches (e.g., >100) to increase your confidence in the results
+```python
+class CustomPlayer:
+ def get_action(self, state):
+ actions = state.actions()
+ return actions[0] if actions else None
+```
-**Adding a basic opening book**
-- You will need to write your own code to develop a good opening book, but you can pass data into your agent by saving the file as "data.pickle" in the same folder as `my_custom_player.py`. Use the [pickle](https://docs.python.org/3/library/pickle.html) module to serialize the object you want to save. The pickled object will be accessible to your agent through the `self.data` attribute.
+The original queue style is also supported:
-For example, the contents of dictionary `my_data` can be saved to disk:
-```
-import pickle
-from isolation import Isolation
-state = Isolation()
-my_data = {state: 57} # opening book always chooses the middle square on an open board
-with open("data.pickle", 'wb') as f:
- pickle.dump(my_data, f)
+```python
+self.queue.put(action)
```
+The player may use `self.context` during one game. A fresh container and player instance are created for the next game. The trusted engine always validates the proposed action against `state.actions()`.
-### Option 3: Build an agent using advanced search techniques (for example: killer heuristic, principle variation search (not in lecture), or monte carlo tree search (not in lecture))
+## Simple data model
-- Create a performance baseline using `run_search.py` to evaluate the effectiveness of a baseline agent (e.g., an agent using your minimax or alpha-beta search code from the classroom)
-- Use `run_search.py` to evaluate the effectiveness of your agent using your own custom search techniques
-- You must decide whether to test with or without "fair" matches enabled--justify your choice in your report
+- **Agent**: stable name and owner identity.
+- **Submission**: immutable accepted source; the newest accepted submission is active.
+- **Fixture**: one pair of mirrored games.
+- **Game**: one result and one canonical replay list.
-**Hints:**
-- If the results are very close, try increasing the number of matches (e.g., >100) to increase your confidence in the results
-- Experiment with adding more search time--does adding time confer any advantage to your agent?
-- Augment the code to count the number of nodes your agent searches--does your agent have an advantage compared to the baseline search algorithm you chose?
+There is no Standing table. The leaderboard is recalculated from completed Fixtures and Games whenever it is requested.
-**Note:**
-- You MAY implement advanced techniques from the reading list at the end of the lesson (like Monte Carlo Tree Search, principle variation search, etc.), but your agent is being evaluated for _performance_ rather than _correctness_. It's possible to pass the project requirements **without** using these advanced techniques, so project reviewers may encourage you to implement a simpler solution if you are struggling with correct implementation. (That's good general advice: do the simplest thing first, and only add complexity when you must.)
+## Common commands
+```bash
+make dev # launch the normal development database
+make benchmark # launch with the isolated 15-agent benchmark database
+make seed # idempotently create demo agents and initial fixtures
+make run-round # synchronously run one balanced all-active round
+make test # backend and frontend formatting, lint, tests, and build
+make clean-start # start temporary API/web ports and probe both
+make runner-build # build the local Python 3.12 runner image
+```
-## Report Requirements
+On Windows, launch benchmark mode with:
-Your report must include a table or chart with data from an experiment to evaluate the performance of your agent as described above. Use the data from your experiment to answer the relevant questions below. (You may choose one set of questions if your agent incorporates multiple techniques.)
+```powershell
+.\.venv\Scripts\python.exe scripts\benchmark.py
+```
-**Advanced Heuristic**
-- What features of the game does your heuristic incorporate, and why do you think those features matter in evaluating states during search?
-- Analyze the search depth your agent achieves using your custom heuristic. Does search speed matter more or less than accuracy to the performance of your heuristic?
+Stop the normal development servers first because both modes use ports 5173 and 8000. Benchmark mode applies the same migration and seeds 15 trusted strategy variants into `var/benchmark/agent_arena.db`; it does not read, seed, or modify `var/agent_arena.db`. Open the Leaderboard and click **Run competition round**. One 15-agent round runs 105 mirrored fixtures (210 games), gives every agent 14 fixtures, and therefore clears the default 10-fixture ranking threshold.
-**Opening book**
-- Describe your process for collecting statistics to build your opening book. How did you choose states to sample? And how did you perform rollouts to determine a winner?
-- What opening moves does your book suggest are most effective on an empty board for player 1 and what is player 2's best reply?
+The CLI intentionally contains only three commands:
-**Advanced Search Techniques**
-- Choose a baseline search algorithm for comparison (for example, alpha-beta search with iterative deepening, etc.). How much performance difference does your agent show compared to the baseline?
-- Why do you think the technique you chose was more (or less) effective than the baseline?
+```bash
+agent-arena doctor
+agent-arena seed
+agent-arena run-round
+```
+`doctor` checks whether the database file and Docker runner image exist without creating a schema.
+## Repository map
-## Evaluation
+```text
+apps/web React browser interface
+apps/api FastAPI routes, SQLAlchemy, Alembic, operations
+packages/isolation_engine authoritative state, baselines, game, replay, scoring
+packages/agent_runner Docker coordinator, worker, queue compatibility
+tests focused backend and API flow tests
+docs architecture, API, security, limits, learning map
+```
-Your project will be reviewed by a Udacity reviewer against the project rubric [here](https://review.udacity.com/#!/rubrics/1801/view). Review this rubric thoroughly, and self-evaluate your project before submission. All criteria found in the rubric must meet specifications for you to pass.
+## Testing
+
+```powershell
+.\.venv\Scripts\ruff.exe format --check apps packages scripts tests
+.\.venv\Scripts\ruff.exe check apps packages scripts tests
+.\.venv\Scripts\pytest.exe
+npm.cmd run lint --prefix apps\web
+npm.cmd run typecheck --prefix apps\web
+npm.cmd test --prefix apps\web -- --run
+npm.cmd run build --prefix apps\web
+.\.venv\Scripts\python.exe scripts\startup_check.py
+```
+The main API acceptance path is `tests/integration/test_api.py::test_submit_practice_compete_leaderboard_and_replay`. It uses a controller test double so it can run without Docker. A real submitted-source run still requires the built Docker image.
-## Submission
+## Security scope
-Before you can submit your project for review in the classroom, you must run the remote test suite & generate a zip archive of the required project files. Submit the archive in your classroom for review. (See notes on submissions below for more details.) From your terminal, run the command: (make sure to activate the aind conda environment if you're running the project in your local environment; workspace users do **not** need to activate an environment.)
-```
-$ udacity submit
-```
-The script will automatically create a zip archive of the required files (`my_custom_player.py` and `report.pdf` are required; `data.pickle` will be included if it exists) and submit your code to a remote server for testing. You can only submit a zip archive created by the PA script (even if you're only submitting a partial solution), and you **must submit the exact zip file created by the Project Assistant** in your classroom for review. The classroom verifies the zip file submitted against records on the Project Assistant system; any changes in the file will cause your submission to be rejected.
+This is a local beta, not a production multi-tenant sandbox. Do not expose it to arbitrary internet users. Docker reduces risk but does not eliminate kernel, daemon, denial-of-service, side-channel, or supply-chain risks.
-**NOTE:** Students who authenticate with Facebook or Google accounts _must_ follow the instructions on the FAQ page [here](https://project-assistant.udacity.com/faq) to obtain an authentication token. (The Workspace already includes instructions for obtaining and configuring your token.)
+The original engine encoding details remain available in
+[the Isolation library guide](isolation/README.md).
diff --git a/Projects/3_Adversarial Search/agent_arena_lab_v7_simple.html b/Projects/3_Adversarial Search/agent_arena_lab_v7_simple.html
new file mode 100644
index 00000000..b15d7f50
--- /dev/null
+++ b/Projects/3_Adversarial Search/agent_arena_lab_v7_simple.html
@@ -0,0 +1,871 @@
+
+
+
+
+
+ Agent Arena · Knight Isolation Concept
+
+
+
+
+
+
+
+
+
+
+
+
+
+
One game · one permanent points table
+
Submit once. Your Knight Isolation agent keeps competing.
+
Write a Python agent, test it against a baseline, deploy an immutable version, and follow its fixtures on a simple permanent leaderboard.
+
+ Start coding
+ View standings
+
+
+
+
Competition at a glance
+
+
64 Active agent versions
+
1,248 Completed fixtures
+
150 ms Fixed move budget
+
+
+
Game A Agent 1 starts
+
⇄
+
Game B Agent 2 starts
+
+
Simple points: a fixture win earns 3 points, a 1–1 mirrored draw earns 1 point per agent, and a loss earns 0.
+
+
+
+ How it works The smallest coherent version of the arena.
+
+
1 Write Implement get_action(state) in the browser Code Lab.
+
2 Test Run conceptual scrimmages against a stable baseline and inspect logs.
+
3 Submit Create an immutable Python build under fixed platform limits.
+
4 Compete The platform assigns mirrored fixtures and updates simple points.
+
+
+ Featured fixture A public replay from the permanent competition.
Open full viewer →
+
+
+
+
Atlas-AB v17 vs MobilityLab v12 Fixture KI-01248 · Game A of 2
+
Official
+
+
+
+
+
+
Atlas-AB v17 @ali-lab
+
VS
+
MobilityLab v12 @nova-byte
+
+
+
Fixture result: Atlas-AB wins both mirrored games, earns 3 points ; MobilityLab earns 0 .
+
+
+
+ Top of the table All agents shown have completed the same number of fixtures.
Full leaderboard →
+
+
+ # Agent Developer Fixtures W-D-L Points
+
+
+
+
+
+
+
+
Code & Play A compact game viewer, problem statement, Python editor, and console inspired by browser coding arenas.
+
Simulation only · code is not executed
+
+
+
+
+
+
+
YourAgent vs GreedyFox Practice fixture · Game A · seed demo-17
+
Ready
+
+
+
+ ↺
+ ←
+ ▶ Play
+ →
+
+ Move 0 / 16
+
+
+
+
+
+ The Goal
+ Rules
+ Agent API
+
+
+
Block your opponent before they block you.
+
Each agent controls one knight. Every square a knight leaves becomes blocked forever. The first agent with no legal knight move loses.
+
Adversarial search Perfect information 150 ms / move
+
+
+
+ The board is 11 × 9.
+ Opening placements may use any open square.
+ Later actions use chess-knight movement.
+ Visited squares become permanently blocked.
+ A timeout, crash, or illegal action loses the game.
+ A ranked fixture contains two games with starting order swapped.
+
+
+
+
get_action(state) must return one legal action before the fixed deadline.
+
The conceptual state exposes:
+
state.actions()state.result(action)state.player()state.liberties(location)
+
+
+
+
+
+
+
+
+
+
+
+ ▶ Run code (simulated)
+ ↺ Replay same conditions
+ ✓ Prepare submission
+
+
+
+
+
+
+
+
+
Permanent Knight Isolation leaderboard No rating model: only equal-fixture totals using 3 points for a win, 1 for a draw, and 0 for a loss.
+
42 fixtures per active version
+
+ Fixture definition: agents play two mirrored games. A 2–0 result is a win; 1–1 is a draw; 0–2 is a loss.
+
+
+
+ # Agent version Developer Fixtures Wins Draws Losses Points Form
+
+
+
+
+
+
Selected competitor
Atlas-AB v17 @ali-lab
+
Active
+
+
AGT-KI-00017 · AGV-KI-00017-17 · build 8fa31c2
+
+
88 Points
+
28 Wins
+
4 Draws
+
10 Losses
+
+
Strong mobility heuristic with stable time management.
+
+
+
+
+
+
+
+
My Agents Manage your Knight Isolation lineages and immutable versions.
+
Submit new agent
+
+ Only one version per lineage is active in the permanent points table. Older versions remain privately visible and auditable.
+
+
+
+
+
+
Submission Lab A condensed conceptual flow for creating a new agent or immutable version.
+
No real upload or deployment
+
+
+
+
+ New agent
+ New version
+
+
+
+ Run conceptual checks
+ Create immutable build
+
+
+
+
Conceptual pipeline
+
What the finished platform would do
+
+
1
Package Snapshot source and assign stable IDs.
Waiting
+
2
Check safety Conceptual screening before isolated execution.
Waiting
+
3
Validate Import, legal actions, deterministic replay and timeouts.
Waiting
+
4
Deploy version Immutable private build enters the points competition.
Waiting
+
+
+
No network · read-only root · bounded scratch space · CPU, wall-time, memory and output limits · private source by default · auditable replay artifacts.
+
+
+
+
+
+
+
+
+ Overview
+ Code
+ Table
+ Agents
+ Submit
+
+
+
+
+
+
+
diff --git a/Projects/3_Adversarial Search/alembic.ini b/Projects/3_Adversarial Search/alembic.ini
new file mode 100644
index 00000000..38ab1ff6
--- /dev/null
+++ b/Projects/3_Adversarial Search/alembic.ini
@@ -0,0 +1,31 @@
+[alembic]
+script_location = apps/api/alembic
+prepend_sys_path = . apps/api packages/isolation_engine packages/agent_runner
+sqlalchemy.url = sqlite:///var/agent_arena.db
+
+[loggers]
+keys = root,sqlalchemy,alembic
+[handlers]
+keys = console
+[formatters]
+keys = generic
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/__init__.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/__init__.py
new file mode 100644
index 00000000..c8b77ede
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/__init__.py
@@ -0,0 +1 @@
+"""Agent Arena FastAPI application package."""
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/api.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/api.py
new file mode 100644
index 00000000..b61d9d00
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/api.py
@@ -0,0 +1,255 @@
+from __future__ import annotations
+
+from typing import Annotated
+
+from agent_runner import DockerAgent
+from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
+from isolation_engine import ENGINE_VERSION, RULESET_VERSION
+from sqlalchemy import select
+from sqlalchemy.exc import OperationalError
+from sqlalchemy.orm import Session
+
+from .competition import agent_fixture_history, build_leaderboard, fixture_details, run_round
+from .database import get_session
+from .models import Agent, Fixture, Submission
+from .schemas import (
+ AgentCreate,
+ AgentFixtureHistoryEntry,
+ AgentResponse,
+ FixtureResponse,
+ HealthResponse,
+ LeaderboardEntry,
+ PracticeCreate,
+ PracticeResponse,
+ RecordResponse,
+ RoundResponse,
+ RulesResponse,
+ SubmissionCreate,
+ SubmissionResponse,
+)
+from .submissions import prepare_submission, run_practice, save_new_agent, save_submission
+
+router = APIRouter(prefix="/api")
+SessionDependency = Annotated[Session, Depends(get_session)]
+
+
+@router.get("/health", response_model=HealthResponse)
+def health(request: Request) -> HealthResponse:
+ settings = request.app.state.settings
+ database_status = "ready"
+ try:
+ with request.app.state.database.sessions() as session:
+ session.scalar(select(Agent.id).limit(1))
+ except OperationalError:
+ database_status = "migration_required"
+ return HealthResponse(
+ database=database_status,
+ runner=DockerAgent.diagnostic(settings.runner_image),
+ )
+
+
+@router.get("/rules", response_model=RulesResponse)
+def rules(request: Request) -> RulesResponse:
+ settings = request.app.state.settings
+ return RulesResponse(
+ move_budget_ms=settings.move_budget_ms,
+ minimum_ranked_fixtures=settings.minimum_ranked_fixtures,
+ engine_version=ENGINE_VERSION,
+ ruleset_version=RULESET_VERSION,
+ demo_owner_name=settings.demo_owner_name,
+ )
+
+
+@router.get("/leaderboard", response_model=list[LeaderboardEntry])
+def leaderboard(request: Request, session: SessionDependency) -> list[LeaderboardEntry]:
+ rows = build_leaderboard(session, request.app.state.settings.minimum_ranked_fixtures)
+ return [LeaderboardEntry.model_validate(row) for row in rows]
+
+
+@router.get("/agents", response_model=list[AgentResponse])
+def agents(
+ session: SessionDependency,
+ owner_name: Annotated[str | None, Query(max_length=80)] = None,
+) -> list[AgentResponse]:
+ statement = select(Agent).order_by(Agent.owner_name, Agent.name)
+ if owner_name:
+ statement = statement.where(Agent.owner_name == owner_name)
+ return [_agent_response(session, item) for item in session.scalars(statement)]
+
+
+@router.get("/agents/{agent_id}", response_model=AgentResponse)
+def agent(agent_id: str, session: SessionDependency) -> AgentResponse:
+ item = session.get(Agent, agent_id)
+ if item is None:
+ raise HTTPException(status_code=404, detail="agent was not found")
+ return _agent_response(session, item)
+
+
+@router.get(
+ "/agents/{agent_id}/fixtures",
+ response_model=list[AgentFixtureHistoryEntry],
+)
+def agent_fixtures(
+ agent_id: str,
+ session: SessionDependency,
+) -> list[AgentFixtureHistoryEntry]:
+ if session.get(Agent, agent_id) is None:
+ raise HTTPException(status_code=404, detail="agent was not found")
+ return [
+ AgentFixtureHistoryEntry.model_validate(item)
+ for item in agent_fixture_history(session, agent_id)
+ ]
+
+
+@router.post("/agents", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
+def create_agent(
+ payload: AgentCreate,
+ request: Request,
+ session: SessionDependency,
+) -> AgentResponse:
+ settings = request.app.state.settings
+ try:
+ prepared = prepare_submission(payload.source, settings)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error)) from error
+ if not prepared.accepted:
+ raise HTTPException(status_code=422, detail=prepared.validation)
+ try:
+ item = save_new_agent(
+ session,
+ settings,
+ owner_name=payload.owner_name,
+ name=payload.name,
+ prepared=prepared,
+ )
+ session.commit()
+ session.refresh(item)
+ return _agent_response(session, item)
+ except ValueError as error:
+ session.rollback()
+ raise HTTPException(status_code=409, detail=str(error)) from error
+
+
+@router.post(
+ "/agents/{agent_id}/submissions",
+ response_model=AgentResponse,
+ status_code=status.HTTP_201_CREATED,
+)
+def create_submission(
+ agent_id: str,
+ payload: SubmissionCreate,
+ request: Request,
+ session: SessionDependency,
+) -> AgentResponse:
+ item = session.get(Agent, agent_id)
+ if item is None:
+ raise HTTPException(status_code=404, detail="agent was not found")
+ settings = request.app.state.settings
+ try:
+ prepared = prepare_submission(payload.source, settings)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error)) from error
+ if not prepared.accepted:
+ raise HTTPException(status_code=422, detail=prepared.validation)
+ save_submission(session, settings, agent=item, prepared=prepared)
+ session.commit()
+ session.refresh(item)
+ return _agent_response(session, item)
+
+
+@router.post("/practice", response_model=PracticeResponse)
+def practice(payload: PracticeCreate, request: Request) -> PracticeResponse:
+ try:
+ result = run_practice(
+ payload.source,
+ payload.baseline,
+ payload.seed,
+ request.app.state.settings,
+ )
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error)) from error
+ return PracticeResponse.model_validate(result)
+
+
+@router.get("/fixtures/featured", response_model=FixtureResponse | None)
+def featured_fixture(session: SessionDependency) -> FixtureResponse | None:
+ item = session.scalar(
+ select(Fixture)
+ .where(Fixture.status == "complete")
+ .order_by(Fixture.completed_at.desc(), Fixture.id)
+ .limit(1)
+ )
+ return FixtureResponse.model_validate(fixture_details(session, item)) if item else None
+
+
+@router.get("/fixtures/{fixture_id}", response_model=FixtureResponse)
+def fixture(fixture_id: str, session: SessionDependency) -> FixtureResponse:
+ item = session.get(Fixture, fixture_id)
+ if item is None:
+ raise HTTPException(status_code=404, detail="fixture was not found")
+ return FixtureResponse.model_validate(fixture_details(session, item))
+
+
+@router.post("/competition/run-round", response_model=RoundResponse)
+def run_competition_round(request: Request) -> RoundResponse:
+ database = request.app.state.database
+ try:
+ fixture_ids = run_round(database, request.app.state.settings)
+ except ValueError as error:
+ raise HTTPException(status_code=409, detail=str(error)) from error
+ return RoundResponse(
+ fixture_ids=fixture_ids,
+ participant_count=_participant_count(len(fixture_ids)),
+ )
+
+
+def _agent_response(session: Session, item: Agent) -> AgentResponse:
+ submissions = list(
+ session.scalars(
+ select(Submission)
+ .where(Submission.agent_id == item.id)
+ .order_by(Submission.number.desc())
+ )
+ )
+ table = build_leaderboard(session)
+ active_record = next(
+ (row for row in table if row["submission_id"] == item.active_submission_id),
+ None,
+ )
+ record = None
+ if active_record:
+ record = RecordResponse(
+ fixtures=active_record["fixtures"],
+ wins=active_record["wins"],
+ draws=active_record["draws"],
+ losses=active_record["losses"],
+ points=active_record["points"],
+ )
+ return AgentResponse(
+ id=item.id,
+ owner_name=item.owner_name,
+ name=item.name,
+ active_submission_id=item.active_submission_id,
+ created_at=item.created_at,
+ submissions=[
+ SubmissionResponse(
+ id=submission.id,
+ agent_id=submission.agent_id,
+ number=submission.number,
+ source_hash=submission.source_hash,
+ execution_kind=submission.execution_kind,
+ validation_result=submission.validation_result,
+ created_at=submission.created_at,
+ is_active=submission.id == item.active_submission_id,
+ )
+ for submission in submissions
+ ],
+ record=record,
+ )
+
+
+def _participant_count(fixture_count: int) -> int:
+ count = 0
+ while count * (count - 1) // 2 < fixture_count:
+ count += 1
+ return count
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/benchmark_seed.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/benchmark_seed.py
new file mode 100644
index 00000000..10e004c4
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/benchmark_seed.py
@@ -0,0 +1,73 @@
+"""Seed only the isolated benchmark database with trusted comparison agents."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import uuid
+from dataclasses import asdict
+
+from isolation_engine import BENCHMARK_STRATEGY_LIST
+from sqlalchemy import func, select
+
+from .config import settings
+from .database import Database
+from .models import Agent, Fixture, Submission
+
+BENCHMARK_OWNER = "benchmark-lab"
+BENCHMARK_NAMESPACE = uuid.UUID("f0e2dbd3-9718-46ed-9890-f9dc4329886c")
+
+
+def benchmark_id(kind: str, strategy_key: str) -> str:
+ return str(uuid.uuid5(BENCHMARK_NAMESPACE, f"{kind}:{strategy_key}"))
+
+
+def seed_benchmark_database(database: Database) -> dict[str, int]:
+ with database.sessions.begin() as session:
+ for strategy in BENCHMARK_STRATEGY_LIST:
+ agent_id = benchmark_id("agent", strategy.key)
+ submission_id = benchmark_id("submission", strategy.key)
+ if session.get(Agent, agent_id) is not None:
+ continue
+ agent = Agent(
+ id=agent_id,
+ owner_name=BENCHMARK_OWNER,
+ name=strategy.agent_name,
+ active_submission_id=submission_id,
+ )
+ submission = Submission(
+ id=submission_id,
+ agent_id=agent_id,
+ number=1,
+ source_hash=hashlib.sha256(
+ f"trusted-benchmark:{strategy.key}:1".encode()
+ ).hexdigest(),
+ source_path=f"builtin://benchmark/{strategy.key}",
+ execution_kind=strategy.key,
+ validation_result={
+ "accepted": True,
+ "trusted_benchmark": True,
+ "strategy": asdict(strategy),
+ },
+ )
+ session.add_all([agent, submission])
+
+ with database.sessions() as session:
+ return {
+ "agents": session.scalar(select(func.count(Agent.id))) or 0,
+ "submissions": session.scalar(select(func.count(Submission.id))) or 0,
+ "fixtures": session.scalar(select(func.count(Fixture.id))) or 0,
+ }
+
+
+def main() -> int:
+ database = Database.create(settings.database_url)
+ try:
+ print(json.dumps(seed_benchmark_database(database), indent=2))
+ finally:
+ database.engine.dispose()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/cli.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/cli.py
new file mode 100644
index 00000000..3c879292
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/cli.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import argparse
+import json
+import sqlite3
+from pathlib import Path
+
+from agent_runner import DockerAgent
+
+from .competition import run_round
+from .config import settings
+from .database import Database
+from .seed import seed_database
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(prog="agent-arena")
+ parser.add_argument("command", choices=("seed", "run-round", "doctor"))
+ args = parser.parse_args()
+
+ if args.command == "doctor":
+ print(
+ json.dumps(
+ {
+ "database": _database_diagnostic(),
+ "runner": DockerAgent.diagnostic(settings.runner_image),
+ },
+ indent=2,
+ )
+ )
+ return 0
+
+ database = Database.create(settings.database_url)
+ try:
+ if args.command == "seed":
+ print(json.dumps(seed_database(database, settings), indent=2))
+ elif args.command == "run-round":
+ fixture_ids = run_round(database, settings)
+ print(json.dumps({"fixtures": fixture_ids, "status": "complete"}, indent=2))
+ finally:
+ database.engine.dispose()
+ return 0
+
+
+def _database_diagnostic() -> dict[str, object]:
+ prefix = "sqlite:///"
+ if not settings.database_url.startswith(prefix):
+ return {"available": True, "url": settings.database_url}
+ path = Path(settings.database_url.removeprefix(prefix))
+ if not path.exists():
+ return {
+ "available": False,
+ "path": str(path),
+ "message": "Run the Alembic migration first.",
+ }
+ expected = {"alembic_version", "agents", "submissions", "fixtures", "games"}
+ try:
+ with sqlite3.connect(f"file:{path.as_posix()}?mode=ro", uri=True) as connection:
+ rows = connection.execute("SELECT name FROM sqlite_master WHERE type='table'")
+ tables = {row[0] for row in rows}
+ except sqlite3.Error as error:
+ return {"available": False, "path": str(path), "message": str(error)}
+ if not expected.issubset(tables):
+ return {
+ "available": False,
+ "path": str(path),
+ "message": "The earlier beta schema is present; back it up and run the new migration.",
+ }
+ return {
+ "available": True,
+ "path": str(path),
+ "message": "Simplified database schema is ready.",
+ }
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/competition.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/competition.py
new file mode 100644
index 00000000..b26bb2e1
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/competition.py
@@ -0,0 +1,339 @@
+"""Mirrored fixtures, persisted games, and leaderboard calculation."""
+
+from __future__ import annotations
+
+from itertools import combinations
+from pathlib import Path
+from typing import Any
+
+from agent_runner import DockerAgent
+from isolation_engine import (
+ BENCHMARK_STRATEGIES,
+ BaselineController,
+ BenchmarkController,
+ GameResult,
+ play_game,
+ score_fixture,
+)
+from sqlalchemy import or_, select
+from sqlalchemy.orm import Session
+
+from .config import Settings
+from .database import Database
+from .models import Agent, Fixture, Game, Submission, utcnow
+
+
+def run_round(database: Database, settings: Settings) -> list[str]:
+ """Synchronously play one fixture for every pair of active submissions."""
+
+ with database.sessions() as session:
+ agents = list(
+ session.scalars(
+ select(Agent).where(Agent.active_submission_id.is_not(None)).order_by(Agent.name)
+ )
+ )
+ submissions = [session.get(Submission, agent.active_submission_id) for agent in agents]
+ active = [submission for submission in submissions if submission is not None]
+ if len(active) < 2:
+ raise ValueError("at least two active submissions are required")
+
+ fixture_ids: list[str] = []
+ for submission_a, submission_b in combinations(active, 2):
+ with database.sessions.begin() as session:
+ fixture = Fixture(
+ submission_a_id=submission_a.id,
+ submission_b_id=submission_b.id,
+ status="running",
+ )
+ session.add(fixture)
+ session.flush()
+ fixture_ids.append(fixture.id)
+ _play_fixture(database, settings, fixture.id, submission_a, submission_b)
+ return fixture_ids
+
+
+def _play_fixture(
+ database: Database,
+ settings: Settings,
+ fixture_id: str,
+ submission_a: Submission,
+ submission_b: Submission,
+) -> None:
+ try:
+ game_a = play_game(
+ controller_for(submission_a, settings, seed=101),
+ controller_for(submission_b, settings, seed=202),
+ time_limit_ms=settings.move_budget_ms,
+ )
+ game_b = play_game(
+ controller_for(submission_b, settings, seed=303),
+ controller_for(submission_a, settings, seed=404),
+ time_limit_ms=settings.move_budget_ms,
+ )
+ with database.sessions.begin() as session:
+ fixture = session.get(Fixture, fixture_id)
+ assert fixture is not None
+ session.add_all(
+ [
+ game_model(fixture.id, 1, submission_a.id, submission_b.id, game_a),
+ game_model(fixture.id, 2, submission_b.id, submission_a.id, game_b),
+ ]
+ )
+ fixture.status = "complete"
+ fixture.completed_at = utcnow()
+ except Exception:
+ with database.sessions.begin() as session:
+ fixture = session.get(Fixture, fixture_id)
+ assert fixture is not None
+ fixture.status = "failed"
+ fixture.completed_at = utcnow()
+ raise
+
+
+def controller_for(submission: Submission, settings: Settings, seed: int) -> Any:
+ if submission.execution_kind in {"random", "greedy", "minimax"}:
+ return BaselineController(submission.execution_kind, seed=seed)
+ if submission.execution_kind in BENCHMARK_STRATEGIES:
+ return BenchmarkController(submission.execution_kind, seed=seed)
+ return DockerAgent(Path(submission.source_path), settings.runner_image)
+
+
+def game_model(
+ fixture_id: str,
+ leg_number: int,
+ first_id: str,
+ second_id: str,
+ result: GameResult,
+) -> Game:
+ winner_id = first_id if result.winner_player == 0 else second_id
+ return Game(
+ fixture_id=fixture_id,
+ leg_number=leg_number,
+ first_submission_id=first_id,
+ second_submission_id=second_id,
+ winner_submission_id=winner_id,
+ end_status=result.end_status,
+ losing_reason=result.losing_reason,
+ replay=result.replay_dicts(),
+ engine_version=result.engine_version,
+ ruleset_version=result.ruleset_version,
+ )
+
+
+def fixture_score(session: Session, fixture: Fixture) -> Any | None:
+ games = list(
+ session.scalars(select(Game).where(Game.fixture_id == fixture.id).order_by(Game.leg_number))
+ )
+ if len(games) != 2 or any(game.winner_submission_id is None for game in games):
+ return None
+ winners = tuple(
+ 0 if game.winner_submission_id == fixture.submission_a_id else 1 for game in games
+ )
+ return score_fixture((winners[0], winners[1]))
+
+
+def build_leaderboard(
+ session: Session,
+ minimum_ranked_fixtures: int = 10,
+) -> list[dict[str, Any]]:
+ """Calculate points directly from completed fixtures at read time."""
+
+ agents = list(session.scalars(select(Agent).order_by(Agent.name)))
+ active_agents = [agent for agent in agents if agent.active_submission_id]
+ entries: dict[str, dict[str, Any]] = {}
+ for agent in active_agents:
+ submission = session.get(Submission, agent.active_submission_id)
+ if submission is None:
+ continue
+ entries[submission.id] = {
+ "agent_id": agent.id,
+ "agent_name": agent.name,
+ "owner_name": agent.owner_name,
+ "submission_id": submission.id,
+ "submission_number": submission.number,
+ "fixtures": 0,
+ "wins": 0,
+ "draws": 0,
+ "losses": 0,
+ "points": 0,
+ "recent_fixtures": [],
+ }
+
+ all_submissions = {item.id: item for item in session.scalars(select(Submission))}
+ all_agents = {item.id: item for item in agents}
+ fixtures = list(
+ session.scalars(
+ select(Fixture)
+ .where(Fixture.status == "complete")
+ .order_by(Fixture.completed_at.desc(), Fixture.id)
+ )
+ )
+ for fixture in fixtures:
+ scored = fixture_score(session, fixture)
+ if scored is None or fixture.completed_at is None:
+ continue
+ for submission_id, points, opponent_id in (
+ (fixture.submission_a_id, scored.points_a, fixture.submission_b_id),
+ (fixture.submission_b_id, scored.points_b, fixture.submission_a_id),
+ ):
+ entry = entries.get(submission_id)
+ if entry is None:
+ continue
+ entry["fixtures"] += 1
+ entry["points"] += points
+ if scored.is_draw:
+ entry["draws"] += 1
+ outcome = "D"
+ elif points == 3:
+ entry["wins"] += 1
+ outcome = "W"
+ else:
+ entry["losses"] += 1
+ outcome = "L"
+ opponent_submission = all_submissions[opponent_id]
+ opponent = all_agents[opponent_submission.agent_id]
+ opponent_points = (
+ scored.points_b if submission_id == fixture.submission_a_id else scored.points_a
+ )
+ entry["recent_fixtures"].append(
+ {
+ "id": fixture.id,
+ "opponent_name": opponent.name,
+ "outcome": outcome,
+ "score": f"{points}-{opponent_points}",
+ "completed_at": fixture.completed_at,
+ }
+ )
+
+ for entry in entries.values():
+ fixtures_played = entry["fixtures"]
+ entry["points_per_fixture"] = entry["points"] / fixtures_played if fixtures_played else 0.0
+ entry["qualified"] = fixtures_played >= minimum_ranked_fixtures
+
+ ordered = sorted(
+ entries.values(),
+ key=lambda item: (
+ not item["qualified"],
+ -item["points_per_fixture"],
+ -item["points"],
+ -item["wins"],
+ item["agent_name"].lower(),
+ ),
+ )
+ rank = 0
+ for entry in ordered:
+ if entry["qualified"]:
+ rank += 1
+ entry["rank"] = rank
+ else:
+ entry["rank"] = None
+ entry["recent_fixtures"] = entry["recent_fixtures"][:5]
+ entry["recent_form"] = [item["outcome"] for item in entry["recent_fixtures"]]
+ del entry["recent_fixtures"]
+ return ordered
+
+
+def agent_fixture_history(session: Session, agent_id: str) -> list[dict[str, Any]]:
+ submissions = list(
+ session.scalars(
+ select(Submission).where(Submission.agent_id == agent_id).order_by(Submission.number)
+ )
+ )
+ submission_by_id = {submission.id: submission for submission in submissions}
+ if not submission_by_id:
+ return []
+ submission_ids = tuple(submission_by_id)
+ fixtures = list(
+ session.scalars(
+ select(Fixture)
+ .where(
+ Fixture.status == "complete",
+ or_(
+ Fixture.submission_a_id.in_(submission_ids),
+ Fixture.submission_b_id.in_(submission_ids),
+ ),
+ )
+ .order_by(Fixture.completed_at.desc(), Fixture.id)
+ )
+ )
+ history: list[dict[str, Any]] = []
+ for fixture in fixtures:
+ scored = fixture_score(session, fixture)
+ if scored is None or fixture.completed_at is None:
+ continue
+ agent_is_a = fixture.submission_a_id in submission_by_id
+ own_id = fixture.submission_a_id if agent_is_a else fixture.submission_b_id
+ opponent_id = fixture.submission_b_id if agent_is_a else fixture.submission_a_id
+ own_points = scored.points_a if agent_is_a else scored.points_b
+ opponent_points = scored.points_b if agent_is_a else scored.points_a
+ outcome = "D" if scored.is_draw else "W" if own_points == 3 else "L"
+ opponent_submission = session.get(Submission, opponent_id)
+ assert opponent_submission is not None
+ opponent = session.get(Agent, opponent_submission.agent_id)
+ assert opponent is not None
+ history.append(
+ {
+ "id": fixture.id,
+ "submission_number": submission_by_id[own_id].number,
+ "opponent_name": opponent.name,
+ "opponent_submission_number": opponent_submission.number,
+ "outcome": outcome,
+ "score": f"{own_points}-{opponent_points}",
+ "completed_at": fixture.completed_at,
+ }
+ )
+ return history
+
+
+def fixture_details(session: Session, fixture: Fixture) -> dict[str, Any]:
+ submission_a = session.get(Submission, fixture.submission_a_id)
+ submission_b = session.get(Submission, fixture.submission_b_id)
+ assert submission_a is not None and submission_b is not None
+ agent_a = session.get(Agent, submission_a.agent_id)
+ agent_b = session.get(Agent, submission_b.agent_id)
+ assert agent_a is not None and agent_b is not None
+ games = list(
+ session.scalars(select(Game).where(Game.fixture_id == fixture.id).order_by(Game.leg_number))
+ )
+ scored = fixture_score(session, fixture)
+ winner_submission_id = None
+ is_draw = False
+ points_a = points_b = 0
+ if scored is not None:
+ points_a, points_b = scored.points_a, scored.points_b
+ is_draw = scored.is_draw
+ if scored.winner == 0:
+ winner_submission_id = fixture.submission_a_id
+ elif scored.winner == 1:
+ winner_submission_id = fixture.submission_b_id
+ return {
+ "id": fixture.id,
+ "submission_a_id": fixture.submission_a_id,
+ "submission_b_id": fixture.submission_b_id,
+ "agent_a_name": agent_a.name,
+ "agent_b_name": agent_b.name,
+ "status": fixture.status,
+ "created_at": fixture.created_at,
+ "completed_at": fixture.completed_at,
+ "winner_submission_id": winner_submission_id,
+ "is_draw": is_draw,
+ "points_a": points_a,
+ "points_b": points_b,
+ "games": [
+ {
+ "id": game.id,
+ "leg_number": game.leg_number,
+ "first_submission_id": game.first_submission_id,
+ "second_submission_id": game.second_submission_id,
+ "winner_submission_id": game.winner_submission_id,
+ "end_status": game.end_status,
+ "losing_reason": game.losing_reason,
+ "replay": game.replay,
+ "engine_version": game.engine_version,
+ "ruleset_version": game.ruleset_version,
+ "started_at": game.started_at,
+ "completed_at": game.completed_at,
+ }
+ for game in games
+ ],
+ }
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/config.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/config.py
new file mode 100644
index 00000000..8c586f04
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/config.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+ROOT = Path(__file__).resolve().parents[3]
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(
+ env_file=ROOT / ".env", env_prefix="AGENT_ARENA_", extra="ignore"
+ )
+
+ database_url: str = f"sqlite:///{(ROOT / 'var' / 'agent_arena.db').as_posix()}"
+ source_root: Path = ROOT / "var" / "sources"
+ temp_root: Path = ROOT / "var" / "tmp"
+ runner_image: str = "agent-arena-runner:py312-v1"
+ move_budget_ms: int = 150
+ minimum_ranked_fixtures: int = 10
+ demo_owner_name: str = "oualid"
+ max_source_bytes: int = 131_072
+
+
+settings = Settings()
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/database.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/database.py
new file mode 100644
index 00000000..6272a39f
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/database.py
@@ -0,0 +1,33 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+from dataclasses import dataclass
+
+from fastapi import Request
+from sqlalchemy import Engine, create_engine, event
+from sqlalchemy.orm import Session, sessionmaker
+
+
+@dataclass
+class Database:
+ engine: Engine
+ sessions: sessionmaker[Session]
+
+ @classmethod
+ def create(cls, url: str) -> Database:
+ connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {}
+ engine = create_engine(url, connect_args=connect_args)
+ if url.startswith("sqlite"):
+ event.listen(engine, "connect", _enable_sqlite_foreign_keys)
+ return cls(engine=engine, sessions=sessionmaker(engine, expire_on_commit=False))
+
+
+def _enable_sqlite_foreign_keys(connection: object, _: object) -> None:
+ cursor = connection.cursor() # type: ignore[attr-defined]
+ cursor.execute("PRAGMA foreign_keys=ON")
+ cursor.close()
+
+
+def get_session(request: Request) -> Generator[Session, None, None]:
+ with request.app.state.database.sessions() as session:
+ yield session
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/main.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/main.py
new file mode 100644
index 00000000..928365bf
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/main.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+
+from .api import router
+from .config import Settings
+from .config import settings as default_settings
+from .database import Database
+
+
+def create_app(settings: Settings | None = None) -> FastAPI:
+ active_settings = settings or default_settings
+ database = Database.create(active_settings.database_url)
+
+ @asynccontextmanager
+ async def lifespan(_: FastAPI):
+ active_settings.source_root.mkdir(parents=True, exist_ok=True)
+ active_settings.temp_root.mkdir(parents=True, exist_ok=True)
+ yield
+ database.engine.dispose()
+
+ app = FastAPI(
+ title="Agent Arena local beta API",
+ version="0.2.0",
+ description=(
+ "Local Knight Isolation API. Submitted source is imported only inside "
+ "the Docker runner; this is not a production sandbox."
+ ),
+ lifespan=lifespan,
+ )
+ app.state.database = database
+ app.state.settings = active_settings
+ app.include_router(router)
+ return app
+
+
+app = create_app()
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/models.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/models.py
new file mode 100644
index 00000000..4f1287a1
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/models.py
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+import uuid
+from datetime import UTC, datetime
+from typing import Any
+
+from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
+from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
+
+
+def new_id() -> str:
+ return str(uuid.uuid4())
+
+
+def utcnow() -> datetime:
+ return datetime.now(UTC)
+
+
+class Base(DeclarativeBase):
+ pass
+
+
+class Agent(Base):
+ """Stable public identity for one competitor."""
+
+ __tablename__ = "agents"
+ __table_args__ = (UniqueConstraint("owner_name", "name", name="uq_owner_agent_name"),)
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
+ owner_name: Mapped[str] = mapped_column(String(80), index=True)
+ name: Mapped[str] = mapped_column(String(100))
+ active_submission_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
+
+
+class Submission(Base):
+ """Immutable source used by historical games."""
+
+ __tablename__ = "submissions"
+ __table_args__ = (UniqueConstraint("agent_id", "number", name="uq_agent_submission"),)
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
+ agent_id: Mapped[str] = mapped_column(ForeignKey("agents.id"), index=True)
+ number: Mapped[int] = mapped_column(Integer)
+ source_hash: Mapped[str] = mapped_column(String(64))
+ source_path: Mapped[str] = mapped_column(Text)
+ execution_kind: Mapped[str] = mapped_column(String(20), default="docker")
+ validation_result: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
+
+
+class Fixture(Base):
+ """Two mirrored games between the same submissions."""
+
+ __tablename__ = "fixtures"
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
+ submission_a_id: Mapped[str] = mapped_column(ForeignKey("submissions.id"), index=True)
+ submission_b_id: Mapped[str] = mapped_column(ForeignKey("submissions.id"), index=True)
+ status: Mapped[str] = mapped_column(String(20), default="running", index=True)
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
+
+
+class Game(Base):
+ """One leg and its single canonical replay."""
+
+ __tablename__ = "games"
+ __table_args__ = (UniqueConstraint("fixture_id", "leg_number", name="uq_fixture_leg"),)
+
+ id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
+ fixture_id: Mapped[str] = mapped_column(ForeignKey("fixtures.id"), index=True)
+ leg_number: Mapped[int] = mapped_column(Integer)
+ first_submission_id: Mapped[str] = mapped_column(ForeignKey("submissions.id"))
+ second_submission_id: Mapped[str] = mapped_column(ForeignKey("submissions.id"))
+ winner_submission_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+ end_status: Mapped[str] = mapped_column(String(30))
+ losing_reason: Mapped[str] = mapped_column(Text, default="")
+ replay: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
+ engine_version: Mapped[str] = mapped_column(String(80))
+ ruleset_version: Mapped[str] = mapped_column(String(80))
+ started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
+ completed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/schemas.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/schemas.py
new file mode 100644
index 00000000..523ac576
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/schemas.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Any, Literal
+
+from pydantic import BaseModel, Field
+
+
+class HealthResponse(BaseModel):
+ status: Literal["ok"] = "ok"
+ database: Literal["ready", "migration_required"]
+ runner: dict[str, Any]
+
+
+class RulesResponse(BaseModel):
+ game: Literal["knight_isolation"] = "knight_isolation"
+ board_width: int = 11
+ board_height: int = 9
+ runtime: str = "Python 3.12"
+ move_budget_ms: int = 150
+ minimum_ranked_fixtures: int = 10
+ fixture_games: int = 2
+ points: dict[str, int] = {"win": 3, "draw": 1, "loss": 0}
+ engine_version: str
+ ruleset_version: str
+ demo_owner_name: str
+
+
+class AgentCreate(BaseModel):
+ owner_name: str = Field(min_length=1, max_length=80)
+ name: str = Field(min_length=1, max_length=100)
+ source: str = Field(min_length=1)
+
+
+class SubmissionCreate(BaseModel):
+ source: str = Field(min_length=1)
+
+
+class PracticeCreate(BaseModel):
+ source: str = Field(min_length=1)
+ baseline: Literal["random", "greedy", "minimax"] = "greedy"
+ seed: int = 17
+
+
+class SubmissionResponse(BaseModel):
+ id: str
+ agent_id: str
+ number: int
+ source_hash: str
+ execution_kind: str
+ validation_result: dict[str, Any]
+ created_at: datetime
+ is_active: bool
+
+
+class RecordResponse(BaseModel):
+ fixtures: int
+ wins: int
+ draws: int
+ losses: int
+ points: int
+
+
+class AgentResponse(BaseModel):
+ id: str
+ owner_name: str
+ name: str
+ active_submission_id: str | None
+ created_at: datetime
+ submissions: list[SubmissionResponse]
+ record: RecordResponse | None = None
+
+
+class LeaderboardEntry(BaseModel):
+ rank: int | None
+ agent_id: str
+ agent_name: str
+ owner_name: str
+ submission_id: str
+ submission_number: int
+ fixtures: int
+ wins: int
+ draws: int
+ losses: int
+ points: int
+ points_per_fixture: float
+ qualified: bool
+ recent_form: list[Literal["W", "D", "L"]]
+
+
+class AgentFixtureHistoryEntry(BaseModel):
+ id: str
+ submission_number: int
+ opponent_name: str
+ opponent_submission_number: int
+ outcome: Literal["W", "D", "L"]
+ score: str
+ completed_at: datetime
+
+
+class PracticeResponse(BaseModel):
+ status: str
+ result: dict[str, Any]
+ replay: list[dict[str, Any]]
+ logs: list[dict[str, str]]
+
+
+class GameResponse(BaseModel):
+ id: str
+ leg_number: int
+ first_submission_id: str
+ second_submission_id: str
+ winner_submission_id: str | None
+ end_status: str
+ losing_reason: str
+ replay: list[dict[str, Any]]
+ engine_version: str
+ ruleset_version: str
+ started_at: datetime
+ completed_at: datetime
+
+
+class FixtureResponse(BaseModel):
+ id: str
+ submission_a_id: str
+ submission_b_id: str
+ agent_a_name: str
+ agent_b_name: str
+ status: str
+ created_at: datetime
+ completed_at: datetime | None
+ winner_submission_id: str | None
+ is_draw: bool
+ points_a: int
+ points_b: int
+ games: list[GameResponse]
+
+
+class RoundResponse(BaseModel):
+ fixture_ids: list[str]
+ status: Literal["complete"] = "complete"
+ participant_count: int
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/seed.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/seed.py
new file mode 100644
index 00000000..f3000854
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/seed.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+import hashlib
+
+from sqlalchemy import func, select
+
+from .competition import run_round
+from .config import Settings
+from .database import Database
+from .models import Agent, Fixture, Submission, utcnow
+
+SEED_AGENTS = (
+ (
+ "31111111-1111-4111-8111-111111111111",
+ "41111111-1111-4111-8111-111111111111",
+ "oualid",
+ "Atlas-AB",
+ "minimax",
+ ),
+ (
+ "32222222-2222-4222-8222-222222222222",
+ "42222222-2222-4222-8222-222222222222",
+ "oualid",
+ "PartitionScout",
+ "greedy",
+ ),
+ (
+ "33333333-3333-4333-8333-333333333333",
+ "43333333-3333-4333-8333-333333333333",
+ "starter-kit",
+ "GreedyFox",
+ "greedy",
+ ),
+ (
+ "34444444-4444-4444-8444-444444444444",
+ "44444444-4444-4444-8444-444444444444",
+ "starter-kit",
+ "RandomKnight",
+ "random",
+ ),
+)
+
+
+def seed_database(database: Database, settings: Settings) -> dict[str, int]:
+ with database.sessions.begin() as session:
+ for agent_id, submission_id, owner_name, name, execution_kind in SEED_AGENTS:
+ if session.get(Agent, agent_id) is not None:
+ continue
+ source_hash = hashlib.sha256(f"trusted:{execution_kind}:1".encode()).hexdigest()
+ agent = Agent(
+ id=agent_id,
+ owner_name=owner_name,
+ name=name,
+ active_submission_id=submission_id,
+ )
+ submission = Submission(
+ id=submission_id,
+ agent_id=agent_id,
+ number=1,
+ source_hash=source_hash,
+ source_path=f"builtin://{execution_kind}",
+ execution_kind=execution_kind,
+ validation_result={
+ "accepted": True,
+ "trusted_builtin": True,
+ "controller": execution_kind,
+ },
+ created_at=utcnow(),
+ )
+ session.add_all([agent, submission])
+
+ with database.sessions() as session:
+ fixture_count = session.scalar(select(func.count(Fixture.id))) or 0
+ if fixture_count == 0:
+ run_round(database, settings)
+
+ with database.sessions() as session:
+ return {
+ "agents": session.scalar(select(func.count(Agent.id))) or 0,
+ "submissions": session.scalar(select(func.count(Submission.id))) or 0,
+ "fixtures": session.scalar(select(func.count(Fixture.id))) or 0,
+ }
diff --git a/Projects/3_Adversarial Search/apps/api/agent_arena_api/submissions.py b/Projects/3_Adversarial Search/apps/api/agent_arena_api/submissions.py
new file mode 100644
index 00000000..fc2dff85
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/agent_arena_api/submissions.py
@@ -0,0 +1,307 @@
+"""Straight-line source validation, storage, and practice operations."""
+
+from __future__ import annotations
+
+import ast
+import hashlib
+from collections.abc import Iterator
+from contextlib import contextmanager
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+from agent_runner import DockerAgent
+from isolation_engine import BaselineController, Isolation, play_game
+from sqlalchemy import func, select
+from sqlalchemy.orm import Session
+
+from .config import Settings
+from .models import Agent, Submission, new_id
+
+ALLOWED_IMPORT_ROOTS = {
+ "collections",
+ "dataclasses",
+ "functools",
+ "heapq",
+ "itertools",
+ "math",
+ "operator",
+ "queue",
+ "random",
+ "statistics",
+ "typing",
+ "isolation_engine",
+}
+
+
+@dataclass(frozen=True)
+class PreparedSubmission:
+ source: bytes
+ source_hash: str
+ validation: dict[str, Any]
+
+ @property
+ def accepted(self) -> bool:
+ return bool(self.validation.get("accepted"))
+
+
+def normalize_source(source: str, max_bytes: int) -> bytes:
+ normalized = source.replace("\r\n", "\n").replace("\r", "\n")
+ encoded = normalized.encode("utf-8")
+ if len(encoded) > max_bytes:
+ raise ValueError(f"agent.py exceeds the {max_bytes} byte beta limit")
+ if b"\x00" in encoded:
+ raise ValueError("agent.py contains a null byte")
+ return encoded
+
+
+def static_source_checks(source: bytes) -> dict[str, Any]:
+ checks: dict[str, Any] = {
+ "syntax_valid": False,
+ "custom_player_present": False,
+ "get_action_present": False,
+ "imports_allowed": False,
+ }
+ try:
+ tree = ast.parse(source.decode("utf-8"), filename="agent.py")
+ except (SyntaxError, UnicodeDecodeError) as error:
+ checks["error"] = f"{type(error).__name__}: {error}"
+ return checks
+ checks["syntax_valid"] = True
+ player_class = next(
+ (
+ node
+ for node in tree.body
+ if isinstance(node, ast.ClassDef) and node.name == "CustomPlayer"
+ ),
+ None,
+ )
+ checks["custom_player_present"] = player_class is not None
+ checks["get_action_present"] = bool(
+ player_class
+ and any(
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == "get_action"
+ for node in player_class.body
+ )
+ )
+ imports: list[str] = []
+ for node in ast.walk(tree):
+ if isinstance(node, ast.Import):
+ imports.extend(alias.name.split(".")[0] for alias in node.names)
+ elif isinstance(node, ast.ImportFrom) and node.module:
+ imports.append(node.module.split(".")[0])
+ rejected = sorted({name for name in imports if name not in ALLOWED_IMPORT_ROOTS})
+ checks["imports_allowed"] = not rejected
+ checks["rejected_imports"] = rejected
+ return checks
+
+
+def prepare_submission(source: str, settings: Settings) -> PreparedSubmission:
+ """Validate source without importing it into this Python process."""
+
+ source_bytes = normalize_source(source, settings.max_source_bytes)
+ source_hash = hashlib.sha256(source_bytes).hexdigest()
+ static = static_source_checks(source_bytes)
+ validation: dict[str, Any] = {
+ "accepted": False,
+ "source_hash": source_hash,
+ "static_contract": static,
+ "sample_states": {},
+ "scrimmage": {},
+ "errors": [],
+ }
+ required = (
+ "syntax_valid",
+ "custom_player_present",
+ "get_action_present",
+ "imports_allowed",
+ )
+ if not all(static.get(name) for name in required):
+ validation["errors"].append("The Python contract or import policy is invalid.")
+ return PreparedSubmission(source_bytes, source_hash, validation)
+
+ diagnostic = DockerAgent.diagnostic(settings.runner_image)
+ validation["runner"] = diagnostic
+ if not diagnostic["available"]:
+ validation["errors"].append(diagnostic["message"])
+ return PreparedSubmission(source_bytes, source_hash, validation)
+
+ with temporary_source(settings, source_bytes) as path:
+ try:
+ states = validation_states()
+ for label, state in states.items():
+ controller = DockerAgent(path, settings.runner_image)
+ try:
+ controller.reset(state.player())
+ response = controller.choose_action(state, settings.move_budget_ms)
+ legal_actions = state.actions()
+ legal = (
+ response.action in legal_actions
+ if legal_actions
+ else response.action is None
+ )
+ validation["sample_states"][label] = {
+ "legal_response": legal,
+ "action": response.action,
+ }
+ finally:
+ controller.close()
+ samples_passed = all(
+ item["legal_response"] for item in validation["sample_states"].values()
+ )
+ if not samples_passed:
+ validation["errors"].append("The agent returned an illegal sample action.")
+ return PreparedSubmission(source_bytes, source_hash, validation)
+
+ result = play_game(
+ DockerAgent(path, settings.runner_image),
+ BaselineController("greedy", seed=7),
+ time_limit_ms=settings.move_budget_ms,
+ )
+ validation["scrimmage"] = {
+ "completed_normally": result.end_status == "game_over",
+ "end_status": result.end_status,
+ "losing_reason": result.losing_reason,
+ }
+ if result.end_status != "game_over":
+ validation["errors"].append(f"The Greedy scrimmage ended with {result.end_status}.")
+ return PreparedSubmission(source_bytes, source_hash, validation)
+ except Exception as error:
+ validation["errors"].append(f"{type(error).__name__}: {error}")
+ return PreparedSubmission(source_bytes, source_hash, validation)
+
+ validation["accepted"] = True
+ return PreparedSubmission(source_bytes, source_hash, validation)
+
+
+def save_new_agent(
+ session: Session,
+ settings: Settings,
+ *,
+ owner_name: str,
+ name: str,
+ prepared: PreparedSubmission,
+) -> Agent:
+ if not prepared.accepted:
+ raise ValueError("only an accepted submission can be stored")
+ clean_owner = owner_name.strip()
+ clean_name = name.strip()
+ existing = session.scalar(
+ select(Agent).where(
+ func.lower(Agent.owner_name) == clean_owner.lower(),
+ func.lower(Agent.name) == clean_name.lower(),
+ )
+ )
+ if existing is not None:
+ raise ValueError("this owner already has an agent with that name")
+ agent = Agent(owner_name=clean_owner, name=clean_name)
+ session.add(agent)
+ session.flush()
+ save_submission(session, settings, agent=agent, prepared=prepared)
+ return agent
+
+
+def save_submission(
+ session: Session,
+ settings: Settings,
+ *,
+ agent: Agent,
+ prepared: PreparedSubmission,
+) -> Submission:
+ if not prepared.accepted:
+ raise ValueError("only an accepted submission can be stored")
+ number = (
+ session.scalar(select(func.max(Submission.number)).where(Submission.agent_id == agent.id))
+ or 0
+ ) + 1
+ submission_id = new_id()
+ directory = settings.source_root / agent.id
+ directory.mkdir(parents=True, exist_ok=True)
+ path = directory / f"{submission_id}.py"
+ path.write_bytes(prepared.source)
+ submission = Submission(
+ id=submission_id,
+ agent_id=agent.id,
+ number=number,
+ source_hash=prepared.source_hash,
+ source_path=str(path),
+ execution_kind="docker",
+ validation_result=prepared.validation,
+ )
+ session.add(submission)
+ session.flush()
+ agent.active_submission_id = submission.id
+ return submission
+
+
+def run_practice(
+ source: str,
+ baseline: str,
+ seed: int,
+ settings: Settings,
+) -> dict[str, Any]:
+ source_bytes = normalize_source(source, settings.max_source_bytes)
+ static = static_source_checks(source_bytes)
+ required = ("syntax_valid", "custom_player_present", "get_action_present", "imports_allowed")
+ if not all(static.get(name) for name in required):
+ return {
+ "status": "invalid",
+ "result": {"reason": "The Python contract or import policy is invalid."},
+ "replay": [],
+ "logs": [{"level": "error", "message": "Static source validation failed."}],
+ }
+ diagnostic = DockerAgent.diagnostic(settings.runner_image)
+ if not diagnostic["available"]:
+ return {
+ "status": "runner_unavailable",
+ "result": {"reason": diagnostic["message"]},
+ "replay": [],
+ "logs": [{"level": "error", "message": diagnostic["message"]}],
+ }
+ with temporary_source(settings, source_bytes) as path:
+ result = play_game(
+ DockerAgent(path, settings.runner_image),
+ BaselineController(baseline, seed=seed),
+ time_limit_ms=settings.move_budget_ms,
+ )
+ return {
+ "status": "complete" if result.end_status == "game_over" else result.end_status,
+ "result": {
+ "winner": "submitted" if result.winner_player == 0 else baseline,
+ "end_status": result.end_status,
+ "losing_reason": result.losing_reason,
+ "seed": seed,
+ "engine_version": result.engine_version,
+ "ruleset_version": result.ruleset_version,
+ },
+ "replay": result.replay_dicts(),
+ "logs": [
+ {"level": "info", "message": f"Opponent: {baseline.title()}."},
+ {"level": "success", "message": f"Game ended: {result.end_status}."},
+ ],
+ }
+
+
+@contextmanager
+def temporary_source(settings: Settings, source: bytes) -> Iterator[Path]:
+ settings.temp_root.mkdir(parents=True, exist_ok=True)
+ path = settings.temp_root / f"{new_id()}.py"
+ path.write_bytes(source)
+ try:
+ yield path
+ finally:
+ path.unlink(missing_ok=True)
+
+
+def validation_states() -> dict[str, Isolation]:
+ empty = Isolation()
+ opening = empty.result(57)
+ midgame = opening.result(0)
+ for _ in range(4):
+ if midgame.terminal_test():
+ break
+ midgame = midgame.result(midgame.actions()[0])
+ terminal = Isolation()
+ while not terminal.terminal_test():
+ terminal = terminal.result(terminal.actions()[0])
+ return {"empty": empty, "opening": opening, "midgame": midgame, "terminal": terminal}
diff --git a/Projects/3_Adversarial Search/apps/api/alembic/env.py b/Projects/3_Adversarial Search/apps/api/alembic/env.py
new file mode 100644
index 00000000..f5019508
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/alembic/env.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from agent_arena_api.config import settings
+from agent_arena_api.models import Base
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+config = context.config
+config.set_main_option("sqlalchemy.url", settings.database_url)
+target_metadata = Base.metadata
+
+
+def run_migrations_offline() -> None:
+ context.configure(
+ url=settings.database_url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online() -> None:
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section, {}),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+ with connectable.connect() as connection:
+ context.configure(connection=connection, target_metadata=target_metadata)
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/Projects/3_Adversarial Search/apps/api/alembic/versions/0001_initial_beta.py b/Projects/3_Adversarial Search/apps/api/alembic/versions/0001_initial_beta.py
new file mode 100644
index 00000000..5be24380
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/api/alembic/versions/0001_initial_beta.py
@@ -0,0 +1,96 @@
+"""Create the simplified Agent Arena beta schema.
+
+Revision ID: 0001_initial_beta
+Revises:
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+revision = "0001_initial_beta"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+ op.create_table(
+ "agents",
+ sa.Column("id", sa.String(length=36), primary_key=True),
+ sa.Column("owner_name", sa.String(length=80), nullable=False),
+ sa.Column("name", sa.String(length=100), nullable=False),
+ sa.Column("active_submission_id", sa.String(length=36), nullable=True),
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
+ sa.UniqueConstraint("owner_name", "name", name="uq_owner_agent_name"),
+ )
+ op.create_index("ix_agents_owner_name", "agents", ["owner_name"])
+ op.create_table(
+ "submissions",
+ sa.Column("id", sa.String(length=36), primary_key=True),
+ sa.Column("agent_id", sa.String(length=36), sa.ForeignKey("agents.id"), nullable=False),
+ sa.Column("number", sa.Integer(), nullable=False),
+ sa.Column("source_hash", sa.String(length=64), nullable=False),
+ sa.Column("source_path", sa.Text(), nullable=False),
+ sa.Column("execution_kind", sa.String(length=20), nullable=False),
+ sa.Column("validation_result", sa.JSON(), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
+ sa.UniqueConstraint("agent_id", "number", name="uq_agent_submission"),
+ )
+ op.create_index("ix_submissions_agent_id", "submissions", ["agent_id"])
+ op.create_table(
+ "fixtures",
+ sa.Column("id", sa.String(length=36), primary_key=True),
+ sa.Column(
+ "submission_a_id",
+ sa.String(length=36),
+ sa.ForeignKey("submissions.id"),
+ nullable=False,
+ ),
+ sa.Column(
+ "submission_b_id",
+ sa.String(length=36),
+ sa.ForeignKey("submissions.id"),
+ nullable=False,
+ ),
+ sa.Column("status", sa.String(length=20), nullable=False),
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
+ )
+ op.create_index("ix_fixtures_submission_a_id", "fixtures", ["submission_a_id"])
+ op.create_index("ix_fixtures_submission_b_id", "fixtures", ["submission_b_id"])
+ op.create_index("ix_fixtures_status", "fixtures", ["status"])
+ op.create_table(
+ "games",
+ sa.Column("id", sa.String(length=36), primary_key=True),
+ sa.Column("fixture_id", sa.String(length=36), sa.ForeignKey("fixtures.id"), nullable=False),
+ sa.Column("leg_number", sa.Integer(), nullable=False),
+ sa.Column(
+ "first_submission_id",
+ sa.String(length=36),
+ sa.ForeignKey("submissions.id"),
+ nullable=False,
+ ),
+ sa.Column(
+ "second_submission_id",
+ sa.String(length=36),
+ sa.ForeignKey("submissions.id"),
+ nullable=False,
+ ),
+ sa.Column("winner_submission_id", sa.String(length=36), nullable=True),
+ sa.Column("end_status", sa.String(length=30), nullable=False),
+ sa.Column("losing_reason", sa.Text(), nullable=False),
+ sa.Column("replay", sa.JSON(), nullable=False),
+ sa.Column("engine_version", sa.String(length=80), nullable=False),
+ sa.Column("ruleset_version", sa.String(length=80), nullable=False),
+ sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
+ sa.Column("completed_at", sa.DateTime(timezone=True), nullable=False),
+ sa.UniqueConstraint("fixture_id", "leg_number", name="uq_fixture_leg"),
+ )
+ op.create_index("ix_games_fixture_id", "games", ["fixture_id"])
+
+
+def downgrade() -> None:
+ op.drop_table("games")
+ op.drop_table("fixtures")
+ op.drop_table("submissions")
+ op.drop_table("agents")
diff --git a/Projects/3_Adversarial Search/apps/web/eslint.config.js b/Projects/3_Adversarial Search/apps/web/eslint.config.js
new file mode 100644
index 00000000..95903e0f
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/eslint.config.js
@@ -0,0 +1,25 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+
+export default tseslint.config(
+ { ignores: ['dist', 'coverage'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2022,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+ },
+ },
+)
diff --git a/Projects/3_Adversarial Search/apps/web/index.html b/Projects/3_Adversarial Search/apps/web/index.html
new file mode 100644
index 00000000..166aa06d
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Agent Arena · Knight Isolation
+
+
+
+
+
+
diff --git a/Projects/3_Adversarial Search/apps/web/package-lock.json b/Projects/3_Adversarial Search/apps/web/package-lock.json
new file mode 100644
index 00000000..2086ab9b
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/package-lock.json
@@ -0,0 +1,4679 @@
+{
+ "name": "@agent-arena/web",
+ "version": "0.2.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@agent-arena/web",
+ "version": "0.2.0",
+ "dependencies": {
+ "@vitejs/plugin-react": "4.6.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "vite": "7.3.6"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.30.1",
+ "@testing-library/jest-dom": "6.6.3",
+ "@testing-library/react": "16.3.0",
+ "@testing-library/user-event": "14.6.1",
+ "@types/node": "24.0.13",
+ "@types/react": "19.1.8",
+ "@types/react-dom": "19.1.6",
+ "eslint": "9.30.1",
+ "eslint-plugin-react-hooks": "5.2.0",
+ "eslint-plugin-react-refresh": "0.4.20",
+ "globals": "16.3.0",
+ "jsdom": "26.1.0",
+ "typescript": "5.8.3",
+ "typescript-eslint": "8.36.0",
+ "vitest": "3.2.7"
+ },
+ "engines": {
+ "node": ">=22.17.0 <25"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz",
+ "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.3.0",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.30.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz",
+ "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "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==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "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/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
+ "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
+ "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "chalk": "^3.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "lodash": "^4.17.21",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
+ "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.0.13",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz",
+ "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.8.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.1.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
+ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.1.6",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
+ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
+ "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.36.0",
+ "@typescript-eslint/type-utils": "8.36.0",
+ "@typescript-eslint/utils": "8.36.0",
+ "@typescript-eslint/visitor-keys": "8.36.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.36.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
+ "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.36.0",
+ "@typescript-eslint/types": "8.36.0",
+ "@typescript-eslint/typescript-estree": "8.36.0",
+ "@typescript-eslint/visitor-keys": "8.36.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
+ "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.36.0",
+ "@typescript-eslint/types": "^8.36.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
+ "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.36.0",
+ "@typescript-eslint/visitor-keys": "8.36.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
+ "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
+ "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.36.0",
+ "@typescript-eslint/utils": "8.36.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
+ "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
+ "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.36.0",
+ "@typescript-eslint/tsconfig-utils": "8.36.0",
+ "@typescript-eslint/types": "8.36.0",
+ "@typescript-eslint/visitor-keys": "8.36.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
+ "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.36.0",
+ "@typescript-eslint/types": "8.36.0",
+ "@typescript-eslint/typescript-estree": "8.36.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
+ "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.36.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz",
+ "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.27.4",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.19",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
+ "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
+ "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.7",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
+ "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
+ "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.7",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
+ "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
+ "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
+ "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.7",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.43",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
+ "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "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.6",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
+ "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
+ "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.42",
+ "caniuse-lite": "^1.0.30001803",
+ "electron-to-chromium": "^1.5.389",
+ "node-releases": "^2.0.51",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001805",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz",
+ "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "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/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.389",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
+ "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.30.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz",
+ "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.0",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.30.1",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
+ "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "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/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "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/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "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/globals": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
+ "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-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-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/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/js-yaml": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
+ "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/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/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.51",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "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/postcss": {
+ "version": "8.5.18",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.18.tgz",
+ "integrity": "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ==",
+ "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/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/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": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.1.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "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/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/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "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": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.36.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz",
+ "integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.36.0",
+ "@typescript-eslint/parser": "8.36.0",
+ "@typescript-eslint/utils": "8.36.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+ "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "devOptional": 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==",
+ "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/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
+ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.7",
+ "@vitest/mocker": "3.2.7",
+ "@vitest/pretty-format": "^3.2.7",
+ "@vitest/runner": "3.2.7",
+ "@vitest/snapshot": "3.2.7",
+ "@vitest/spy": "3.2.7",
+ "@vitest/utils": "3.2.7",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.7",
+ "@vitest/ui": "3.2.7",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/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/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/Projects/3_Adversarial Search/apps/web/package.json b/Projects/3_Adversarial Search/apps/web/package.json
new file mode 100644
index 00000000..1c569bab
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@agent-arena/web",
+ "private": true,
+ "version": "0.2.0",
+ "type": "module",
+ "engines": {
+ "node": ">=22.17.0 <25"
+ },
+ "scripts": {
+ "dev": "vite --host 127.0.0.1 --configLoader runner",
+ "build": "tsc --noEmit && vite build --configLoader runner",
+ "lint": "eslint .",
+ "typecheck": "tsc --noEmit --pretty false",
+ "test": "vitest --configLoader runner"
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "4.6.0",
+ "vite": "7.3.6",
+ "react": "19.1.0",
+ "react-dom": "19.1.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.30.1",
+ "@testing-library/jest-dom": "6.6.3",
+ "@testing-library/react": "16.3.0",
+ "@testing-library/user-event": "14.6.1",
+ "@types/node": "24.0.13",
+ "@types/react": "19.1.8",
+ "@types/react-dom": "19.1.6",
+ "eslint": "9.30.1",
+ "eslint-plugin-react-hooks": "5.2.0",
+ "eslint-plugin-react-refresh": "0.4.20",
+ "globals": "16.3.0",
+ "jsdom": "26.1.0",
+ "typescript": "5.8.3",
+ "typescript-eslint": "8.36.0",
+ "vitest": "3.2.7"
+ }
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/App.test.tsx b/Projects/3_Adversarial Search/apps/web/src/App.test.tsx
new file mode 100644
index 00000000..48a1875d
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/App.test.tsx
@@ -0,0 +1,267 @@
+import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { beforeEach, describe, expect, test, vi } from 'vitest'
+
+import App from './App'
+
+const mockApi = vi.hoisted(() => ({
+ health: vi.fn(),
+ rules: vi.fn(),
+ leaderboard: vi.fn(),
+ agents: vi.fn(),
+ agent: vi.fn(),
+ agentFixtures: vi.fn(),
+ featuredFixture: vi.fn(),
+ fixture: vi.fn(),
+ createAgent: vi.fn(),
+ createSubmission: vi.fn(),
+ practice: vi.fn(),
+ runRound: vi.fn(),
+}))
+
+vi.mock('./api', () => ({ api: mockApi }))
+
+const blankBoard = '41523161203939122082683632224299007'
+const replay = [
+ {
+ ply_index: 0,
+ active_player: 0,
+ board: blankBoard,
+ player_locations: [null, null],
+ chosen_action: 57,
+ chosen_destination: 57,
+ legal_actions: [57],
+ elapsed_ms: 1,
+ stdout_excerpt: '',
+ stderr_excerpt: '',
+ outcome: null,
+ },
+ {
+ ply_index: 1,
+ active_player: 1,
+ board: blankBoard,
+ player_locations: [57, null],
+ chosen_action: null,
+ chosen_destination: null,
+ legal_actions: [],
+ elapsed_ms: 0,
+ stdout_excerpt: '',
+ stderr_excerpt: '',
+ outcome: 'game_over',
+ },
+]
+const rules = {
+ game: 'knight_isolation',
+ board_width: 11,
+ board_height: 9,
+ runtime: 'Python 3.12',
+ move_budget_ms: 150,
+ fixture_games: 2,
+ points: { win: 3, draw: 1, loss: 0 },
+ engine_version: 'engine-v1',
+ ruleset_version: 'rules-v1',
+ demo_owner_name: 'oualid',
+ minimum_ranked_fixtures: 10,
+}
+const table = [{
+ rank: 1,
+ agent_id: 'agent-1',
+ agent_name: 'Atlas-AB',
+ owner_name: 'oualid',
+ submission_id: 'submission-1',
+ submission_number: 2,
+ fixtures: 12,
+ wins: 2,
+ draws: 1,
+ losses: 0,
+ points: 25,
+ points_per_fixture: 2.083,
+ qualified: true,
+ recent_form: ['W', 'D'],
+}]
+const submission = {
+ id: 'submission-1',
+ agent_id: 'agent-1',
+ number: 2,
+ source_hash: 'a'.repeat(64),
+ execution_kind: 'docker',
+ validation_result: { accepted: true },
+ created_at: '2026-07-13T00:00:00Z',
+ is_active: true,
+}
+const agents = [{
+ id: 'agent-1',
+ owner_name: 'oualid',
+ name: 'Atlas-AB',
+ active_submission_id: 'submission-1',
+ created_at: '2026-07-13T00:00:00Z',
+ submissions: [submission, { ...submission, id: 'old', number: 1, is_active: false }],
+ record: { fixtures: 3, wins: 2, draws: 1, losses: 0, points: 7 },
+}]
+const fixtureHistory = [{
+ id: 'fixture-1',
+ submission_number: 2,
+ opponent_name: 'GreedyFox',
+ opponent_submission_number: 1,
+ outcome: 'W',
+ score: '3-0',
+ completed_at: '2026-07-13T00:00:00Z',
+}]
+const fixture = {
+ id: 'fixture-1',
+ submission_a_id: 'submission-1',
+ submission_b_id: 'submission-2',
+ agent_a_name: 'Atlas-AB',
+ agent_b_name: 'GreedyFox',
+ status: 'complete',
+ winner_submission_id: 'submission-1',
+ is_draw: false,
+ points_a: 3,
+ points_b: 0,
+ games: [1, 2].map((leg) => ({
+ id: `game-${leg}`,
+ leg_number: leg,
+ first_submission_id: leg === 1 ? 'submission-1' : 'submission-2',
+ second_submission_id: leg === 1 ? 'submission-2' : 'submission-1',
+ winner_submission_id: 'submission-1',
+ end_status: 'game_over',
+ losing_reason: 'no legal moves',
+ replay,
+ engine_version: 'engine-v1',
+ ruleset_version: 'rules-v1',
+ })),
+}
+const completedPractice = {
+ status: 'complete',
+ result: { winner: 'submitted' },
+ logs: [{ level: 'success', message: 'Real game complete.' }],
+ replay,
+}
+
+beforeEach(() => {
+ window.history.replaceState(null, '', '/')
+ vi.clearAllMocks()
+ mockApi.rules.mockResolvedValue(rules)
+ mockApi.health.mockResolvedValue({
+ status: 'ok',
+ runner: { available: true, code: 'ready', message: 'Runner ready.' },
+ })
+ mockApi.leaderboard.mockResolvedValue(table)
+ mockApi.agents.mockResolvedValue(agents)
+ mockApi.agent.mockResolvedValue(agents[0])
+ mockApi.agentFixtures.mockResolvedValue(fixtureHistory)
+ mockApi.featuredFixture.mockResolvedValue(fixture)
+ mockApi.fixture.mockResolvedValue(fixture)
+ mockApi.practice.mockResolvedValue(completedPractice)
+ mockApi.runRound.mockResolvedValue({ fixture_ids: ['fixture-1'], status: 'complete' })
+ mockApi.createAgent.mockResolvedValue(agents[0])
+ mockApi.createSubmission.mockResolvedValue(agents[0])
+})
+
+test('route navigation and editor-to-submission handoff preserve code', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(screen.getByRole('button', { name: /Code & Play/ }))
+ const editor = screen.getByRole('textbox', { name: 'agent.py editor' })
+ fireEvent.change(editor, { target: { value: 'class CustomPlayer:\n pass' } })
+ await user.click(screen.getByRole('button', { name: 'Prepare submission' }))
+ expect(await screen.findByRole('heading', { name: 'Submit Agent' })).toBeInTheDocument()
+ expect(screen.getByRole('textbox', { name: 'Submission source' })).toHaveValue(
+ 'class CustomPlayer:\n pass',
+ )
+})
+
+test('synchronous practice renders logs and replay controls', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(screen.getByRole('button', { name: /Code & Play/ }))
+ await user.click(screen.getByRole('button', { name: 'Run practice' }))
+ expect(await screen.findByText('Real game complete.')).toBeInTheDocument()
+ expect(mockApi.practice).toHaveBeenCalled()
+ expect(screen.getAllByText('complete').length).toBeGreaterThan(0)
+ expect(screen.getByRole('slider', { name: 'Replay position' })).toBeInTheDocument()
+})
+
+test('submission is validated and activated in one visible action', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(primaryNavigation().getByRole('button', { name: /Submit Agent/ }))
+ await user.click(screen.getByRole('button', { name: 'Validate and submit' }))
+ expect(await screen.findByText(/Accepted submission 2/)).toBeInTheDocument()
+ expect(mockApi.createAgent).toHaveBeenCalled()
+})
+
+test('competition round refreshes the leaderboard without opening fixture details', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(primaryNavigation().getByRole('button', { name: /Leaderboard/ }))
+ await user.click(screen.getByRole('button', { name: 'Run competition round' }))
+ expect(await screen.findByText('Completed 1 fixtures.')).toBeInTheDocument()
+ expect(mockApi.runRound).toHaveBeenCalled()
+ expect(screen.queryByRole('region', { name: /Game 1.*replay/ })).not.toBeInTheDocument()
+})
+
+test('leaderboard summary opens a dedicated agent page with fixture history and replays', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(primaryNavigation().getByRole('button', { name: /Leaderboard/ }))
+ expect(screen.queryByText('Recent fixtures')).not.toBeInTheDocument()
+ await user.click(screen.getByRole('button', { name: 'Open Agent page' }))
+ await waitFor(() => expect(mockApi.agent).toHaveBeenCalledWith('agent-1'))
+ expect(window.location.hash).toBe('#/agent/agent-1')
+ expect(await screen.findByText('Agent details and immutable competition history for @oualid.')).toBeInTheDocument()
+ await user.click(screen.getByRole('tab', { name: 'Fixture history' }))
+ await user.click(screen.getByRole('button', { name: /Submission 2 vs GreedyFox v1/ }))
+ const gameOne = within(screen.getByRole('region', { name: /Game 1.*replay/ }))
+ const gameTwo = within(screen.getByRole('region', { name: /Game 2.*replay/ }))
+ expect(gameOne.getByText('Atlas-AB').closest('.replay-player')).toHaveClass('p1')
+ expect(gameOne.getByLabelText('Game score 1 to 0')).toBeInTheDocument()
+ expect(gameTwo.getByText('Atlas-AB').closest('.replay-player')).toHaveClass('p2')
+ expect(gameTwo.getByLabelText('Game score 0 to 1')).toBeInTheDocument()
+})
+
+test('My Agents shows active and previous immutable submissions', async () => {
+ const user = userEvent.setup()
+ render( )
+ await user.click(primaryNavigation().getByRole('button', { name: /My Agents/ }))
+ expect((await screen.findAllByText('src aaaaaaaaaaaa')).length).toBe(2)
+ expect(screen.getByText('previous')).toBeInTheDocument()
+ expect(screen.getByText('7 pts')).toBeInTheDocument()
+})
+
+describe.each(['timeout', 'invalid_action'])('practice failure %s', (failure) => {
+ test('is presented explicitly', async () => {
+ mockApi.practice.mockResolvedValue({
+ ...completedPractice,
+ status: failure,
+ logs: [{ level: 'error', message: `Runner ended with ${failure}` }],
+ })
+ const user = userEvent.setup()
+ render( )
+ await user.click(screen.getByRole('button', { name: /Code & Play/ }))
+ await user.click(screen.getByRole('button', { name: 'Run practice' }))
+ expect(await screen.findByText(`Runner ended with ${failure}`)).toBeInTheDocument()
+ expect(screen.getAllByText(failure.replace('_', ' ')).length).toBeGreaterThan(0)
+ })
+})
+
+test('malicious names and logs are rendered as inert text', async () => {
+ mockApi.leaderboard.mockResolvedValue([{ ...table[0], agent_name: ' ' }])
+ mockApi.practice.mockResolvedValue({
+ ...completedPractice,
+ logs: [{ level: 'error', message: '' }],
+ })
+ const user = userEvent.setup()
+ render( )
+ await user.click(screen.getByRole('button', { name: /Code & Play/ }))
+ await user.click(screen.getByRole('button', { name: 'Run practice' }))
+ expect(await screen.findByText('')).toBeInTheDocument()
+ expect(document.querySelector('script:not([type="module"])')).toBeNull()
+ await user.click(primaryNavigation().getByRole('button', { name: /Leaderboard/ }))
+ await waitFor(() => expect(screen.getAllByText(' ').length).toBeGreaterThan(0))
+ expect(document.querySelector('img')).toBeNull()
+})
+
+function primaryNavigation() {
+ return within(screen.getByRole('navigation', { name: 'Primary navigation' }))
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/App.tsx b/Projects/3_Adversarial Search/apps/web/src/App.tsx
new file mode 100644
index 00000000..a14bd0df
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/App.tsx
@@ -0,0 +1,786 @@
+import { useCallback, useEffect, useState, type ReactNode } from 'react'
+
+import { api } from './api'
+import { ReplayViewer, type ReplayPlayer } from './components/ReplayBoard'
+import type {
+ Agent,
+ AgentFixtureHistoryEntry,
+ Fixture,
+ Game,
+ LeaderboardEntry,
+ PracticeResult,
+ Rules,
+ ViewName,
+} from './types'
+
+type AppView = ViewName | 'agent'
+
+interface AppRoute {
+ view: AppView
+ agentId?: string
+}
+
+const views: Array<{ id: ViewName; label: string; icon: string }> = [
+ { id: 'overview', label: 'Overview', icon: '◫' },
+ { id: 'code', label: 'Code & Play', icon: '⌨' },
+ { id: 'leaderboard', label: 'Leaderboard', icon: '↗' },
+ { id: 'agents', label: 'My Agents', icon: '◇' },
+ { id: 'submit', label: 'Submit Agent', icon: '⇧' },
+]
+
+const starterCode = `class CustomPlayer:
+ """Return a legal action before the 150 ms deadline."""
+
+ def __init__(self, player_id=0):
+ self.player_id = player_id
+ self.context = None
+
+ def get_action(self, state):
+ actions = state.actions()
+ if not actions:
+ return None
+
+ return max(actions, key=lambda action: self.score(state.result(action)))
+
+ def score(self, state):
+ own = state.liberties(state.locs[self.player_id])
+ opponent = state.liberties(state.locs[1 - self.player_id])
+ return len(own) - len(opponent)
+`
+
+export default function App() {
+ const [route, navigate] = useHashRoute()
+ const view = route.view
+ const [rules, setRules] = useState(null)
+ const [runner, setRunner] = useState<{ available: boolean; message: string } | null>(null)
+ const [leaderboard, setLeaderboard] = useState([])
+ const [agents, setAgents] = useState([])
+ const [featured, setFeatured] = useState(null)
+ const [preparedSource, setPreparedSource] = useState(starterCode)
+ const [submissionTarget, setSubmissionTarget] = useState(null)
+ const [error, setError] = useState('')
+
+ const refresh = useCallback(async () => {
+ try {
+ const [nextRules, health, table, localAgents, fixture] = await Promise.all([
+ api.rules(),
+ api.health(),
+ api.leaderboard(),
+ api.agents(),
+ api.featuredFixture(),
+ ])
+ setRules(nextRules)
+ setRunner(health.runner)
+ setLeaderboard(table)
+ setAgents(localAgents.filter((agent) => agent.owner_name === nextRules.demo_owner_name))
+ setFeatured(fixture)
+ setError('')
+ } catch (loadError) {
+ setError(loadError instanceof Error ? loadError.message : 'The API could not be loaded.')
+ }
+ }, [])
+
+ useEffect(() => {
+ void refresh()
+ }, [refresh])
+
+ const prepareSubmission = (source: string) => {
+ setPreparedSource(source)
+ setSubmissionTarget(null)
+ navigate('submit')
+ }
+
+ const submitNewVersion = (agentId: string) => {
+ setSubmissionTarget(agentId)
+ navigate('submit')
+ }
+
+ return (
+
+
+ navigate('overview')}>
+ ♞
+ Agent Arena Knight Isolation
+
+ Competition
+
+ {views.map((item) => (
+ navigate(item.id)}
+ aria-current={view === item.id ? 'page' : undefined}
+ >
+ {item.icon} {item.label}
+
+ ))}
+
+
+ {runner?.available ? 'Runner ready' : 'Execution unavailable'}
+ {runner?.message ?? 'Checking the Docker runner…'}
+
+
+
+
+
+ Knight Isolation / {views.find((item) => item.id === view)?.label ?? 'Agent'}
+
+ Points from completed fixtures
+ navigate('code')}>Open Code Lab
+
+
+
+
+ {error ?
API unavailable: {error}
: null}
+ {view === 'overview' ? (
+
+ ) : null}
+ {view === 'code' ? (
+
+ ) : null}
+ {view === 'leaderboard' ? (
+
navigate('agent', agentId)}
+ onRefresh={refresh}
+ />
+ ) : null}
+ {view === 'agent' && route.agentId ? (
+ navigate('leaderboard')} />
+ ) : null}
+ {view === 'agents' ? (
+
+ ) : null}
+ {view === 'submit' ? (
+
+ ) : null}
+
+
+
+
+ {views.map((item) => (
+ navigate(item.id)}>
+ {item.icon} {item.label.split(' ')[0]}
+
+ ))}
+
+
+ )
+}
+
+function Overview({
+ rules,
+ table,
+ featured,
+ navigate,
+}: {
+ rules: Rules | null
+ table: LeaderboardEntry[]
+ featured: Fixture | null
+ navigate: (view: ViewName) => void
+}) {
+ return (
+
+
+
+
One game · one points table
+
Submit once. Your Knight Isolation agent keeps competing.
+
Write a Python agent, test it against a trusted baseline, submit one immutable version, and inspect every mirrored fixture.
+
+ navigate('code')}>Write an agent
+ navigate('leaderboard')}>View leaderboard
+
+
+
+ Fixed competition rules
+
+
+
+
+
+ Every ranked fixture contains two games. Player order swaps, then points are calculated from the two winners.
+
+
+
+
+ {[
+ ['01', 'Code', 'Implement CustomPlayer.get_action(state).'],
+ ['02', 'Practice', 'Run one real game against a baseline.'],
+ ['03', 'Submit', 'Validation succeeds before source is stored.'],
+ ['04', 'Compete', 'Mirrored games produce fixture points.'],
+ ].map(([number, title, copy]) => (
+
{number} {title} {copy}
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function CodeAndPlay({
+ runnerAvailable,
+ onPrepare,
+}: {
+ runnerAvailable: boolean
+ onPrepare: (source: string) => void
+}) {
+ const [source, setSource] = useState(starterCode)
+ const [baseline, setBaseline] = useState('greedy')
+ const [practice, setPractice] = useState(null)
+ const [running, setRunning] = useState(false)
+ const [tab, setTab] = useState<'goal' | 'rules' | 'api'>('goal')
+
+ const runPractice = async () => {
+ setRunning(true)
+ setPractice(null)
+ try {
+ setPractice(await api.practice(source, baseline))
+ } catch (runError) {
+ setPractice({
+ status: 'exception',
+ result: {},
+ replay: [],
+ logs: [{ level: 'error', message: runError instanceof Error ? runError.message : 'Practice request failed.' }],
+ })
+ } finally {
+ setRunning(false)
+ }
+ }
+
+ const status = running ? 'running' : practice?.status ?? 'idle'
+ const lines = source.split('\n').map((_, index) => index + 1).join('\n')
+ return (
+
+
+
+
+
+
+
YourAgent vs {titleCase(baseline)} Practice · seed 17 · no table points
+
{status.replace('_', ' ')}
+
+
+
+
+
+ {(['goal', 'rules', 'api'] as const).map((name) => (
+ setTab(name)}>{titleCase(name)}
+ ))}
+
+ {tab === 'goal' ?
: null}
+ {tab === 'rules' ?
: null}
+ {tab === 'api' ?
: null}
+
+
+
+
+
+
agent.py Python 3.12
+
+ setBaseline(event.target.value)}>
+ Random
+ Greedy
+ Minimax
+
+ setSource(starterCode)}>Reset
+
+
+
+
+
+
Runner output {status.replace('_', ' ')}
+
+ {!runnerAvailable ?
Docker is unavailable. Submitted source will not execute.
: null}
+ {!practice && !running ?
Choose a baseline and run one practice game.
: null}
+ {running ?
Waiting for the isolated game to finish…
: null}
+ {practice?.logs.map((log, index) =>
{log.message}
)}
+
+
+ void runPractice()}>Run practice
+ void runPractice()}>Replay same
+ onPrepare(source)}>Prepare submission
+
+
+
+
+
+ )
+}
+
+function Leaderboard({
+ table,
+ minimumFixtures,
+ onOpenAgent,
+ onRefresh,
+}: {
+ table: LeaderboardEntry[]
+ minimumFixtures: number
+ onOpenAgent: (agentId: string) => void
+ onRefresh: () => Promise
+}) {
+ const [selectedId, setSelectedId] = useState(null)
+ const [runningRound, setRunningRound] = useState(false)
+ const [message, setMessage] = useState('')
+ const selected = table.find((entry) => entry.submission_id === selectedId) ?? table[0]
+
+ const runCompetition = async () => {
+ setRunningRound(true)
+ setMessage('Running every mirrored fixture. This request stays open until the round finishes.')
+ try {
+ const result = await api.runRound()
+ setMessage(`Completed ${result.fixture_ids.length} fixtures.`)
+ await onRefresh()
+ } catch (roundError) {
+ setMessage(roundError instanceof Error ? roundError.message : 'The round failed.')
+ } finally {
+ setRunningRound(false)
+ }
+ }
+
+ return (
+
+ void runCompetition()}>{runningRound ? 'Running round…' : 'Run competition round'}}
+ />
+ {message ? {message}
: null}
+
+ Official ranking starts at {minimumFixtures} fixtures.
+ Qualified agents are ordered by points per fixture, then total points, wins, and agent name. New agents remain provisional until they reach the threshold.
+
+
+
setSelectedId(entry.submission_id)} selectedId={selected?.submission_id} />
+
+
+
+ )
+}
+
+function AgentPage({ agentId, onBack }: { agentId: string; onBack: () => void }) {
+ const [agent, setAgent] = useState(null)
+ const [history, setHistory] = useState([])
+ const [selectedFixture, setSelectedFixture] = useState(null)
+ const [tab, setTab] = useState<'submissions' | 'fixtures'>('submissions')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ let active = true
+ setLoading(true)
+ setSelectedFixture(null)
+ void Promise.all([api.agent(agentId), api.agentFixtures(agentId)])
+ .then(([nextAgent, nextHistory]) => {
+ if (!active) return
+ setAgent(nextAgent)
+ setHistory(nextHistory)
+ setError('')
+ })
+ .catch((loadError: unknown) => {
+ if (active) setError(loadError instanceof Error ? loadError.message : 'Agent could not be loaded.')
+ })
+ .finally(() => {
+ if (active) setLoading(false)
+ })
+ return () => { active = false }
+ }, [agentId])
+
+ const loadFixture = async (fixtureId: string) => {
+ try {
+ setSelectedFixture(await api.fixture(fixtureId))
+ setError('')
+ } catch (fixtureError) {
+ setError(fixtureError instanceof Error ? fixtureError.message : 'Fixture could not be loaded.')
+ }
+ }
+
+ if (loading) return Loading agent page…
+ if (!agent) {
+ return {error || 'Agent was not found.'}
Back to leaderboard
+ }
+
+ const record = agent.record
+ const pointsPerFixture = record?.fixtures ? record.points / record.fixtures : 0
+ return (
+
+ Back to leaderboard}
+ />
+ {error ? {error}
: null}
+
+
{agent.active_submission_id ? 'Active' : 'Inactive'}
{agent.name} @{agent.owner_name}
+
+
+
+
+
+
+
+
+ setTab('submissions')}>Submission history
+ setTab('fixtures')}>Fixture history
+
+ {tab === 'submissions' ? (
+
+ {agent.submissions.map((submission) => (
+
+ Submission {submission.number}
+ src {submission.source_hash}{submission.execution_kind} · {submission.validation_result.accepted ? 'validated' : 'trusted'} · {new Date(submission.created_at).toLocaleString()}
+ {submission.is_active ? 'active' : 'previous'}
+
+ ))}
+
+ ) : (
+
+ {history.map((fixture) => (
+ void loadFixture(fixture.id)}>
+ {fixture.outcome}
+ Submission {fixture.submission_number} vs {fixture.opponent_name} v{fixture.opponent_submission_number}
+ {fixture.score}
+ {new Date(fixture.completed_at).toLocaleString()}
+
+ ))}
+ {!history.length ? No completed fixtures yet. : null}
+
+ )}
+ {selectedFixture ? : null}
+
+ )
+}
+
+function MyAgents({
+ agents,
+ onNewSubmission,
+ onRefresh,
+}: {
+ agents: Agent[]
+ onNewSubmission: (id: string) => void
+ onRefresh: () => Promise
+}) {
+ return (
+
+ void onRefresh()}>Refresh}
+ />
+
+ {agents.map((agent) => (
+
+
+
{agent.active_submission_id ? 'Active' : 'No submission'}
{agent.name} @{agent.owner_name}
+
{agent.record?.points ?? 0} pts
+
+
+
+
+
+
+
+ {agent.submissions.map((submission) => (
+
+ v{submission.number}
+ src {submission.source_hash.slice(0, 12)}{submission.validation_result.accepted ? 'validated' : 'trusted built-in'}
+ {submission.is_active ? 'active' : 'previous'}
+
+ ))}
+
+ onNewSubmission(agent.id)}>Submit new version
+
+ ))}
+ {!agents.length ?
No agents yet. Create one in Submit Agent.
: null}
+
+
+ )
+}
+
+function SubmitAgent({
+ agents,
+ initialOwner,
+ initialSource,
+ initialTarget,
+ onComplete,
+}: {
+ agents: Agent[]
+ initialOwner: string
+ initialSource: string
+ initialTarget: string | null
+ onComplete: () => Promise
+}) {
+ const [mode, setMode] = useState<'new' | 'submission'>(initialTarget ? 'submission' : 'new')
+ const [owner, setOwner] = useState(initialOwner)
+ const [name, setName] = useState('MyKnightAgent')
+ const [target, setTarget] = useState(initialTarget ?? agents[0]?.id ?? '')
+ const [source, setSource] = useState(initialSource)
+ const [submitting, setSubmitting] = useState(false)
+ const [result, setResult] = useState(null)
+ const [message, setMessage] = useState('Validation happens before a permanent Submission is created.')
+
+ useEffect(() => {
+ if (initialTarget) {
+ setMode('submission')
+ setTarget(initialTarget)
+ }
+ }, [initialTarget])
+
+ const submit = async () => {
+ setSubmitting(true)
+ setResult(null)
+ setMessage('Checking the Python contract, isolated sample actions, and Greedy scrimmage…')
+ try {
+ const accepted = mode === 'new'
+ ? await api.createAgent(owner, name, source)
+ : await api.createSubmission(target, source)
+ setResult(accepted)
+ const active = accepted.submissions.find((item) => item.is_active)
+ setMessage(`Accepted submission ${active?.number ?? ''}. It is now the active competitor.`)
+ await onComplete()
+ } catch (submitError) {
+ setMessage(submitError instanceof Error ? submitError.message : 'Submission failed.')
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ const loadFile = async (file: File | undefined) => {
+ if (file) setSource(await file.text())
+ }
+
+ const active = result?.submissions.find((item) => item.is_active)
+ return (
+
+
+
+
+
+ setMode('new')}>New agent
+ setMode('submission')}>New submission
+
+
+ {mode === 'new' ? (
+ <>
+
Owner name setOwner(event.target.value)} />
+
Agent name setName(event.target.value)} />
+ >
+ ) : (
+
Agent setTarget(event.target.value)}>Choose an agent {agents.map((agent) => {agent.name} )}
+ )}
+
GameKnight Isolation
+
RuntimePython 3.12 · 150 ms
+
Upload agent.py void loadFile(event.target.files?.[0])} />
+
Source
+
+
+ void submit()}>{submitting ? 'Validating…' : 'Validate and submit'}
+
+
+
+ {active ? 'accepted' : submitting ? 'validating' : 'ready'}
+ Submission result
+ {message}
+ {active ? (
+
+
submission {active.id}
+
source {active.source_hash}
+
Validation result {JSON.stringify(active.validation_result, null, 2)}
+
+ ) : null}
+
+
+
+ )
+}
+
+function FixtureSummary({ fixture }: { fixture: Fixture | null }) {
+ return (
+
+
{fixture?.status ?? 'Not seeded'}
+
{fixture ? `${fixture.agent_a_name} ${fixture.points_a}-${fixture.points_b} ${fixture.agent_b_name}` : 'Seed data to see a fixture'}
+
Two games, order swapped. Points are derived from the recorded winners.
+ {fixture?.games.map((game) => (
+
+ Game {game.leg_number}
+ {game.end_status.replace('_', ' ')}
+ {Math.max(0, game.replay.length - 1)} plies
+
+ ))}
+
+ )
+}
+
+function FixtureReplays({ fixture }: { fixture: Fixture }) {
+ return (
+
+
+
+ {fixture.games.map((game) => (
+
+ ))}
+
+
+ )
+}
+
+function LeaderboardTable({
+ rows,
+ onSelect,
+ selectedId,
+}: {
+ rows: LeaderboardEntry[]
+ onSelect?: (entry: LeaderboardEntry) => void
+ selectedId?: string
+}) {
+ return (
+
+
+ Rank Agent submission Owner Fixtures PPF W D L Points Form
+
+ {rows.map((entry) => (
+ onSelect?.(entry)} tabIndex={onSelect ? 0 : undefined} onKeyDown={(event) => { if (onSelect && (event.key === 'Enter' || event.key === ' ')) onSelect(entry) }}>
+ {entry.rank ?? '—'}
+ {entry.agent_name} v{entry.submission_number} {!entry.qualified ? provisional : null}
+ @{entry.owner_name}
+ {entry.fixtures} {entry.points_per_fixture.toFixed(3)} {entry.wins} {entry.draws} {entry.losses}
+ {entry.points}
+ {entry.recent_form.map((outcome, index) => {outcome} )}
+
+ ))}
+
+
+ {!rows.length ?
No completed standings yet. Run the seed command.
: null}
+
+ )
+}
+
+function SectionHead({
+ title,
+ copy,
+ id,
+ action,
+}: {
+ title: string
+ copy: string
+ id?: string
+ action?: ReactNode
+}) {
+ return
+}
+
+function Metric({ value, label }: { value: string; label: string }) {
+ return {value} {label}
+}
+
+function Problem({ title, items }: { title: string; items: string[] }) {
+ return {title} {items.map((item) => {item} )}
+}
+
+function useHashRoute(): [AppRoute, (view: AppView, agentId?: string) => void] {
+ const read = (): AppRoute => {
+ const value = window.location.hash.replace('#/', '')
+ const [view, encodedId] = value.split('/')
+ if (view === 'agent' && encodedId) return { view: 'agent', agentId: decodeURIComponent(encodedId) }
+ if (views.some((item) => item.id === view)) return { view: view as ViewName }
+ return { view: 'overview' }
+ }
+ const [route, setRoute] = useState(read)
+ useEffect(() => {
+ const onHash = () => setRoute(read())
+ window.addEventListener('hashchange', onHash)
+ return () => window.removeEventListener('hashchange', onHash)
+ }, [])
+ return [route, (next, agentId) => {
+ window.location.hash = next === 'agent' && agentId
+ ? `/agent/${encodeURIComponent(agentId)}`
+ : `/${next}`
+ setRoute(read())
+ }]
+}
+
+function statusTone(status: string): string {
+ if (['complete', 'accepted', 'active'].includes(status)) return 'active'
+ if (['timeout', 'invalid_action', 'exception', 'invalid', 'runner_unavailable'].includes(status)) return 'danger'
+ return 'draft'
+}
+
+function titleCase(value: string): string {
+ return value.charAt(0).toUpperCase() + value.slice(1)
+}
+
+function fixtureReplayPlayers(fixture: Fixture, game: Game): [ReplayPlayer, ReplayPlayer] {
+ const nameFor = (submissionId: string) => (
+ submissionId === fixture.submission_a_id ? fixture.agent_a_name : fixture.agent_b_name
+ )
+ return [
+ {
+ name: nameFor(game.first_submission_id),
+ score: game.winner_submission_id === game.first_submission_id ? 1 : 0,
+ },
+ {
+ name: nameFor(game.second_submission_id),
+ score: game.winner_submission_id === game.second_submission_id ? 1 : 0,
+ },
+ ]
+}
+
+function practiceReplayPlayers(
+ practice: PracticeResult | null,
+ baseline: string,
+): [ReplayPlayer, ReplayPlayer] {
+ const winner = typeof practice?.result.winner === 'string' ? practice.result.winner : null
+ return [
+ { name: 'Your Agent', score: winner === 'submitted' ? 1 : 0 },
+ { name: titleCase(baseline), score: winner === baseline ? 1 : 0 },
+ ]
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/api.ts b/Projects/3_Adversarial Search/apps/web/src/api.ts
new file mode 100644
index 00000000..a021a79e
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/api.ts
@@ -0,0 +1,54 @@
+import type {
+ Agent,
+ AgentFixtureHistoryEntry,
+ Fixture,
+ LeaderboardEntry,
+ PracticeResult,
+ Rules,
+} from './types'
+
+async function request(path: string, init?: RequestInit): Promise {
+ const response = await fetch(path, {
+ ...init,
+ headers: { 'Content-Type': 'application/json', ...init?.headers },
+ })
+ if (!response.ok) {
+ const payload = (await response.json().catch(() => ({}))) as { detail?: unknown }
+ const detail = typeof payload.detail === 'string'
+ ? payload.detail
+ : JSON.stringify(payload.detail ?? `Request failed (${response.status})`)
+ throw new Error(detail)
+ }
+ return response.json() as Promise
+}
+
+export const api = {
+ health: () => request<{ status: 'ok'; runner: { available: boolean; code: string; message: string } }>('/api/health'),
+ rules: () => request('/api/rules'),
+ leaderboard: () => request('/api/leaderboard'),
+ agents: () => request('/api/agents'),
+ agent: (agentId: string) => request(`/api/agents/${agentId}`),
+ agentFixtures: (agentId: string) =>
+ request(`/api/agents/${agentId}/fixtures`),
+ featuredFixture: () => request('/api/fixtures/featured'),
+ fixture: (fixtureId: string) => request(`/api/fixtures/${fixtureId}`),
+ createAgent: (ownerName: string, name: string, source: string) =>
+ request('/api/agents', {
+ method: 'POST',
+ body: JSON.stringify({ owner_name: ownerName, name, source }),
+ }),
+ createSubmission: (agentId: string, source: string) =>
+ request(`/api/agents/${agentId}/submissions`, {
+ method: 'POST',
+ body: JSON.stringify({ source }),
+ }),
+ practice: (source: string, baseline: string) =>
+ request('/api/practice', {
+ method: 'POST',
+ body: JSON.stringify({ source, baseline, seed: 17 }),
+ }),
+ runRound: () =>
+ request<{ fixture_ids: string[]; status: string }>('/api/competition/run-round', {
+ method: 'POST',
+ }),
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.test.tsx b/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.test.tsx
new file mode 100644
index 00000000..d40c424a
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.test.tsx
@@ -0,0 +1,69 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { expect, test } from 'vitest'
+
+import { ReplayViewer } from './ReplayBoard'
+
+const blankBoard = '41523161203939122082683632224299007'
+
+test('replay stepping and scrubbing select canonical frames', () => {
+ render(
+ ,
+ )
+ fireEvent.click(screen.getByRole('button', { name: 'Next move' }))
+ expect(screen.getByRole('slider', { name: 'Replay position' })).toHaveValue('1')
+ fireEvent.change(screen.getByRole('slider', { name: 'Replay position' }), {
+ target: { value: '2' },
+ })
+ expect(screen.getByRole('slider', { name: 'Replay position' })).toHaveValue('2')
+ expect(screen.getByLabelText(/player 2/)).toBeInTheDocument()
+ expect(screen.getByText('Teal Knight').closest('.replay-player')).toHaveClass('p1')
+ expect(screen.getByText('Blue Knight').closest('.replay-player')).toHaveClass('p2')
+ expect(screen.getByLabelText('Game score 1 to 0')).toBeInTheDocument()
+})
diff --git a/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.tsx b/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.tsx
new file mode 100644
index 00000000..fad64bc0
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/components/ReplayBoard.tsx
@@ -0,0 +1,140 @@
+import { useEffect, useMemo, useState } from 'react'
+
+import type { ReplayFrame } from '../types'
+
+const WIDTH = 11
+const HEIGHT = 9
+const PADDED_WIDTH = 13
+
+export interface ReplayPlayer {
+ name: string
+ score: number
+}
+
+type ReplayPlayers = readonly [ReplayPlayer, ReplayPlayer]
+
+function cellIndex(row: number, column: number): number {
+ return (HEIGHT - 1 - row) * PADDED_WIDTH + (WIDTH - 1 - column)
+}
+
+export function ReplayBoard({ replay, step }: { replay?: ReplayFrame[]; step: number }) {
+ const frame = replay?.[step]
+ const board = frame ? BigInt(frame.board) : null
+ const locations = frame?.player_locations ?? [null, null]
+ const previousFrame = step > 0 ? replay?.[step - 1] : undefined
+ const cells = useMemo(
+ () =>
+ Array.from({ length: WIDTH * HEIGHT }, (_, position) => {
+ const row = Math.floor(position / WIDTH)
+ const column = position % WIDTH
+ return { row, column, index: cellIndex(row, column) }
+ }),
+ [],
+ )
+
+ return (
+
+ {cells.map(({ row, column, index }) => {
+ const player = locations[0] === index ? 1 : locations[1] === index ? 2 : null
+ const isOpen = board === null || (board & (1n << BigInt(index))) !== 0n
+ const blocked = !isOpen && player === null
+ const last = previousFrame?.chosen_destination === index
+ const label = player
+ ? `row ${row + 1} column ${column + 1}, player ${player}`
+ : `row ${row + 1} column ${column + 1}, ${blocked ? 'blocked' : 'open'}`
+ return (
+
+ {player ? ♞ : null}
+
+ )
+ })}
+
+ )
+}
+
+export function ReplayViewer({
+ replay,
+ title,
+ players,
+}: {
+ replay?: ReplayFrame[]
+ title: string
+ players?: ReplayPlayers
+}) {
+ const lastStep = Math.max(0, (replay?.length ?? 1) - 1)
+ const [step, setStep] = useReplayStep(lastStep)
+ const frame = replay?.[step]
+ return (
+
+
+
+ {title}
+ {replay?.length ? `${replay.length} replay frames` : 'Replay appears after a completed game'}
+
+
Ply {frame?.ply_index ?? 0}
+
+
+ {players ? : null}
+
+ setStep(0)} aria-label="Reset replay">↺
+ setStep(Math.max(0, step - 1))} aria-label="Previous move">←
+ setStep(Number(event.target.value))}
+ />
+ setStep(Math.min(lastStep, step + 1))} aria-label="Next move">→
+ {step} / {lastStep}
+
+ {frame?.outcome ? Outcome: {frame.outcome.replace('_', ' ')}
: null}
+
+ )
+}
+
+function ReplayNameplate({ players }: { players: ReplayPlayers }) {
+ return (
+
+
+
+ {players[0].score}– {players[1].score}
+
+
+
+ )
+}
+
+function PlayerIdentity({
+ player,
+ playerNumber,
+}: {
+ player: ReplayPlayer
+ playerNumber: 1 | 2
+}) {
+ return (
+
+ ♞
+
+ Player {playerNumber}
+ {player.name}
+
+
+ )
+}
+
+function useReplayStep(lastStep: number): [number, (value: number) => void] {
+ const [step, setStep] = useState(0)
+ useEffect(() => setStep(0), [lastStep])
+ return [Math.min(step, lastStep), setStep]
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/main.tsx b/Projects/3_Adversarial Search/apps/web/src/main.tsx
new file mode 100644
index 00000000..3bcaed17
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/main.tsx
@@ -0,0 +1,11 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+
+import App from './App'
+import './styles.css'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/Projects/3_Adversarial Search/apps/web/src/styles.css b/Projects/3_Adversarial Search/apps/web/src/styles.css
new file mode 100644
index 00000000..06462f73
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/styles.css
@@ -0,0 +1,273 @@
+:root {
+ color-scheme: dark;
+ --bg: #071019;
+ --panel: #0d1823;
+ --panel-2: #111f2d;
+ --panel-3: #142638;
+ --text: #eef7ff;
+ --muted: #91a7b9;
+ --line: #20384b;
+ --accent: #65f4c0;
+ --blue: #78a8ff;
+ --warn: #ffcb6b;
+ --danger: #ff7d8f;
+ --success: #7ce7a4;
+ --radius: 17px;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ background: var(--bg);
+ color: var(--text);
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 8% 0%, rgba(101, 244, 192, .08), transparent 29%), radial-gradient(circle at 95% 7%, rgba(120, 168, 255, .07), transparent 25%), var(--bg); }
+button, input, select, textarea { font: inherit; }
+button { cursor: pointer; }
+button:disabled { cursor: not-allowed; opacity: .45; }
+button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, tr:focus-visible, summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
+a { color: var(--blue); text-decoration: none; }
+code, pre, textarea { font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace; }
+
+.app-shell { display: grid; grid-template-columns: 245px minmax(0, 1fr); min-height: 100vh; }
+.sidebar { position: sticky; top: 0; height: 100vh; padding: 20px 15px; border-right: 1px solid var(--line); background: rgba(7, 16, 25, .92); backdrop-filter: blur(18px); z-index: 20; }
+.brand { display: flex; align-items: center; gap: 11px; width: 100%; padding: 4px 7px 23px; color: var(--text); background: transparent; border: 0; text-align: left; }
+.brand span:last-child { display: grid; }
+.brand strong { font-size: 15px; }
+.brand small { color: var(--muted); font-size: 11px; }
+.brand-mark { width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center; background: linear-gradient(145deg, var(--accent), #b5ffe4); color: #062118; font-size: 22px; font-weight: 950; box-shadow: 0 0 28px rgba(101, 244, 192, .17); }
+.nav-label { display: block; padding: 10px; color: #5f778c; font-size: 10px; letter-spacing: 1.4px; text-transform: uppercase; }
+.sidebar nav { display: grid; gap: 6px; }
+.sidebar nav button { display: flex; align-items: center; gap: 10px; padding: 11px; border: 1px solid transparent; border-radius: 12px; background: transparent; color: var(--muted); text-align: left; }
+.sidebar nav button span { width: 24px; text-align: center; }
+.sidebar nav button:hover { color: var(--text); background: rgba(255, 255, 255, .03); }
+.sidebar nav button.active { color: var(--text); border-color: #28475e; background: linear-gradient(90deg, rgba(101, 244, 192, .12), rgba(120, 168, 255, .05)); }
+.runner-note { position: absolute; left: 15px; right: 15px; bottom: 17px; display: grid; gap: 5px; padding: 13px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); }
+.runner-note strong { font-size: 12px; }
+.runner-note span { color: var(--muted); font-size: 10px; line-height: 1.45; }
+
+main { min-width: 0; }
+.topbar { position: sticky; top: 0; z-index: 15; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 15px 27px; border-bottom: 1px solid rgba(32, 56, 75, .86); background: rgba(7, 16, 25, .8); backdrop-filter: blur(16px); }
+.crumb { color: var(--muted); font-size: 12px; }
+.crumb strong { color: var(--text); }
+.top-actions, .actions, .controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+.content { max-width: 1540px; margin: 0 auto; padding: 27px; }
+.error-banner { margin-bottom: 16px; padding: 12px 14px; border: 1px solid rgba(255, 125, 143, .35); border-radius: 12px; background: rgba(255, 125, 143, .09); color: #ffc1ca; }
+
+.pill, .status { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 999px; white-space: nowrap; }
+.pill { padding: 7px 10px; background: var(--panel); color: var(--muted); font-size: 11px; }
+.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 11px var(--accent); }
+.status { padding: 5px 7px; font-size: 9px; font-weight: 850; letter-spacing: .65px; text-transform: uppercase; }
+.status.active { color: #a9f8d8; background: rgba(101, 244, 192, .08); border-color: rgba(101, 244, 192, .28); }
+.status.draft { color: #ffdda0; background: rgba(255, 203, 107, .07); border-color: rgba(255, 203, 107, .25); }
+.status.danger { color: #ffbac4; background: rgba(255, 125, 143, .08); border-color: rgba(255, 125, 143, .28); }
+.primary, .secondary, .ghost { min-height: 38px; padding: 9px 13px; border: 1px solid transparent; border-radius: 10px; font-weight: 800; transition: .17s ease; }
+.primary { background: var(--accent); color: #052018; }
+.primary:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(101, 244, 192, .14); }
+.secondary { border-color: #2b4a61; background: #172b3c; color: var(--text); }
+.ghost { border-color: var(--line); background: transparent; color: var(--muted); }
+.secondary:hover:not(:disabled), .ghost:hover:not(:disabled) { color: var(--text); background: #1c3245; }
+
+.hero { display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(330px, .7fr); gap: 18px; }
+.hero-copy, .hero-side, .card, .table-card, .replay-card { border: 1px solid var(--line); border-radius: var(--radius); background: linear-gradient(180deg, rgba(17, 31, 45, .98), rgba(10, 22, 32, .98)); box-shadow: 0 20px 55px rgba(0, 0, 0, .25); }
+.hero-copy { position: relative; overflow: hidden; padding: 31px; }
+.hero-copy::after { position: absolute; top: -170px; right: -130px; width: 320px; height: 320px; border-radius: 50%; background: radial-gradient(circle, rgba(101, 244, 192, .15), transparent 68%); content: ""; }
+.hero-copy h1 { position: relative; z-index: 1; max-width: 820px; margin: 12px 0; font-size: clamp(2.2rem, 4vw, 3.5rem); line-height: 1.03; letter-spacing: -.045em; }
+.hero-copy p { max-width: 780px; margin: 0 0 22px; color: #a8bac9; font-size: 15px; line-height: 1.65; }
+.hero-side { display: grid; align-content: start; gap: 14px; padding: 21px; }
+.eyebrow { color: var(--accent); font-size: 10px; font-weight: 900; letter-spacing: 1.5px; text-transform: uppercase; }
+.metric-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 9px; }
+.metric { min-width: 0; padding: 12px; border: 1px solid var(--line); border-radius: 12px; background: #091621; }
+.metric strong { display: block; font-size: clamp(15px, 2vw, 20px); overflow-wrap: anywhere; }
+.metric span { color: var(--muted); font-size: 9px; text-transform: uppercase; }
+.callout { padding: 14px; border: 1px solid rgba(101, 244, 192, .21); border-radius: 12px; background: rgba(101, 244, 192, .065); color: #b9e9d8; font-size: 12px; line-height: 1.55; }
+.section-gap { margin-top: 16px; }
+.steps-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
+.simple-step { padding: 15px; border: 1px solid var(--line); border-radius: 13px; background: #0b1823; }
+.simple-step span { display: grid; place-items: center; width: 27px; height: 27px; border-radius: 8px; background: rgba(101, 244, 192, .1); color: var(--accent); font-size: 10px; font-weight: 900; }
+.simple-step strong { display: block; margin: 10px 0 4px; font-size: 13px; }
+.simple-step p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.45; }
+.section-head { display: flex; justify-content: space-between; align-items: end; gap: 12px; margin: 26px 0 12px; }
+.section-head h2 { margin: 0; font-size: 21px; }
+.section-head p { margin: 4px 0 0; color: var(--muted); font-size: 12px; }
+.feature-grid { display: grid; grid-template-columns: minmax(400px, 1.1fr) minmax(300px, .9fr); gap: 16px; }
+.fixture-summary { padding: 19px; }
+.fixture-summary h2 { margin: 14px 0 8px; font-size: 24px; }
+.fixture-summary > p { color: var(--muted); line-height: 1.55; }
+.event-row { display: grid; grid-template-columns: 70px 1fr auto; align-items: center; gap: 10px; margin-top: 8px; padding: 10px; border: 1px solid #1c3345; border-radius: 10px; background: #091621; font-size: 11px; }
+.event-row span, .event-row small { color: var(--muted); }
+.fixture-link { width: 100%; color: inherit; text-align: left; cursor: pointer; }
+.fixture-link:hover, .fixture-link:focus-visible { border-color: var(--cyan); background: rgba(58, 211, 198, .08); }
+
+.replay-card { min-width: 0; padding: 14px; background: #07131d; }
+.match-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 11px; }
+.match-head > div { display: grid; gap: 3px; }
+.match-head strong { font-size: 13px; }
+.match-head span:not(.status) { color: var(--muted); font-size: 10px; }
+.board { display: grid; grid-template-columns: repeat(11, minmax(0, 1fr)); grid-template-rows: repeat(9, minmax(0, 1fr)); gap: 3px; width: 100%; aspect-ratio: 11 / 9; padding: 3px; overflow: hidden; border-radius: 12px; background: #14283a; }
+.cell { position: relative; display: grid; place-items: center; min-width: 0; min-height: 0; background: #102433; color: var(--text); }
+.cell.alt { background: #0d1d2a; }
+.cell.blocked { background: repeating-linear-gradient(135deg, #182b3a, #182b3a 5px, #142532 5px, #142532 10px); }
+.cell.blocked::after { color: #526a7c; font-size: 10px; content: "×"; }
+.cell.last { box-shadow: inset 0 0 0 2px var(--warn); }
+.piece { display: grid; place-items: center; width: 72%; height: 72%; border-radius: 50%; font-size: clamp(10px, 1.5vw, 22px); font-weight: 950; box-shadow: 0 7px 16px rgba(0, 0, 0, .3); }
+.piece.p1 { background: linear-gradient(145deg, #b7ffe5, var(--accent)); color: #062019; }
+.piece.p2 { background: linear-gradient(145deg, #c0d3ff, var(--blue)); color: #08172b; }
+.replay-nameplate { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 12px; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 11px; background: #0c1c28; }
+.replay-player { display: flex; align-items: center; gap: 9px; min-width: 0; }
+.replay-player.p2 { flex-direction: row-reverse; text-align: right; }
+.replay-knight { display: grid; flex: 0 0 auto; place-items: center; width: 30px; height: 30px; border-radius: 50%; font-size: 17px; font-weight: 950; box-shadow: 0 5px 12px rgba(0, 0, 0, .25); }
+.replay-knight.p1 { background: linear-gradient(145deg, #b7ffe5, var(--accent)); color: #062019; }
+.replay-knight.p2 { background: linear-gradient(145deg, #c0d3ff, var(--blue)); color: #08172b; }
+.replay-player-copy { display: grid; min-width: 0; }
+.replay-player-copy small { color: var(--muted); font-size: 8px; letter-spacing: .7px; text-transform: uppercase; }
+.replay-player-copy strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
+.replay-player.p1 strong { color: var(--accent); }
+.replay-player.p2 strong { color: var(--blue); }
+.replay-game-score { display: flex; align-items: center; gap: 5px; color: var(--text); font-size: 18px; font-variant-numeric: tabular-nums; }
+.replay-game-score span { color: var(--muted); }
+.replay-controls { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
+.replay-controls button { min-height: 32px; padding: 5px 9px; }
+.scrubber { flex: 1; min-width: 100px; accent-color: var(--accent); }
+.tiny { color: var(--muted); font-size: 10px; }
+
+.table-card { overflow-x: auto; }
+table { width: 100%; border-collapse: collapse; }
+th, td { padding: 12px 10px; border-bottom: 1px solid var(--line); text-align: left; font-size: 12px; white-space: nowrap; }
+th { color: #6f899f; font-size: 9px; letter-spacing: 1px; text-transform: uppercase; }
+tbody tr { transition: background .15s; }
+tbody tr[tabindex] { cursor: pointer; }
+tbody tr:hover, tbody tr.selected { background: #112536; }
+tbody tr:last-child td { border-bottom: 0; }
+.rank { font-size: 16px; font-weight: 950; }
+.developer { color: var(--blue); font-size: 10px; }
+.points { color: var(--accent); font-size: 17px; font-weight: 950; }
+.form-row { display: flex; align-items: center; gap: 4px; }
+.form-dot { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 6px; background: rgba(124, 231, 164, .12); color: var(--success); font-size: 9px; font-style: normal; font-weight: 900; }
+.form-dot.D { background: rgba(255, 203, 107, .12); color: var(--warn); }
+.form-dot.L { background: rgba(255, 125, 143, .12); color: var(--danger); }
+.empty-state { padding: 24px; color: var(--muted); text-align: center; }
+
+.workspace { display: grid; grid-template-columns: minmax(420px, .95fr) minmax(460px, 1.05fr); min-height: 760px; overflow: hidden; border: 1px solid var(--line); border-radius: var(--radius); background: #09131d; box-shadow: 0 20px 55px rgba(0, 0, 0, .28); }
+.workspace-left, .workspace-right { display: grid; min-width: 0; }
+.workspace-left { grid-template-rows: minmax(470px, 1.2fr) minmax(245px, .8fr); border-right: 1px solid var(--line); }
+.workspace-right { grid-template-rows: minmax(470px, 1fr) 290px; }
+.game-pane { min-width: 0; padding: 13px; overflow: auto; border-bottom: 1px solid var(--line); }
+.game-pane .replay-card { max-width: 660px; margin: 0 auto; border: 0; box-shadow: none; }
+.problem-pane { padding: 16px; overflow: auto; background: #0d1924; }
+.tabs, .mode-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
+.tabs button, .mode-tabs button { padding: 8px 10px; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--muted); font-size: 10px; font-weight: 850; }
+.tabs button.active, .mode-tabs button.active { border-color: #2c4b61; background: #14283a; color: var(--text); }
+.problem-pane h3 { margin: 0 0 8px; font-size: 16px; }
+.problem-pane li { margin: 5px 0; color: #a7bac9; font-size: 11px; line-height: 1.55; }
+.editor-pane { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; border-bottom: 1px solid var(--line); background: #0a141e; }
+.editor-toolbar, .console-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; padding: 9px 11px; border-bottom: 1px solid var(--line); background: #11212f; }
+.editor-toolbar > div:first-child { display: grid; }
+.editor-toolbar span { color: var(--muted); font-size: 9px; }
+.editor-toolbar select { padding: 7px 8px; border: 1px solid var(--line); border-radius: 8px; background: #091621; color: var(--text); font-size: 11px; }
+.editor-toolbar button { min-height: 32px; padding: 5px 9px; }
+.editor-wrap { display: grid; grid-template-columns: 44px minmax(0, 1fr); min-height: 0; }
+.line-numbers { height: 100%; margin: 0; padding: 13px 8px; overflow: hidden; background: #07121b; color: #496579; font-size: 12px; line-height: 1.6; text-align: right; user-select: none; }
+.editor-wrap textarea { width: 100%; height: 100%; padding: 13px; resize: none; border: 0; outline: 0; background: #0a141e; color: #d8e7f3; font-size: 12px; line-height: 1.6; tab-size: 4; }
+.console-pane { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; min-height: 0; background: #08131c; }
+.console-body { padding: 11px; overflow: auto; color: #9fb5c6; font-family: "Cascadia Code", Consolas, monospace; font-size: 11px; line-height: 1.55; white-space: pre-wrap; }
+.console-body p { margin: 0 0 5px; }
+.log-success { color: var(--success); }
+.log-warn { color: var(--warn); }
+.log-error { color: var(--danger); }
+.workspace-actions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 10px 11px; border-top: 1px solid var(--line); background: #11212f; }
+
+.leader-layout { display: grid; grid-template-columns: minmax(620px, 1.4fr) minmax(300px, .6fr); gap: 16px; }
+.ranking-note { display: grid; gap: 4px; margin: 0 0 14px; padding: 11px 14px; border: 1px solid rgba(101, 244, 192, .23); border-radius: 12px; background: rgba(101, 244, 192, .06); }
+.ranking-note strong { color: #a9f8d8; font-size: 11px; }
+.ranking-note span { color: var(--muted); font-size: 10px; line-height: 1.45; }
+.agent-profile { display: grid; align-content: start; gap: 12px; padding: 19px; }
+.agent-profile h2, .agent-profile h3 { margin: 0; }
+.agent-profile h3 { margin-top: 8px; font-size: 13px; }
+.agent-profile code { overflow-wrap: anywhere; color: #7794aa; font-size: 9px; }
+.agent-profile .secondary { margin-top: 4px; }
+.provisional { display: block; margin-top: 3px; color: var(--warn); font-size: 8px; font-weight: 700; letter-spacing: .55px; text-transform: uppercase; }
+.agent-page-summary { display: grid; grid-template-columns: minmax(220px, .55fr) minmax(480px, 1fr); align-items: center; gap: 22px; padding: 20px; }
+.agent-page-summary h2 { margin: 8px 0 3px; }
+.agent-page-tabs { margin: 18px 0 8px; }
+.agent-page-list { padding: 12px; }
+.agent-page-list.version-list { gap: 7px; }
+.agent-page-list .version-row { grid-template-columns: 115px minmax(0, 1fr) auto; }
+.fixture-history { display: grid; gap: 6px; }
+.fixture-history .event-row { display: grid; grid-template-columns: 26px minmax(240px, 1fr) 50px minmax(150px, auto); align-items: center; gap: 10px; margin: 0; }
+.fixture-history .event-row > span:nth-child(3) { color: var(--accent); font-weight: 850; }
+.record-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
+
+.agent-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
+.agent-card { padding: 18px; }
+.card-title { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
+.card-title h2 { margin: 9px 0 2px; font-size: 19px; }
+.agent-card .record-grid { margin-top: 14px; grid-template-columns: repeat(3, 1fr); }
+.version-list { display: grid; gap: 7px; margin: 14px 0; }
+.version-row { display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; align-items: center; gap: 9px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; background: #091621; font-size: 10px; }
+.version-row > span:nth-child(2) { display: grid; gap: 3px; min-width: 0; }
+.version-row code { overflow: hidden; color: #829db1; font-size: 9px; text-overflow: ellipsis; }
+.version-row small { color: var(--muted); font-size: 8px; }
+.recent-fixtures { display: grid; gap: 3px; }
+.recent-fixtures .event-row { margin-top: 0; }
+
+.submission-layout { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(320px, .9fr); gap: 16px; }
+.submission-layout > .card { padding: 19px; }
+.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
+.form-grid label { display: grid; gap: 6px; color: #9eb2c3; font-size: 10px; }
+.form-grid label.full { grid-column: 1 / -1; }
+.form-grid input, .form-grid select, .form-grid textarea, .fixed-field { width: 100%; padding: 10px; border: 1px solid var(--line); border-radius: 9px; background: #091621; color: var(--text); }
+.form-grid input[type="file"] { color: var(--muted); }
+.submission-source { min-height: 250px; resize: vertical; font-size: 11px; line-height: 1.5; }
+.submission-layout .actions { margin-top: 14px; }
+.submission-preview { align-content: start; }
+.submission-preview h2 { margin: 14px 0 5px; }
+.submission-preview > p { color: var(--muted); font-size: 12px; line-height: 1.55; }
+.pipeline { display: grid; gap: 8px; margin-top: 14px; }
+.pipeline-row { display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: 10px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; background: #091621; }
+.pipeline-row > span { display: grid; place-items: center; width: 25px; height: 25px; border-radius: 7px; background: #142b3c; color: var(--accent); font-size: 9px; font-weight: 900; }
+.pipeline-row strong { font-size: 11px; }
+.pipeline-row b { color: var(--muted); font-size: 9px; }
+.hash-preview { display: grid; gap: 6px; margin-top: 14px; }
+.hash-preview code { overflow-wrap: anywhere; color: #829db1; font-size: 9px; }
+.hash-preview details { margin-top: 8px; color: var(--muted); font-size: 11px; }
+.hash-preview pre { max-height: 280px; padding: 10px; overflow: auto; border: 1px solid var(--line); border-radius: 9px; background: #07121b; color: #a9bdcc; font-size: 9px; }
+
+.mobile-nav { display: none; }
+
+@media (max-width: 1180px) {
+ .hero, .feature-grid, .submission-layout, .leader-layout, .agent-page-summary { grid-template-columns: 1fr; }
+ .workspace { grid-template-columns: 1fr; }
+ .workspace-left { border-right: 0; border-bottom: 1px solid var(--line); }
+ .workspace-left, .workspace-right { grid-template-rows: auto auto; }
+ .game-pane .replay-card { max-width: 720px; }
+}
+
+@media (max-width: 860px) {
+ .app-shell { display: block; }
+ .sidebar { display: none; }
+ .topbar { padding: 12px 14px; }
+ .top-actions .pill { display: none; }
+ .content { padding: 17px 14px 88px; }
+ .mobile-nav { position: fixed; z-index: 40; right: 8px; bottom: 8px; left: 8px; display: flex; justify-content: space-around; padding: 6px; border: 1px solid var(--line); border-radius: 14px; background: rgba(12, 23, 34, .97); backdrop-filter: blur(14px); }
+ .mobile-nav button { display: grid; place-items: center; gap: 2px; padding: 5px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
+ .mobile-nav button span { font-size: 16px; }
+ .mobile-nav button.active { color: var(--accent); }
+ .steps-grid, .agent-grid { grid-template-columns: 1fr; }
+ .workspace { min-height: 0; }
+ .workspace-right { grid-template-rows: 520px 300px; }
+ .leader-layout .table-card { max-width: calc(100vw - 28px); }
+ .fixture-history .event-row { grid-template-columns: 26px minmax(170px, 1fr) 45px; }
+ .fixture-history .event-row small { grid-column: 2 / -1; }
+}
+
+@media (max-width: 560px) {
+ .hero-copy, .hero-side { padding: 20px; }
+ .hero-copy h1 { font-size: 2.25rem; }
+ .metric-grid, .record-grid, .agent-card .record-grid { grid-template-columns: 1fr 1fr; }
+ .workspace-actions { grid-template-columns: 1fr; }
+ .form-grid { grid-template-columns: 1fr; }
+ .form-grid label.full { grid-column: auto; }
+ .feature-grid { display: block; }
+ .fixture-summary { margin-top: 14px; }
+ th:nth-child(4), td:nth-child(4), th:nth-child(7), td:nth-child(7), th:nth-child(9), td:nth-child(9) { display: none; }
+}
diff --git a/Projects/3_Adversarial Search/apps/web/src/test/setup.ts b/Projects/3_Adversarial Search/apps/web/src/test/setup.ts
new file mode 100644
index 00000000..10130d73
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/test/setup.ts
@@ -0,0 +1,19 @@
+import '@testing-library/jest-dom/vitest'
+import { cleanup } from '@testing-library/react'
+import { afterEach } from 'vitest'
+
+afterEach(() => cleanup())
+
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: (query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => undefined,
+ removeListener: () => undefined,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ dispatchEvent: () => false,
+ }),
+})
diff --git a/Projects/3_Adversarial Search/apps/web/src/types.ts b/Projects/3_Adversarial Search/apps/web/src/types.ts
new file mode 100644
index 00000000..bd9e42af
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/src/types.ts
@@ -0,0 +1,119 @@
+export type ViewName = 'overview' | 'code' | 'leaderboard' | 'agents' | 'submit'
+
+export interface Rules {
+ game: 'knight_isolation'
+ board_width: number
+ board_height: number
+ runtime: string
+ move_budget_ms: number
+ fixture_games: number
+ points: { win: number; draw: number; loss: number }
+ engine_version: string
+ ruleset_version: string
+ demo_owner_name: string
+ minimum_ranked_fixtures: number
+}
+
+export interface ReplayFrame {
+ ply_index: number
+ active_player: number
+ board: string
+ player_locations: [number | null, number | null]
+ chosen_action: number | null
+ chosen_destination: number | null
+ legal_actions: number[]
+ elapsed_ms: number
+ stdout_excerpt: string
+ stderr_excerpt: string
+ outcome: string | null
+}
+
+export interface Submission {
+ id: string
+ agent_id: string
+ number: number
+ source_hash: string
+ execution_kind: string
+ validation_result: Record
+ created_at: string
+ is_active: boolean
+}
+
+export interface RecordSummary {
+ fixtures: number
+ wins: number
+ draws: number
+ losses: number
+ points: number
+}
+
+export interface Agent {
+ id: string
+ owner_name: string
+ name: string
+ active_submission_id: string | null
+ created_at: string
+ submissions: Submission[]
+ record: RecordSummary | null
+}
+
+export interface LeaderboardEntry {
+ rank: number | null
+ agent_id: string
+ agent_name: string
+ owner_name: string
+ submission_id: string
+ submission_number: number
+ fixtures: number
+ wins: number
+ draws: number
+ losses: number
+ points: number
+ points_per_fixture: number
+ qualified: boolean
+ recent_form: Array<'W' | 'D' | 'L'>
+}
+
+export interface AgentFixtureHistoryEntry {
+ id: string
+ submission_number: number
+ opponent_name: string
+ opponent_submission_number: number
+ outcome: 'W' | 'D' | 'L'
+ score: string
+ completed_at: string
+}
+
+export interface PracticeResult {
+ status: string
+ result: Record
+ replay: ReplayFrame[]
+ logs: Array<{ level: string; message: string }>
+}
+
+export interface Game {
+ id: string
+ leg_number: number
+ first_submission_id: string
+ second_submission_id: string
+ winner_submission_id: string | null
+ end_status: string
+ losing_reason: string
+ replay: ReplayFrame[]
+ engine_version: string
+ ruleset_version: string
+}
+
+export interface Fixture {
+ id: string
+ submission_a_id: string
+ submission_b_id: string
+ agent_a_name: string
+ agent_b_name: string
+ status: string
+ winner_submission_id: string | null
+ is_draw: boolean
+ points_a: number
+ points_b: number
+ games: Game[]
+}
diff --git a/Projects/3_Adversarial Search/apps/web/tsconfig.json b/Projects/3_Adversarial Search/apps/web/tsconfig.json
new file mode 100644
index 00000000..58c79652
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
+ },
+ "include": ["src", "vite.config.ts"]
+}
diff --git a/Projects/3_Adversarial Search/apps/web/vite.config.ts b/Projects/3_Adversarial Search/apps/web/vite.config.ts
new file mode 100644
index 00000000..de75f0cf
--- /dev/null
+++ b/Projects/3_Adversarial Search/apps/web/vite.config.ts
@@ -0,0 +1,27 @@
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig(() => ({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ proxy: { '/api': 'http://127.0.0.1:8000' },
+ },
+ // Keep dependency discovery inside this restricted Windows workspace while
+ // still converting React's CommonJS entry points into browser-safe ESM.
+ optimizeDeps: {
+ noDiscovery: true,
+ include: [
+ 'react',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
+ 'react-dom',
+ 'react-dom/client',
+ ],
+ },
+ test: {
+ environment: 'jsdom',
+ setupFiles: './src/test/setup.ts',
+ css: true,
+ },
+}))
diff --git a/Projects/3_Adversarial Search/packages/agent_runner/Dockerfile b/Projects/3_Adversarial Search/packages/agent_runner/Dockerfile
new file mode 100644
index 00000000..6aa2766d
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/agent_runner/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.12.10-slim-bookworm
+
+ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PYTHONPATH=/opt/arena/packages/isolation_engine:/opt/arena/packages/agent_runner
+RUN useradd --create-home --uid 10001 arena
+WORKDIR /opt/arena
+COPY packages/isolation_engine packages/isolation_engine
+COPY packages/agent_runner packages/agent_runner
+USER arena
+ENTRYPOINT []
diff --git a/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/__init__.py b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/__init__.py
new file mode 100644
index 00000000..84db47e9
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/__init__.py
@@ -0,0 +1,11 @@
+from .compat import ActionQueue, DecisionDeadlineExceeded, invoke_agent
+from .docker import RUNNER_IMAGE, DockerAgent, RunnerUnavailableError
+
+__all__ = [
+ "RUNNER_IMAGE",
+ "ActionQueue",
+ "DecisionDeadlineExceeded",
+ "DockerAgent",
+ "RunnerUnavailableError",
+ "invoke_agent",
+]
diff --git a/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/compat.py b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/compat.py
new file mode 100644
index 00000000..fd9c127c
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/compat.py
@@ -0,0 +1,47 @@
+"""Compatibility helpers used only inside the isolated runner process."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from time import perf_counter
+from typing import Any
+
+from isolation_engine import Isolation
+
+
+class DecisionDeadlineExceeded(TimeoutError):
+ pass
+
+
+@dataclass
+class ActionQueue:
+ """Collect the last action sent through the original Udacity queue API."""
+
+ deadline: float
+ action: int | None = None
+ has_action: bool = False
+
+ def put(self, item: Any, block: bool = True, timeout: float | None = None) -> None:
+ del block, timeout
+ if perf_counter() > self.deadline:
+ raise DecisionDeadlineExceeded("decision deadline exceeded")
+ self.action = int(item)
+ self.has_action = True
+
+ def put_nowait(self, item: Any) -> None:
+ self.put(item, block=False)
+
+
+def invoke_agent(agent: Any, state: Isolation, timeout_ms: int) -> int | None:
+ """Normalize ``return action`` and ``self.queue.put(action)`` styles."""
+
+ queue = ActionQueue(perf_counter() + timeout_ms / 1000)
+ agent.queue = queue
+ if not hasattr(agent, "context"):
+ agent.context = None
+ returned = agent.get_action(state)
+ if perf_counter() > queue.deadline:
+ raise DecisionDeadlineExceeded("decision deadline exceeded")
+ if returned is not None:
+ return int(returned)
+ return queue.action if queue.has_action else None
diff --git a/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/docker.py b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/docker.py
new file mode 100644
index 00000000..cec3065e
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/docker.py
@@ -0,0 +1,220 @@
+"""Docker boundary for submitted Python in the local beta."""
+
+from __future__ import annotations
+
+import json
+import re
+import shutil
+import subprocess
+from concurrent.futures import ThreadPoolExecutor
+from concurrent.futures import TimeoutError as FutureTimeout
+from contextlib import suppress
+from pathlib import Path
+from typing import Any
+
+from isolation_engine import (
+ ControllerTimeoutError,
+ Isolation,
+ MoveResponse,
+)
+
+RUNNER_IMAGE = "agent-arena-runner:py312-v1"
+MAX_PROTOCOL_LINE = 32_768
+STARTUP_TIMEOUT_SECONDS = 10
+
+
+class RunnerUnavailableError(RuntimeError):
+ pass
+
+
+class DockerAgent:
+ """Start one fresh container for one submitted agent in one game."""
+
+ def __init__(self, source_path: Path, image: str = RUNNER_IMAGE) -> None:
+ self.source_path = source_path.resolve()
+ self.image = image
+ self.process: subprocess.Popen[str] | None = None
+ # A reader thread makes a blocking stdout pipe compatible with a wall-clock timeout.
+ self.reader = ThreadPoolExecutor(max_workers=1, thread_name_prefix="docker-agent-line")
+
+ @classmethod
+ def diagnostic(cls, image: str = RUNNER_IMAGE) -> dict[str, Any]:
+ executable = shutil.which("docker")
+ if executable is None:
+ return {
+ "available": False,
+ "code": "docker_not_installed",
+ "message": "Docker CLI is unavailable; submitted-code execution is disabled.",
+ }
+ try:
+ image_reference = _canonical_image_reference(image)
+ inspection = subprocess.run(
+ [executable, "image", "inspect", image_reference, "--format", "{{.Id}}"],
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ except subprocess.TimeoutExpired:
+ return {
+ "available": False,
+ "code": "docker_daemon_unavailable",
+ "message": "Docker did not respond to image inspection within 10 seconds.",
+ }
+ except OSError as error:
+ return {
+ "available": False,
+ "code": "docker_cli_error",
+ "message": f"Docker CLI could not be started: {error}",
+ }
+ if inspection.returncode != 0:
+ detail = _docker_error_excerpt(inspection.stderr, inspection.stdout)
+ if _image_is_missing(detail):
+ return {
+ "available": False,
+ "code": "runner_image_missing",
+ "message": f"Runner image {image} is not built.",
+ }
+ return {
+ "available": False,
+ "code": "docker_daemon_unavailable",
+ "message": (
+ "The API process could not inspect the Docker daemon or active context: "
+ f"{detail}"
+ ),
+ }
+ return {"available": True, "code": "ready", "message": "Runner is ready."}
+
+ def reset(self, player_id: int) -> None:
+ diagnostic = self.diagnostic(self.image)
+ if not diagnostic["available"]:
+ raise RunnerUnavailableError(diagnostic["message"])
+ mount = f"type=bind,src={self.source_path},dst=/workspace/agent.py,readonly"
+ executable = shutil.which("docker")
+ if executable is None:
+ raise RunnerUnavailableError("Docker CLI is unavailable")
+ command = [
+ executable,
+ "run",
+ "--rm",
+ "-i",
+ "--network=none",
+ "--read-only",
+ "--tmpfs=/tmp:rw,noexec,nosuid,size=16m",
+ "--cpus=1",
+ "--memory=256m",
+ "--pids-limit=64",
+ "--cap-drop=ALL",
+ "--security-opt=no-new-privileges:true",
+ "--mount",
+ mount,
+ _canonical_image_reference(self.image),
+ "python",
+ "-m",
+ "agent_runner.worker",
+ "--player-id",
+ str(player_id),
+ ]
+ self.process = subprocess.Popen(
+ command,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ bufsize=1,
+ )
+ assert self.process.stdout is not None
+ startup = self.reader.submit(self.process.stdout.readline)
+ try:
+ line = startup.result(timeout=STARTUP_TIMEOUT_SECONDS)
+ except FutureTimeout as error:
+ self.close()
+ raise RunnerUnavailableError("runner startup exceeded 10 seconds") from error
+ if not line:
+ detail = self._stderr_excerpt()
+ self.close()
+ raise RunnerUnavailableError(f"runner stopped during startup: {detail}")
+ if len(line) > MAX_PROTOCOL_LINE:
+ self.close()
+ raise RunnerUnavailableError("runner startup protocol output limit exceeded")
+ try:
+ response = json.loads(line)
+ except json.JSONDecodeError as error:
+ self.close()
+ raise RunnerUnavailableError("runner emitted invalid startup JSON") from error
+ if response.get("status") != "ready":
+ self.close()
+ raise RunnerUnavailableError(response.get("error", "runner failed during startup"))
+
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ if self.process is None or self.process.stdin is None or self.process.stdout is None:
+ raise RunnerUnavailableError("runner process was not started")
+ self.process.stdin.write(
+ json.dumps(
+ {
+ "type": "choose_action",
+ "state": state.to_dict(),
+ "timeout_ms": timeout_ms,
+ }
+ )
+ + "\n"
+ )
+ self.process.stdin.flush()
+ future = self.reader.submit(self.process.stdout.readline)
+ try:
+ line = future.result(timeout=timeout_ms / 1000)
+ except FutureTimeout as error:
+ self.close()
+ raise ControllerTimeoutError(f"agent exceeded {timeout_ms} ms") from error
+ if not line:
+ raise RuntimeError(f"runner stopped without a response: {self._stderr_excerpt()}")
+ if len(line) > MAX_PROTOCOL_LINE:
+ self.close()
+ raise RuntimeError("runner protocol output limit exceeded")
+ response = json.loads(line)
+ if response.get("status") == "error":
+ raise RuntimeError(response.get("error", "runner error"))
+ return MoveResponse(
+ action=response.get("action"),
+ stdout=response.get("stdout", "")[-8192:],
+ stderr=response.get("stderr", "")[-8192:],
+ )
+
+ def close(self) -> None:
+ if self.process is not None and self.process.poll() is None:
+ self.process.kill()
+ with suppress(subprocess.TimeoutExpired):
+ self.process.wait(timeout=2)
+ self.process = None
+ self.reader.shutdown(wait=False, cancel_futures=True)
+
+ def _stderr_excerpt(self) -> str:
+ if self.process is None or self.process.stderr is None:
+ return ""
+ return self.process.stderr.read(8192)
+
+
+def _docker_error_excerpt(stderr: str, stdout: str) -> str:
+ lines = [line.strip() for line in f"{stderr}\n{stdout}".splitlines() if line.strip()]
+ if not lines:
+ return "docker image inspect failed without an error message"
+ return " | ".join(lines[-3:])[-2_000:]
+
+
+def _image_is_missing(detail: str) -> bool:
+ normalized = detail.lower()
+ return "no such image" in normalized or "image does not exist" in normalized
+
+
+def _canonical_image_reference(image: str) -> str:
+ """Normalize Docker Hub short names for Docker Desktop's containerd store."""
+
+ reference = image.strip()
+ if reference.startswith("sha256:") or re.fullmatch(r"[0-9a-fA-F]{12,64}", reference):
+ return reference
+ first_component, separator, _remainder = reference.partition("/")
+ if not separator:
+ return f"docker.io/library/{reference}"
+ if first_component == "localhost" or "." in first_component or ":" in first_component:
+ return reference
+ return f"docker.io/{reference}"
diff --git a/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/worker.py b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/worker.py
new file mode 100644
index 00000000..20ea3f81
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/agent_runner/agent_runner/worker.py
@@ -0,0 +1,102 @@
+"""JSON-lines worker executed only inside the pinned runner container."""
+
+from __future__ import annotations
+
+import argparse
+import importlib.util
+import io
+import json
+import sys
+import traceback
+from contextlib import redirect_stderr, redirect_stdout
+from pathlib import Path
+from typing import Any
+
+from isolation_engine import Isolation
+
+from .compat import invoke_agent
+
+MAX_CAPTURE = 8_192
+
+
+def load_player(player_id: int) -> Any:
+ source_path = Path("/workspace/agent.py")
+ spec = importlib.util.spec_from_file_location("submitted_agent", source_path)
+ if spec is None or spec.loader is None:
+ raise ImportError("unable to load agent.py")
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ player_class = getattr(module, "CustomPlayer", None)
+ if player_class is None:
+ raise TypeError("CustomPlayer is missing")
+ try:
+ player = player_class(player_id=player_id)
+ except TypeError:
+ try:
+ player = player_class(player_id)
+ except TypeError:
+ player = player_class()
+ player.player_id = player_id
+ player.context = None
+ return player
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--player-id", type=int, required=True)
+ args = parser.parse_args()
+ startup_output, startup_errors = io.StringIO(), io.StringIO()
+ try:
+ with redirect_stdout(startup_output), redirect_stderr(startup_errors):
+ player = load_player(args.player_id)
+ except Exception as error:
+ print(
+ json.dumps(
+ {
+ "status": "error",
+ "error": f"import: {type(error).__name__}: {error}",
+ "stdout": startup_output.getvalue()[-MAX_CAPTURE:],
+ "stderr": startup_errors.getvalue()[-MAX_CAPTURE:],
+ }
+ ),
+ flush=True,
+ )
+ return 1
+ print(
+ json.dumps(
+ {
+ "status": "ready",
+ "stdout": startup_output.getvalue()[-MAX_CAPTURE:],
+ "stderr": startup_errors.getvalue()[-MAX_CAPTURE:],
+ }
+ ),
+ flush=True,
+ )
+
+ for line in sys.stdin:
+ output, errors = io.StringIO(), io.StringIO()
+ try:
+ message = json.loads(line)
+ if message.get("type") != "choose_action":
+ raise ValueError("unsupported protocol message")
+ state = Isolation.from_dict(message["state"])
+ with redirect_stdout(output), redirect_stderr(errors):
+ action = invoke_agent(player, state, int(message["timeout_ms"]))
+ payload = {
+ "status": "ok" if action is not None else "empty",
+ "action": action,
+ "stdout": output.getvalue()[-MAX_CAPTURE:],
+ "stderr": errors.getvalue()[-MAX_CAPTURE:],
+ }
+ except Exception as error:
+ payload = {
+ "status": "error",
+ "error": f"{type(error).__name__}: {error}",
+ "stderr": traceback.format_exc(limit=4)[-MAX_CAPTURE:],
+ }
+ print(json.dumps(payload), flush=True)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/__init__.py b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/__init__.py
new file mode 100644
index 00000000..3fc685a9
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/__init__.py
@@ -0,0 +1,57 @@
+from .benchmark import (
+ BENCHMARK_STRATEGIES,
+ BENCHMARK_STRATEGY_LIST,
+ BenchmarkController,
+ BenchmarkStrategy,
+)
+from .match import FixtureResult, GameResult, ReplayFrame, play_game, score_fixture
+from .providers import (
+ BaselineController,
+ ControllerEmptyResponseError,
+ ControllerTimeoutError,
+ MoveResponse,
+ PlayerController,
+)
+from .state import (
+ ACTION_SET,
+ BITBOARD_SIZE,
+ BLANK_BOARD,
+ CELL_INDICES,
+ ENGINE_VERSION,
+ HEIGHT,
+ RULESET_VERSION,
+ WIDTH,
+ Action,
+ Isolation,
+ board_coordinates_to_index,
+ index_to_board_coordinates,
+)
+
+__all__ = [
+ "ACTION_SET",
+ "BENCHMARK_STRATEGIES",
+ "BENCHMARK_STRATEGY_LIST",
+ "BITBOARD_SIZE",
+ "BLANK_BOARD",
+ "CELL_INDICES",
+ "ENGINE_VERSION",
+ "HEIGHT",
+ "RULESET_VERSION",
+ "WIDTH",
+ "Action",
+ "BaselineController",
+ "BenchmarkController",
+ "BenchmarkStrategy",
+ "ControllerEmptyResponseError",
+ "ControllerTimeoutError",
+ "FixtureResult",
+ "GameResult",
+ "Isolation",
+ "MoveResponse",
+ "PlayerController",
+ "ReplayFrame",
+ "board_coordinates_to_index",
+ "index_to_board_coordinates",
+ "play_game",
+ "score_fixture",
+]
diff --git a/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/benchmark.py b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/benchmark.py
new file mode 100644
index 00000000..2a15eb2a
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/benchmark.py
@@ -0,0 +1,328 @@
+"""Trusted strategy variants used by the isolated local benchmark database."""
+
+from __future__ import annotations
+
+import random
+from dataclasses import dataclass
+from math import inf
+from time import perf_counter
+from typing import Literal
+
+from .providers import MoveResponse
+from .state import HEIGHT, WIDTH, Isolation, index_to_board_coordinates
+
+StrategyMode = Literal["random", "weighted", "greedy", "fixed", "iterative"]
+EvaluationName = Literal["mobility", "differential", "center", "territory", "partition"]
+
+
+@dataclass(frozen=True)
+class BenchmarkStrategy:
+ key: str
+ agent_name: str
+ description: str
+ mode: StrategyMode
+ evaluation: EvaluationName = "differential"
+ depth: int = 0
+ alpha_beta: bool = False
+ move_ordering: bool = False
+
+
+BENCHMARK_STRATEGY_LIST = (
+ BenchmarkStrategy(
+ "bench_random",
+ "Random-Uniform",
+ "Uniform random legal action; establishes the lower-bound control.",
+ "random",
+ ),
+ BenchmarkStrategy(
+ "bench_weighted",
+ "Random-Mobility",
+ "Random action weighted by the player's mobility after the move.",
+ "weighted",
+ "mobility",
+ ),
+ BenchmarkStrategy(
+ "bench_greedy_mob",
+ "Greedy-Mobility",
+ "One-ply choice maximizing only the player's next-move count.",
+ "greedy",
+ "mobility",
+ ),
+ BenchmarkStrategy(
+ "bench_greedy_diff",
+ "Greedy-Differential",
+ "One-ply choice maximizing own mobility minus opponent mobility.",
+ "greedy",
+ "differential",
+ ),
+ BenchmarkStrategy(
+ "bench_greedy_center",
+ "Greedy-Center",
+ "One-ply mobility differential with a small central-position bonus.",
+ "greedy",
+ "center",
+ ),
+ BenchmarkStrategy(
+ "bench_mm1_mob",
+ "Minimax-D1-Mobility",
+ "Depth-1 minimax using own mobility; compares search framing with greedy play.",
+ "fixed",
+ "mobility",
+ depth=1,
+ ),
+ BenchmarkStrategy(
+ "bench_mm2_mob",
+ "Minimax-D2-Mobility",
+ "Depth-2 minimax using own mobility; isolates one extra search ply.",
+ "fixed",
+ "mobility",
+ depth=2,
+ ),
+ BenchmarkStrategy(
+ "bench_mm2_diff",
+ "Minimax-D2-Differential",
+ "Depth-2 minimax with mobility differential; isolates evaluation choice.",
+ "fixed",
+ "differential",
+ depth=2,
+ ),
+ BenchmarkStrategy(
+ "bench_ab2_diff",
+ "AlphaBeta-D2",
+ "Depth-2 alpha-beta with the same differential evaluation as minimax.",
+ "fixed",
+ "differential",
+ depth=2,
+ alpha_beta=True,
+ ),
+ BenchmarkStrategy(
+ "bench_ab3_diff",
+ "AlphaBeta-D3",
+ "Depth-3 alpha-beta; isolates the value of an additional searched ply.",
+ "fixed",
+ "differential",
+ depth=3,
+ alpha_beta=True,
+ ),
+ BenchmarkStrategy(
+ "bench_ab3_ordered",
+ "AlphaBeta-D3-Ordered",
+ "Depth-3 alpha-beta with heuristic move ordering for stronger pruning.",
+ "fixed",
+ "differential",
+ depth=3,
+ alpha_beta=True,
+ move_ordering=True,
+ ),
+ BenchmarkStrategy(
+ "bench_ab3_center",
+ "AlphaBeta-D3-Center",
+ "Strong ordered depth-3 alpha-beta with a center-aware evaluation.",
+ "fixed",
+ "center",
+ depth=3,
+ alpha_beta=True,
+ move_ordering=True,
+ ),
+ BenchmarkStrategy(
+ "bench_id_diff",
+ "Iterative-AB-Differential",
+ "Primary strong contender: ordered alpha-beta deepened to the safe deadline.",
+ "iterative",
+ "differential",
+ depth=6,
+ alpha_beta=True,
+ move_ordering=True,
+ ),
+ BenchmarkStrategy(
+ "bench_id_territory",
+ "Territory-ID-AB",
+ "Iterative ordered alpha-beta evaluating knight-reachable territory and mobility.",
+ "iterative",
+ "territory",
+ depth=6,
+ alpha_beta=True,
+ move_ordering=True,
+ ),
+ BenchmarkStrategy(
+ "bench_id_partition",
+ "Partition-ID-AB",
+ "Experimental iterative agent detecting separated regions and maximizing territory.",
+ "iterative",
+ "partition",
+ depth=7,
+ alpha_beta=True,
+ move_ordering=True,
+ ),
+)
+BENCHMARK_STRATEGIES = {strategy.key: strategy for strategy in BENCHMARK_STRATEGY_LIST}
+
+
+class SearchDeadline(RuntimeError):
+ pass
+
+
+class BenchmarkController:
+ """One configurable controller shared by all benchmark comparisons."""
+
+ def __init__(self, strategy_key: str, seed: int = 0) -> None:
+ try:
+ self.strategy = BENCHMARK_STRATEGIES[strategy_key]
+ except KeyError as error:
+ raise ValueError(f"unknown benchmark strategy: {strategy_key}") from error
+ self.random = random.Random(seed)
+ self.player_id = 0
+
+ def reset(self, player_id: int) -> None:
+ self.player_id = player_id
+
+ def close(self) -> None:
+ return None
+
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ actions = state.actions()
+ if not actions:
+ return MoveResponse(None)
+ if self.strategy.mode == "random":
+ return MoveResponse(self.random.choice(actions))
+ if self.strategy.mode == "weighted":
+ weights = [max(1, self._own_mobility(state.result(action))) for action in actions]
+ return MoveResponse(self.random.choices(actions, weights=weights, k=1)[0])
+
+ fallback = max(actions, key=lambda action: self._evaluate(state.result(action)))
+ if state.locs[self.player_id] is None:
+ return MoveResponse(self._opening_action(state, actions, fallback))
+ if self.strategy.mode == "greedy":
+ return MoveResponse(fallback)
+
+ deadline = perf_counter() + max(0.001, (timeout_ms - 12) / 1000)
+ if self.strategy.mode == "fixed":
+ try:
+ return MoveResponse(self._root_search(state, self.strategy.depth, deadline))
+ except SearchDeadline:
+ return MoveResponse(fallback)
+
+ best_action = fallback
+ for depth in range(1, self.strategy.depth + 1):
+ try:
+ best_action = self._root_search(state, depth, deadline)
+ except SearchDeadline:
+ break
+ return MoveResponse(best_action)
+
+ def _opening_action(self, state: Isolation, actions: list[int], fallback: int) -> int:
+ center = (HEIGHT // 2, WIDTH // 2)
+ ranked = sorted(
+ actions,
+ key=lambda action: (
+ abs(index_to_board_coordinates(action)[0] - center[0])
+ + abs(index_to_board_coordinates(action)[1] - center[1]),
+ -self._evaluate(state.result(action)),
+ action,
+ ),
+ )
+ return ranked[0] if ranked else fallback
+
+ def _root_search(self, state: Isolation, depth: int, deadline: float) -> int:
+ self._check_deadline(deadline)
+ actions = self._ordered_actions(state, deadline)
+ best_action = actions[0]
+ best_value = -inf
+ alpha, beta = -inf, inf
+ for action in actions:
+ score = self._value(state.result(action), depth - 1, alpha, beta, deadline)
+ if score > best_value:
+ best_action, best_value = action, score
+ if self.strategy.alpha_beta:
+ alpha = max(alpha, best_value)
+ return best_action
+
+ def _value(
+ self,
+ state: Isolation,
+ depth: int,
+ alpha: float,
+ beta: float,
+ deadline: float,
+ ) -> float:
+ self._check_deadline(deadline)
+ if state.terminal_test():
+ return 1_000_000.0 if state.utility(self.player_id) > 0 else -1_000_000.0
+ if depth <= 0:
+ return self._evaluate(state)
+
+ maximizing = state.player() == self.player_id
+ value = -inf if maximizing else inf
+ for action in self._ordered_actions(state, deadline):
+ child_value = self._value(state.result(action), depth - 1, alpha, beta, deadline)
+ if maximizing:
+ value = max(value, child_value)
+ alpha = max(alpha, value)
+ else:
+ value = min(value, child_value)
+ beta = min(beta, value)
+ if self.strategy.alpha_beta and alpha >= beta:
+ break
+ return value
+
+ def _ordered_actions(self, state: Isolation, deadline: float) -> list[int]:
+ actions = state.actions()
+ if not self.strategy.move_ordering:
+ return actions
+ self._check_deadline(deadline)
+ maximizing = state.player() == self.player_id
+ return sorted(
+ actions,
+ key=lambda action: self._evaluate(state.result(action)),
+ reverse=maximizing,
+ )
+
+ def _evaluate(self, state: Isolation) -> float:
+ own_mobility = self._own_mobility(state)
+ opponent_mobility = len(state.liberties(state.locs[1 - self.player_id]))
+ if self.strategy.evaluation == "mobility":
+ return float(own_mobility)
+ differential = float(own_mobility - opponent_mobility)
+ if self.strategy.evaluation == "differential":
+ return differential
+ if self.strategy.evaluation == "center":
+ return differential * 2 + self._center_score(state.locs[self.player_id])
+
+ own_region = self._reachable_region(state, state.locs[self.player_id])
+ opponent_region = self._reachable_region(state, state.locs[1 - self.player_id])
+ territory = float(len(own_region) - len(opponent_region))
+ if self.strategy.evaluation == "territory":
+ return differential * 2 + territory
+ separated = own_region.isdisjoint(opponent_region)
+ return territory * (8 if separated else 1) + differential * 2
+
+ def _own_mobility(self, state: Isolation) -> int:
+ return len(state.liberties(state.locs[self.player_id]))
+
+ @staticmethod
+ def _center_score(location: int | None) -> float:
+ if location is None:
+ return 0.0
+ row, column = index_to_board_coordinates(location)
+ return -(abs(row - (HEIGHT - 1) / 2) + abs(column - (WIDTH - 1) / 2))
+
+ @staticmethod
+ def _reachable_region(state: Isolation, location: int | None) -> set[int]:
+ if location is None:
+ return set()
+ pending = list(state.liberties(location))
+ reached: set[int] = set()
+ while pending:
+ cell = pending.pop()
+ if cell in reached:
+ continue
+ reached.add(cell)
+ pending.extend(
+ next_cell for next_cell in state.liberties(cell) if next_cell not in reached
+ )
+ return reached
+
+ @staticmethod
+ def _check_deadline(deadline: float) -> None:
+ if perf_counter() >= deadline:
+ raise SearchDeadline
diff --git a/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/match.py b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/match.py
new file mode 100644
index 00000000..1a7900b5
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/match.py
@@ -0,0 +1,225 @@
+"""Authoritative game loop, canonical replay, and fixture scoring."""
+
+from __future__ import annotations
+
+from dataclasses import asdict, dataclass, field, replace
+from time import perf_counter
+from typing import Any
+
+from .providers import (
+ ControllerEmptyResponseError,
+ ControllerTimeoutError,
+ PlayerController,
+)
+from .state import ENGINE_VERSION, RULESET_VERSION, Isolation
+
+MAX_LOG_CHARS = 8_192
+
+
+@dataclass(frozen=True)
+class ReplayFrame:
+ """One board state plus the decision made from that state."""
+
+ ply_index: int
+ active_player: int
+ board: str
+ player_locations: tuple[int | None, int | None]
+ chosen_action: int | None = None
+ chosen_destination: int | None = None
+ legal_actions: list[int] = field(default_factory=list)
+ elapsed_ms: float = 0.0
+ stdout_excerpt: str = ""
+ stderr_excerpt: str = ""
+ outcome: str | None = None
+
+
+@dataclass
+class GameResult:
+ winner_player: int
+ loser_player: int
+ end_status: str
+ losing_reason: str
+ replay: list[ReplayFrame] = field(default_factory=list)
+ stdout_excerpt: str = ""
+ stderr_excerpt: str = ""
+ engine_version: str = ENGINE_VERSION
+ ruleset_version: str = RULESET_VERSION
+
+ def replay_dicts(self) -> list[dict[str, Any]]:
+ return [asdict(frame) for frame in self.replay]
+
+
+@dataclass(frozen=True)
+class FixtureResult:
+ game_winners: tuple[int, int]
+ points_a: int
+ points_b: int
+ winner: int | None
+ is_draw: bool
+
+
+def score_fixture(game_winners: tuple[int, int]) -> FixtureResult:
+ wins_a = game_winners.count(0)
+ if wins_a == 2:
+ return FixtureResult(game_winners, 3, 0, 0, False)
+ if wins_a == 0:
+ return FixtureResult(game_winners, 0, 3, 1, False)
+ return FixtureResult(game_winners, 1, 1, None, True)
+
+
+def play_game(
+ first: PlayerController,
+ second: PlayerController,
+ *,
+ initial_state: Isolation | None = None,
+ time_limit_ms: int = 150,
+) -> GameResult:
+ """Play one game and validate every proposed action outside the controller."""
+
+ state = initial_state or Isolation()
+ controllers = (first, second)
+ replay = [_empty_frame(state)]
+ stdout_parts: list[str] = []
+ stderr_parts: list[str] = []
+ winner = 1
+ loser = 0
+ end_status = "game_over"
+ losing_reason = "no legal moves"
+
+ try:
+ first.reset(0)
+ second.reset(1)
+ while not state.terminal_test():
+ active = state.player()
+ legal_actions = [int(action) for action in state.actions()]
+ started = perf_counter()
+ action: int | None = None
+ stdout = ""
+ stderr = ""
+ try:
+ response = controllers[active].choose_action(state, time_limit_ms)
+ elapsed_ms = (perf_counter() - started) * 1000
+ action = None if response.action is None else int(response.action)
+ stdout = response.stdout[-MAX_LOG_CHARS:]
+ stderr = response.stderr[-MAX_LOG_CHARS:]
+ if elapsed_ms > time_limit_ms:
+ raise ControllerTimeoutError(
+ f"decision exceeded {time_limit_ms} ms ({elapsed_ms:.2f} ms)"
+ )
+ if action is None:
+ raise ControllerEmptyResponseError("agent produced no action")
+ if action not in legal_actions:
+ end_status = "invalid_action"
+ losing_reason = f"illegal action {action}"
+ winner, loser = 1 - active, active
+ replay[-1] = _decision_frame(
+ state,
+ action,
+ None,
+ legal_actions,
+ elapsed_ms,
+ stdout,
+ stderr,
+ end_status,
+ )
+ break
+ except (ControllerTimeoutError, ControllerEmptyResponseError) as error:
+ elapsed_ms = (perf_counter() - started) * 1000
+ end_status = "timeout"
+ losing_reason = str(error)
+ winner, loser = 1 - active, active
+ replay[-1] = _decision_frame(
+ state,
+ action,
+ None,
+ legal_actions,
+ elapsed_ms,
+ stdout,
+ stderr,
+ end_status,
+ )
+ break
+ except Exception as error: # submitted controller errors lose the game
+ elapsed_ms = (perf_counter() - started) * 1000
+ end_status = "exception"
+ losing_reason = f"{type(error).__name__}: {error}"
+ winner, loser = 1 - active, active
+ replay[-1] = _decision_frame(
+ state,
+ action,
+ None,
+ legal_actions,
+ elapsed_ms,
+ stdout,
+ stderr,
+ end_status,
+ )
+ break
+
+ destination = state.destination(action)
+ replay[-1] = _decision_frame(
+ state,
+ action,
+ destination,
+ legal_actions,
+ elapsed_ms,
+ stdout,
+ stderr,
+ None,
+ )
+ stdout_parts.append(stdout)
+ stderr_parts.append(stderr)
+ state = state.result(action)
+ replay.append(_empty_frame(state))
+ else:
+ winner = 0 if state.utility(0) > 0 else 1
+ loser = 1 - winner
+ replay[-1] = replace(replay[-1], outcome="game_over")
+ finally:
+ first.close()
+ second.close()
+
+ return GameResult(
+ winner_player=winner,
+ loser_player=loser,
+ end_status=end_status,
+ losing_reason=losing_reason,
+ replay=replay,
+ stdout_excerpt="\n".join(stdout_parts)[-MAX_LOG_CHARS:],
+ stderr_excerpt="\n".join(stderr_parts)[-MAX_LOG_CHARS:],
+ )
+
+
+def _empty_frame(state: Isolation) -> ReplayFrame:
+ return ReplayFrame(
+ ply_index=state.ply_count,
+ active_player=state.player(),
+ board=str(state.board),
+ player_locations=state.locs,
+ legal_actions=[int(action) for action in state.actions()],
+ )
+
+
+def _decision_frame(
+ state: Isolation,
+ action: int | None,
+ destination: int | None,
+ legal_actions: list[int],
+ elapsed_ms: float,
+ stdout: str,
+ stderr: str,
+ outcome: str | None,
+) -> ReplayFrame:
+ return ReplayFrame(
+ ply_index=state.ply_count,
+ active_player=state.player(),
+ board=str(state.board),
+ player_locations=state.locs,
+ chosen_action=action,
+ chosen_destination=destination,
+ legal_actions=legal_actions,
+ elapsed_ms=round(elapsed_ms, 3),
+ stdout_excerpt=stdout,
+ stderr_excerpt=stderr,
+ outcome=outcome,
+ )
diff --git a/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/providers.py b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/providers.py
new file mode 100644
index 00000000..23d94db5
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/providers.py
@@ -0,0 +1,97 @@
+"""Trusted controllers used by the Knight Isolation match loop.
+
+The game rules are derived from the Udacity AIND Isolation project and remain
+available under its MIT license; see ``state.py`` for the complete notice.
+"""
+
+from __future__ import annotations
+
+import random
+from dataclasses import dataclass
+from typing import Protocol
+
+from .state import Isolation
+
+
+class ControllerTimeoutError(TimeoutError):
+ """Raised when a controller misses the external move deadline."""
+
+
+class ControllerEmptyResponseError(RuntimeError):
+ """Raised when a controller returns no action on a non-terminal state."""
+
+
+@dataclass(frozen=True)
+class MoveResponse:
+ action: int | None
+ stdout: str = ""
+ stderr: str = ""
+
+
+class PlayerController(Protocol):
+ """The small interface shared by trusted baselines and Docker agents."""
+
+ def reset(self, player_id: int) -> None: ...
+
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse: ...
+
+ def close(self) -> None: ...
+
+
+class BaselineController:
+ """A trusted Random, Greedy, or fixed-depth Minimax opponent."""
+
+ def __init__(self, name: str, seed: int = 0, minimax_depth: int = 2) -> None:
+ normalized = name.lower()
+ if normalized not in {"random", "greedy", "minimax"}:
+ raise ValueError(f"unknown baseline: {name}")
+ self.name = normalized
+ self.random = random.Random(seed)
+ self.player_id = 0
+ self.minimax_depth = minimax_depth
+
+ def reset(self, player_id: int) -> None:
+ self.player_id = player_id
+
+ def close(self) -> None:
+ return None
+
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ del timeout_ms
+ actions = state.actions()
+ if not actions:
+ return MoveResponse(None)
+ if self.name == "random":
+ return MoveResponse(self.random.choice(actions))
+ if self.name == "greedy":
+ return MoveResponse(
+ max(actions, key=lambda action: self._mobility(state.result(action)))
+ )
+ if state.ply_count < 2:
+ center = 57
+ return MoveResponse(center if center in actions else actions[len(actions) // 2])
+ return MoveResponse(self._minimax(state, self.minimax_depth))
+
+ def _mobility(self, state: Isolation) -> int:
+ return len(state.liberties(state.locs[self.player_id]))
+
+ def _score(self, state: Isolation) -> int:
+ own = len(state.liberties(state.locs[self.player_id]))
+ other = len(state.liberties(state.locs[1 - self.player_id]))
+ return own - other
+
+ def _minimax(self, state: Isolation, depth: int) -> int:
+ def value(position: Isolation, remaining: int) -> float:
+ if position.terminal_test():
+ return position.utility(self.player_id)
+ if remaining == 0:
+ return float(self._score(position))
+ candidates = [
+ value(position.result(action), remaining - 1) for action in position.actions()
+ ]
+ return max(candidates) if position.player() == self.player_id else min(candidates)
+
+ return max(
+ state.actions(),
+ key=lambda action: value(state.result(action), depth - 1),
+ )
diff --git a/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/state.py b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/state.py
new file mode 100644
index 00000000..1ea67520
--- /dev/null
+++ b/Projects/3_Adversarial Search/packages/isolation_engine/isolation_engine/state.py
@@ -0,0 +1,154 @@
+"""Knight Isolation bitboard state.
+
+Copyright (c) 2018 Udacity
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+"""
+
+from __future__ import annotations
+
+from enum import IntEnum
+from typing import Any, NamedTuple
+
+WIDTH = 11
+HEIGHT = 9
+PADDED_WIDTH = WIDTH + 2
+BITBOARD_SIZE = PADDED_WIDTH * HEIGHT - 2
+ENGINE_VERSION = "agent-arena-isolation-1.0.0"
+RULESET_VERSION = "knight-isolation-11x9-150ms-v1"
+
+BLANK_BOARD = 0
+_row = (1 << WIDTH) - 1
+for _ in range(HEIGHT):
+ BLANK_BOARD = (BLANK_BOARD << PADDED_WIDTH) | _row
+
+SOUTH, NORTH, WEST, EAST = -PADDED_WIDTH, PADDED_WIDTH, 1, -1
+
+
+class Action(IntEnum):
+ """The eight legal knight offsets in the padded bitboard."""
+
+ NNE = NORTH + NORTH + EAST
+ ENE = EAST + NORTH + EAST
+ ESE = EAST + SOUTH + EAST
+ SSE = SOUTH + SOUTH + EAST
+ SSW = SOUTH + SOUTH + WEST
+ WSW = WEST + SOUTH + WEST
+ WNW = WEST + NORTH + WEST
+ NNW = NORTH + NORTH + WEST
+
+
+ACTION_SET = set(Action)
+CELL_INDICES = tuple(
+ row * PADDED_WIDTH + column for row in range(HEIGHT) for column in range(WIDTH)
+)
+
+
+class Isolation(NamedTuple):
+ """Immutable and hashable Knight Isolation state adapted from Udacity."""
+
+ board: int = BLANK_BOARD
+ ply_count: int = 0
+ locs: tuple[int | None, int | None] = (None, None)
+
+ def player(self) -> int:
+ return self.ply_count % 2
+
+ def actions(self) -> list[int]:
+ location = self.locs[self.player()]
+ if location is None:
+ return self.liberties(None)
+ return [
+ int(action)
+ for action in Action
+ if location + int(action) >= 0 and self.board & (1 << (location + int(action)))
+ ]
+
+ def result(self, action: int) -> Isolation:
+ current_player = self.player()
+ location = self.locs[current_player]
+ if location is not None and action not in ACTION_SET:
+ raise ValueError(f"{action} is not a knight action")
+ destination = int(action) if location is None else location + int(action)
+ if destination < 0 or not self.board & (1 << destination):
+ raise ValueError("invalid action: destination is blocked or off board")
+ next_locs = list(self.locs)
+ next_locs[current_player] = destination
+ return Isolation(
+ board=self.board ^ (1 << destination),
+ ply_count=self.ply_count + 1,
+ locs=(next_locs[0], next_locs[1]),
+ )
+
+ def terminal_test(self) -> bool:
+ return not (self._has_liberties(0) and self._has_liberties(1))
+
+ def utility(self, player_id: int) -> float:
+ if not self.terminal_test():
+ return 0.0
+ player_is_active = player_id == self.player()
+ active_has_liberties = self._has_liberties(self.player())
+ active_player_wins = active_has_liberties == player_is_active
+ return float("inf") if active_player_wins else float("-inf")
+
+ def liberties(self, location: int | None) -> list[int]:
+ cells = (
+ range(BITBOARD_SIZE)
+ if location is None
+ else (location + int(action) for action in Action)
+ )
+ return [cell for cell in cells if cell >= 0 and self.board & (1 << cell)]
+
+ def _has_liberties(self, player_id: int) -> bool:
+ return any(self.liberties(self.locs[player_id]))
+
+ def destination(self, action: int) -> int:
+ location = self.locs[self.player()]
+ return int(action) if location is None else location + int(action)
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "board": str(self.board),
+ "ply_count": self.ply_count,
+ "locs": list(self.locs),
+ }
+
+ @classmethod
+ def from_dict(cls, value: dict[str, Any]) -> Isolation:
+ locations = value.get("locs", [None, None])
+ return cls(
+ board=int(value["board"]),
+ ply_count=int(value["ply_count"]),
+ locs=(locations[0], locations[1]),
+ )
+
+
+def index_to_board_coordinates(index: int) -> tuple[int, int]:
+ """Return top-left-origin row/column coordinates for a valid cell index."""
+
+ bit_row, bit_column = divmod(index, PADDED_WIDTH)
+ if bit_column >= WIDTH or bit_row >= HEIGHT:
+ raise ValueError(f"{index} is a padding bit, not a board cell")
+ return HEIGHT - 1 - bit_row, WIDTH - 1 - bit_column
+
+
+def board_coordinates_to_index(row: int, column: int) -> int:
+ if not (0 <= row < HEIGHT and 0 <= column < WIDTH):
+ raise ValueError("board coordinates are out of range")
+ return (HEIGHT - 1 - row) * PADDED_WIDTH + (WIDTH - 1 - column)
diff --git a/Projects/3_Adversarial Search/pyproject.toml b/Projects/3_Adversarial Search/pyproject.toml
new file mode 100644
index 00000000..a193ccdf
--- /dev/null
+++ b/Projects/3_Adversarial Search/pyproject.toml
@@ -0,0 +1,54 @@
+[build-system]
+requires = ["setuptools==80.9.0", "wheel==0.45.1"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "agent-arena-beta"
+version = "0.2.0"
+description = "Local Knight Isolation developer-agent competition beta"
+requires-python = ">=3.12,<3.13"
+dependencies = [
+ "alembic==1.16.4",
+ "fastapi==0.116.1",
+ "pydantic-settings==2.10.1",
+ "sqlalchemy==2.0.41",
+ "uvicorn[standard]==0.35.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "httpx==0.28.1",
+ "pytest==8.4.1",
+ "ruff==0.12.3",
+]
+
+[project.scripts]
+agent-arena = "agent_arena_api.cli:main"
+
+[tool.setuptools.packages.find]
+where = ["apps/api", "packages/isolation_engine", "packages/agent_runner"]
+
+[tool.ruff]
+line-length = 100
+target-version = "py312"
+exclude = [
+ ".venv",
+ "node_modules",
+ "dist",
+ "alembic/versions",
+ "tests/test_my_custom_player.py",
+]
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "B", "UP", "SIM"]
+
+[tool.pytest.ini_options]
+addopts = "-q --strict-markers"
+testpaths = ["tests/backend", "tests/integration"]
+markers = ["docker: requires the built local Docker runner image"]
+pythonpath = [
+ ".",
+ "apps/api",
+ "packages/isolation_engine",
+ "packages/agent_runner",
+]
diff --git a/Projects/3_Adversarial Search/scripts/benchmark.py b/Projects/3_Adversarial Search/scripts/benchmark.py
new file mode 100644
index 00000000..a6883b06
--- /dev/null
+++ b/Projects/3_Adversarial Search/scripts/benchmark.py
@@ -0,0 +1,48 @@
+"""Launch Agent Arena with an isolated strategy benchmark database."""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+
+from dev import ROOT, development_environment, run_development
+
+BENCHMARK_ROOT = ROOT / "var" / "benchmark"
+
+
+def benchmark_environment() -> dict[str, str]:
+ database_path = (BENCHMARK_ROOT / "agent_arena.db").as_posix()
+ return development_environment(
+ {
+ "AGENT_ARENA_DATABASE_URL": f"sqlite:///{database_path}",
+ "AGENT_ARENA_SOURCE_ROOT": str(BENCHMARK_ROOT / "sources"),
+ "AGENT_ARENA_TEMP_ROOT": str(BENCHMARK_ROOT / "tmp"),
+ "AGENT_ARENA_DEMO_OWNER_NAME": "benchmark-lab",
+ }
+ )
+
+
+def prepare_benchmark(environment: dict[str, str]) -> None:
+ BENCHMARK_ROOT.mkdir(parents=True, exist_ok=True)
+ subprocess.run(
+ [sys.executable, "-m", "alembic", "upgrade", "head"],
+ cwd=ROOT,
+ env=environment,
+ check=True,
+ )
+ subprocess.run(
+ [sys.executable, "-m", "agent_arena_api.benchmark_seed"],
+ cwd=ROOT,
+ env=environment,
+ check=True,
+ )
+
+
+def main() -> int:
+ environment = benchmark_environment()
+ prepare_benchmark(environment)
+ return run_development(environment)
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/scripts/dev.py b/Projects/3_Adversarial Search/scripts/dev.py
new file mode 100644
index 00000000..9eb92e04
--- /dev/null
+++ b/Projects/3_Adversarial Search/scripts/dev.py
@@ -0,0 +1,81 @@
+from __future__ import annotations
+
+import os
+import signal
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+RELOAD_DIRECTORIES = (
+ ROOT / "apps" / "api" / "agent_arena_api",
+ ROOT / "packages" / "isolation_engine" / "isolation_engine",
+ ROOT / "packages" / "agent_runner" / "agent_runner",
+)
+
+
+def api_command() -> list[str]:
+ command = [sys.executable, "-m", "uvicorn", "agent_arena_api.main:app", "--reload"]
+ for directory in RELOAD_DIRECTORIES:
+ command.extend(("--reload-dir", str(directory)))
+ return command
+
+
+def development_environment(overrides: dict[str, str] | None = None) -> dict[str, str]:
+ env = {
+ **os.environ,
+ "PYTHONPATH": os.pathsep.join(
+ str(ROOT / path)
+ for path in (
+ "apps/api",
+ "packages/isolation_engine",
+ "packages/agent_runner",
+ )
+ ),
+ }
+ env.update(overrides or {})
+ return env
+
+
+def run_development(env: dict[str, str] | None = None) -> int:
+ active_env = env or development_environment()
+ api = subprocess.Popen(api_command(), cwd=ROOT, env=active_env)
+ npm = "npm.cmd" if os.name == "nt" else "npm"
+ web = subprocess.Popen([npm, "run", "dev"], cwd=ROOT / "apps" / "web", env=active_env)
+ processes = (api, web)
+
+ def stop(*_: object) -> None:
+ for process in processes:
+ stop_process(process)
+
+ signal.signal(signal.SIGINT, stop)
+ signal.signal(signal.SIGTERM, stop)
+ try:
+ while all(process.poll() is None for process in processes):
+ time.sleep(0.25)
+ return next((process.returncode for process in processes if process.returncode), 0)
+ finally:
+ stop()
+
+
+def main() -> int:
+ return run_development()
+
+
+def stop_process(process: subprocess.Popen[bytes]) -> None:
+ if process.poll() is not None:
+ return
+ if os.name == "nt":
+ subprocess.run(
+ ["taskkill", "/PID", str(process.pid), "/T", "/F"],
+ check=False,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ else:
+ process.terminate()
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/scripts/startup_check.py b/Projects/3_Adversarial Search/scripts/startup_check.py
new file mode 100644
index 00000000..060803fb
--- /dev/null
+++ b/Projects/3_Adversarial Search/scripts/startup_check.py
@@ -0,0 +1,125 @@
+from __future__ import annotations
+
+import json
+import os
+import subprocess
+import sys
+import time
+import urllib.request
+from contextlib import suppress
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+CHECK_DATABASE = ROOT / "var" / "startup_check.db"
+
+
+def main() -> int:
+ cleanup_database()
+ env = {
+ **os.environ,
+ "AGENT_ARENA_DATABASE_URL": f"sqlite:///{CHECK_DATABASE.as_posix()}",
+ "PYTHONPATH": os.pathsep.join(
+ str(ROOT / path)
+ for path in (
+ "apps/api",
+ "packages/isolation_engine",
+ "packages/agent_runner",
+ )
+ ),
+ }
+ migration = subprocess.run(
+ [sys.executable, "-m", "alembic", "upgrade", "head"],
+ cwd=ROOT,
+ env=env,
+ check=False,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ )
+ if migration.returncode != 0:
+ print("The startup-check migration failed", flush=True)
+ cleanup_database()
+ return migration.returncode
+ api = subprocess.Popen(
+ [sys.executable, "-m", "uvicorn", "agent_arena_api.main:app", "--port", "8011"],
+ cwd=ROOT,
+ env=env,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ )
+ npm = "npm.cmd" if os.name == "nt" else "npm"
+ web = subprocess.Popen(
+ [npm, "run", "dev", "--", "--port", "5181"],
+ cwd=ROOT / "apps" / "web",
+ env=env,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT,
+ )
+ processes = (api, web)
+ try:
+ for _ in range(50):
+ try:
+ with urllib.request.urlopen(
+ "http://127.0.0.1:8011/api/health", timeout=1
+ ) as response:
+ payload = json.load(response)
+ with urllib.request.urlopen(
+ "http://127.0.0.1:8011/api/leaderboard", timeout=1
+ ) as response:
+ leaderboard = json.load(response)
+ with urllib.request.urlopen("http://127.0.0.1:5181", timeout=1) as response:
+ web_page = response.read().decode("utf-8")
+ if (
+ payload.get("status") == "ok"
+ and isinstance(leaderboard, list)
+ and "Agent Arena" in web_page
+ ):
+ print(json.dumps(payload, indent=2), flush=True)
+ print("web: ready", flush=True)
+ return 0
+ except Exception:
+ time.sleep(0.1)
+ print("API and web did not both become ready", flush=True)
+ return 1
+ finally:
+ for process in processes:
+ stop_process(process)
+ cleanup_database()
+
+
+def stop_process(process: subprocess.Popen[bytes]) -> None:
+ if process.poll() is not None:
+ return
+ if os.name == "nt":
+ subprocess.run(
+ ["taskkill", "/PID", str(process.pid), "/T", "/F"],
+ check=False,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ with suppress(subprocess.TimeoutExpired):
+ process.wait(timeout=5)
+ return
+ process.terminate()
+ try:
+ process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ process.kill()
+
+
+def cleanup_database() -> None:
+ paths = (
+ CHECK_DATABASE,
+ CHECK_DATABASE.with_name(f"{CHECK_DATABASE.name}-wal"),
+ CHECK_DATABASE.with_name(f"{CHECK_DATABASE.name}-shm"),
+ )
+ for _ in range(20):
+ try:
+ for path in paths:
+ path.unlink(missing_ok=True)
+ return
+ except PermissionError:
+ time.sleep(0.1)
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Projects/3_Adversarial Search/tests/backend/test_benchmark.py b/Projects/3_Adversarial Search/tests/backend/test_benchmark.py
new file mode 100644
index 00000000..9053cb43
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/backend/test_benchmark.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from time import perf_counter
+
+from agent_arena_api.benchmark_seed import BENCHMARK_OWNER, seed_benchmark_database
+from agent_arena_api.database import Database
+from agent_arena_api.models import Agent, Base, Fixture, Submission
+from isolation_engine import BENCHMARK_STRATEGY_LIST, BenchmarkController, Isolation
+from sqlalchemy import func, select
+
+
+def test_benchmark_catalog_is_deliberate_and_unique() -> None:
+ assert len(BENCHMARK_STRATEGY_LIST) == 15
+ assert len({strategy.key for strategy in BENCHMARK_STRATEGY_LIST}) == 15
+ assert len({strategy.agent_name for strategy in BENCHMARK_STRATEGY_LIST}) == 15
+ assert {strategy.mode for strategy in BENCHMARK_STRATEGY_LIST} == {
+ "random",
+ "weighted",
+ "greedy",
+ "fixed",
+ "iterative",
+ }
+ assert {strategy.evaluation for strategy in BENCHMARK_STRATEGY_LIST} == {
+ "mobility",
+ "differential",
+ "center",
+ "territory",
+ "partition",
+ }
+
+
+def test_every_benchmark_strategy_returns_a_legal_action_within_budget() -> None:
+ state = Isolation().result(57).result(0)
+ for strategy in BENCHMARK_STRATEGY_LIST:
+ controller = BenchmarkController(strategy.key, seed=17)
+ controller.reset(state.player())
+ started = perf_counter()
+ response = controller.choose_action(state, timeout_ms=150)
+ elapsed_ms = (perf_counter() - started) * 1000
+ assert response.action in state.actions(), strategy.agent_name
+ assert elapsed_ms < 200, strategy.agent_name
+
+
+def test_benchmark_seed_is_isolated_and_does_not_create_fixtures(tmp_path) -> None:
+ normal = Database.create(f"sqlite:///{(tmp_path / 'normal.db').as_posix()}")
+ benchmark = Database.create(f"sqlite:///{(tmp_path / 'benchmark.db').as_posix()}")
+ Base.metadata.create_all(normal.engine)
+ Base.metadata.create_all(benchmark.engine)
+
+ result = seed_benchmark_database(benchmark)
+ assert result == {"agents": 15, "submissions": 15, "fixtures": 0}
+ assert seed_benchmark_database(benchmark) == result
+
+ with normal.sessions() as session:
+ assert session.scalar(select(func.count(Agent.id))) == 0
+ with benchmark.sessions() as session:
+ assert set(session.scalars(select(Agent.owner_name))) == {BENCHMARK_OWNER}
+ assert set(session.scalars(select(Submission.execution_kind))) == {
+ strategy.key for strategy in BENCHMARK_STRATEGY_LIST
+ }
+ assert session.scalar(select(func.count(Fixture.id))) == 0
+
+ normal.engine.dispose()
+ benchmark.engine.dispose()
diff --git a/Projects/3_Adversarial Search/tests/backend/test_dev_script.py b/Projects/3_Adversarial Search/tests/backend/test_dev_script.py
new file mode 100644
index 00000000..8ea43cf9
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/backend/test_dev_script.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from scripts.dev import RELOAD_DIRECTORIES, ROOT, api_command
+
+
+def test_dev_reloader_watches_source_packages_not_runtime_state() -> None:
+ command = api_command()
+ watched = [command[index + 1] for index, value in enumerate(command) if value == "--reload-dir"]
+
+ assert watched == [str(directory) for directory in RELOAD_DIRECTORIES]
+ assert str(ROOT / "var") not in watched
+ assert str(ROOT) not in watched
diff --git a/Projects/3_Adversarial Search/tests/backend/test_engine.py b/Projects/3_Adversarial Search/tests/backend/test_engine.py
new file mode 100644
index 00000000..d7c3eb5f
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/backend/test_engine.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+from time import sleep
+
+import pytest
+from isolation_engine import (
+ BLANK_BOARD,
+ BaselineController,
+ Isolation,
+ MoveResponse,
+ play_game,
+ score_fixture,
+)
+
+from tests.conftest import FirstLegalController
+
+
+def test_bitboard_opening_and_knight_actions() -> None:
+ state = Isolation()
+ assert state.board == BLANK_BOARD
+ assert len(state.actions()) == 99
+ opened = state.result(57).result(0)
+ assert set(opened.actions()) == {25, 11, -15, -27, -25, -11, 15, 27}
+ assert not opened.board & (1 << 57)
+ assert not opened.board & 1
+
+
+def test_result_is_immutable_and_hashable() -> None:
+ state = Isolation()
+ moved = state.result(57)
+ assert state.ply_count == 0
+ assert moved.ply_count == 1
+ assert hash(state) != hash(moved)
+ with pytest.raises(AttributeError):
+ state.board = 0 # type: ignore[misc]
+
+
+def test_terminal_utility_and_state_round_trip() -> None:
+ state = Isolation()
+ while not state.terminal_test():
+ state = state.result(state.actions()[0])
+ assert state.utility(0) in {float("inf"), float("-inf")}
+ assert state.utility(0) == -state.utility(1)
+ assert Isolation.from_dict(state.to_dict()) == state
+
+
+@pytest.mark.parametrize(
+ ("first", "second"),
+ [("random", "greedy"), ("greedy", "minimax"), ("minimax", "random")],
+)
+def test_baselines_complete_games(first: str, second: str) -> None:
+ result = play_game(
+ BaselineController(first, seed=4),
+ BaselineController(second, seed=9),
+ time_limit_ms=500,
+ )
+ assert result.end_status == "game_over"
+ assert len(result.replay) > 2
+ assert result.replay[-1].outcome == "game_over"
+
+
+class SlowController(FirstLegalController):
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ sleep(0.005)
+ return super().choose_action(state, timeout_ms)
+
+
+class ErrorController(FirstLegalController):
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ del state, timeout_ms
+ raise RuntimeError("boom")
+
+
+class EmptyController(FirstLegalController):
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ del state, timeout_ms
+ return MoveResponse(None)
+
+
+class IllegalController(FirstLegalController):
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ del state, timeout_ms
+ return MoveResponse(999)
+
+
+@pytest.mark.parametrize(
+ ("controller", "status"),
+ [
+ (SlowController(), "timeout"),
+ (ErrorController(), "exception"),
+ (EmptyController(), "timeout"),
+ (IllegalController(), "invalid_action"),
+ ],
+)
+def test_bad_action_outcomes(controller: FirstLegalController, status: str) -> None:
+ result = play_game(controller, FirstLegalController(), time_limit_ms=1)
+ assert result.end_status == status
+ assert result.winner_player == 1
+ assert result.replay[-1].outcome == status
+
+
+@pytest.mark.parametrize(
+ ("winners", "points"),
+ [((0, 0), (3, 0)), ((0, 1), (1, 1)), ((1, 1), (0, 3))],
+)
+def test_fixture_points(winners: tuple[int, int], points: tuple[int, int]) -> None:
+ result = score_fixture(winners)
+ assert (result.points_a, result.points_b) == points
+
+
+def test_one_replay_list_reconstructs_every_legal_move() -> None:
+ result = play_game(FirstLegalController(), FirstLegalController(), time_limit_ms=150)
+ state = Isolation()
+ for frame in result.replay:
+ assert frame.board == str(state.board)
+ assert frame.player_locations == state.locs
+ if frame.chosen_action is not None and frame.outcome is None:
+ assert frame.chosen_action in state.actions()
+ state = state.result(frame.chosen_action)
+ assert result.replay[-1].outcome == "game_over"
diff --git a/Projects/3_Adversarial Search/tests/backend/test_operations.py b/Projects/3_Adversarial Search/tests/backend/test_operations.py
new file mode 100644
index 00000000..4cd94eeb
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/backend/test_operations.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import hashlib
+
+from agent_arena_api.competition import build_leaderboard, run_round
+from agent_arena_api.database import Database
+from agent_arena_api.models import Agent, Base, Submission
+from agent_arena_api.seed import seed_database
+from agent_arena_api.submissions import PreparedSubmission, save_new_agent, save_submission
+
+
+def accepted(source: str) -> PreparedSubmission:
+ source_bytes = source.encode()
+ return PreparedSubmission(
+ source=source_bytes,
+ source_hash=hashlib.sha256(source_bytes).hexdigest(),
+ validation={"accepted": True},
+ )
+
+
+def test_new_valid_submission_becomes_active_without_mutating_previous(test_settings) -> None:
+ database = Database.create(test_settings.database_url)
+ Base.metadata.create_all(database.engine)
+ with database.sessions.begin() as session:
+ agent = save_new_agent(
+ session,
+ test_settings,
+ owner_name="oualid",
+ name="SimpleAgent",
+ prepared=accepted("class CustomPlayer: pass"),
+ )
+ first_id = agent.active_submission_id
+ first_hash = session.get(Submission, first_id).source_hash
+ second = save_submission(
+ session,
+ test_settings,
+ agent=agent,
+ prepared=accepted("class CustomPlayer:\n pass\n"),
+ )
+ assert agent.active_submission_id == second.id
+ assert session.get(Submission, first_id).source_hash == first_hash
+ with database.sessions() as session:
+ stored = session.get(Agent, agent.id)
+ assert stored.active_submission_id == second.id
+
+
+def test_round_is_balanced_and_leaderboard_is_derived_from_games(test_settings) -> None:
+ database = Database.create(test_settings.database_url)
+ Base.metadata.create_all(database.engine)
+ seed_database(database, test_settings)
+ fixture_ids = run_round(database, test_settings)
+ assert len(fixture_ids) == 6
+ with database.sessions() as session:
+ table = build_leaderboard(session)
+ assert len(table) == 4
+ assert {entry["fixtures"] for entry in table} == {6}
+ assert all(entry["points"] >= 0 for entry in table)
+ assert all(entry["rank"] is None for entry in table)
+
+ with database.sessions() as session:
+ qualified_table = build_leaderboard(session, minimum_ranked_fixtures=6)
+ assert [entry["rank"] for entry in qualified_table] == [1, 2, 3, 4]
+ ordering_keys = [
+ (
+ -entry["points_per_fixture"],
+ -entry["points"],
+ -entry["wins"],
+ entry["agent_name"].lower(),
+ )
+ for entry in qualified_table
+ ]
+ assert ordering_keys == sorted(ordering_keys)
diff --git a/Projects/3_Adversarial Search/tests/backend/test_runner_compat.py b/Projects/3_Adversarial Search/tests/backend/test_runner_compat.py
new file mode 100644
index 00000000..542eeb83
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/backend/test_runner_compat.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+import subprocess
+from time import sleep
+
+import pytest
+from agent_runner import DecisionDeadlineExceeded, DockerAgent, invoke_agent
+from agent_runner.docker import _canonical_image_reference
+from isolation_engine import Isolation
+
+
+class QueuePlayer:
+ def get_action(self, state: Isolation) -> None:
+ self.queue.put(state.actions()[0])
+
+
+class ReturnPlayer:
+ def get_action(self, state: Isolation) -> int:
+ return state.actions()[-1]
+
+
+class SlowPlayer:
+ def get_action(self, state: Isolation) -> int:
+ sleep(0.004)
+ return state.actions()[0]
+
+
+def test_queue_and_return_agents_are_compatible() -> None:
+ state = Isolation()
+ assert invoke_agent(QueuePlayer(), state, 150) in state.actions()
+ assert invoke_agent(ReturnPlayer(), state, 150) in state.actions()
+
+
+def test_runner_adapter_enforces_internal_deadline() -> None:
+ with pytest.raises(DecisionDeadlineExceeded):
+ invoke_agent(SlowPlayer(), Isolation(), 1)
+
+
+def test_docker_diagnostic_uses_resolved_windows_executable(monkeypatch) -> None:
+ executable = r"C:\Program Files\Docker\Docker\resources\bin\docker.EXE"
+ commands: list[list[str]] = []
+
+ monkeypatch.setattr("agent_runner.docker.shutil.which", lambda _name: executable)
+
+ def inspect(command: list[str], **_kwargs) -> subprocess.CompletedProcess[str]:
+ commands.append(command)
+ return subprocess.CompletedProcess(command, 0, stdout="sha256:runner\n", stderr="")
+
+ monkeypatch.setattr("agent_runner.docker.subprocess.run", inspect)
+
+ assert DockerAgent.diagnostic()["code"] == "ready"
+ assert commands == [
+ [
+ executable,
+ "image",
+ "inspect",
+ "docker.io/library/agent-arena-runner:py312-v1",
+ "--format",
+ "{{.Id}}",
+ ]
+ ]
+
+
+@pytest.mark.parametrize(
+ ("stderr", "expected_code"),
+ [
+ ("Error response from daemon: No such image: missing:latest", "runner_image_missing"),
+ (
+ "permission denied while trying to connect to the docker API at "
+ "npipe:////./pipe/dockerDesktopLinuxEngine",
+ "docker_daemon_unavailable",
+ ),
+ ],
+)
+def test_docker_diagnostic_distinguishes_missing_image_from_daemon_failure(
+ monkeypatch,
+ stderr: str,
+ expected_code: str,
+) -> None:
+ monkeypatch.setattr("agent_runner.docker.shutil.which", lambda _name: "docker.EXE")
+ monkeypatch.setattr(
+ "agent_runner.docker.subprocess.run",
+ lambda command, **_kwargs: subprocess.CompletedProcess(
+ command,
+ 1,
+ stdout="[]\n",
+ stderr=stderr,
+ ),
+ )
+
+ result = DockerAgent.diagnostic("missing:latest")
+
+ assert result["available"] is False
+ assert result["code"] == expected_code
+ if expected_code == "docker_daemon_unavailable":
+ assert "permission denied" in result["message"]
+
+
+@pytest.mark.parametrize(
+ ("image", "expected"),
+ [
+ ("agent-arena-runner:py312-v1", "docker.io/library/agent-arena-runner:py312-v1"),
+ ("example/team-agent:v1", "docker.io/example/team-agent:v1"),
+ ("localhost:5000/team-agent:v1", "localhost:5000/team-agent:v1"),
+ ("registry.example.com/team-agent:v1", "registry.example.com/team-agent:v1"),
+ ("e5354ca0ee27", "e5354ca0ee27"),
+ ],
+)
+def test_docker_image_reference_normalization(image: str, expected: str) -> None:
+ assert _canonical_image_reference(image) == expected
diff --git a/Projects/3_Adversarial Search/tests/conftest.py b/Projects/3_Adversarial Search/tests/conftest.py
new file mode 100644
index 00000000..33bd08bc
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/conftest.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from agent_arena_api.config import Settings
+from isolation_engine import Isolation, MoveResponse
+
+
+class FirstLegalController:
+ def reset(self, player_id: int) -> None:
+ self.player_id = player_id
+
+ def choose_action(self, state: Isolation, timeout_ms: int) -> MoveResponse:
+ del timeout_ms
+ actions = state.actions()
+ return MoveResponse(actions[0] if actions else None)
+
+ def close(self) -> None:
+ return None
+
+
+@pytest.fixture
+def test_settings(tmp_path: Path) -> Settings:
+ return Settings(
+ database_url=f"sqlite:///{(tmp_path / 'test.db').as_posix()}",
+ source_root=tmp_path / "sources",
+ temp_root=tmp_path / "tmp",
+ runner_image="test-runner",
+ )
diff --git a/Projects/3_Adversarial Search/tests/integration/test_api.py b/Projects/3_Adversarial Search/tests/integration/test_api.py
new file mode 100644
index 00000000..f7e89908
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/integration/test_api.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+import hashlib
+
+from agent_arena_api.main import create_app
+from agent_arena_api.models import Base
+from agent_arena_api.seed import seed_database
+from agent_arena_api.submissions import PreparedSubmission
+from agent_runner import DockerAgent
+from fastapi.testclient import TestClient
+from isolation_engine import BaselineController, play_game
+
+from tests.conftest import FirstLegalController
+
+SOURCE = """class CustomPlayer:
+ def get_action(self, state):
+ actions = state.actions()
+ return actions[0] if actions else None
+"""
+
+
+def fake_preparation(source: str, _settings: object) -> PreparedSubmission:
+ source_bytes = source.encode()
+ return PreparedSubmission(
+ source=source_bytes,
+ source_hash=hashlib.sha256(source_bytes).hexdigest(),
+ validation={
+ "accepted": True,
+ "static_contract": {"syntax_valid": True},
+ "sample_states": {"empty": {"legal_response": True}},
+ "scrimmage": {"completed_normally": True},
+ },
+ )
+
+
+def fake_practice(_source: str, baseline: str, seed: int, settings: object) -> dict:
+ result = play_game(
+ FirstLegalController(),
+ BaselineController(baseline, seed=seed),
+ time_limit_ms=settings.move_budget_ms,
+ )
+ return {
+ "status": "complete",
+ "result": {"end_status": result.end_status, "winner": "submitted"},
+ "replay": result.replay_dicts(),
+ "logs": [{"level": "success", "message": "Practice completed."}],
+ }
+
+
+def test_source_is_not_imported_by_fastapi(test_settings, monkeypatch, tmp_path) -> None:
+ app = create_app(test_settings)
+ Base.metadata.create_all(app.state.database.engine)
+ sentinel = tmp_path / "backend-imported-source.txt"
+ malicious = f"""open(r'{sentinel.as_posix()}', 'w').write('unsafe')
+class CustomPlayer:
+ def get_action(self, state):
+ return state.actions()[0] if state.actions() else None
+"""
+ monkeypatch.setattr(
+ DockerAgent,
+ "diagnostic",
+ classmethod(
+ lambda _cls, _image: {
+ "available": False,
+ "code": "test_unavailable",
+ "message": "Docker intentionally unavailable in this test.",
+ }
+ ),
+ )
+ with TestClient(app) as client:
+ response = client.post(
+ "/api/agents",
+ json={"owner_name": "oualid", "name": "Unsafe", "source": malicious},
+ )
+ assert response.status_code == 422
+ assert not sentinel.exists()
+
+
+def test_submit_practice_compete_leaderboard_and_replay(
+ test_settings,
+ monkeypatch,
+) -> None:
+ test_settings.minimum_ranked_fixtures = 4
+ app = create_app(test_settings)
+ Base.metadata.create_all(app.state.database.engine)
+ seed_database(app.state.database, test_settings)
+ monkeypatch.setattr("agent_arena_api.api.prepare_submission", fake_preparation)
+ monkeypatch.setattr("agent_arena_api.api.run_practice", fake_practice)
+ monkeypatch.setattr(
+ "agent_arena_api.competition.controller_for",
+ lambda _submission, _settings, seed: FirstLegalController(),
+ )
+
+ with TestClient(app) as client:
+ rules = client.get("/api/rules").json()
+ assert rules["move_budget_ms"] == 150
+ assert rules["minimum_ranked_fixtures"] == 4
+ initial_table = client.get("/api/leaderboard").json()
+ assert len(initial_table) == 4
+ assert all(row["qualified"] is False for row in initial_table)
+
+ created = client.post(
+ "/api/agents",
+ json={"owner_name": "oualid", "name": "Smoke Agent", "source": SOURCE},
+ )
+ assert created.status_code == 201
+ agent = created.json()
+ first_submission_id = agent["active_submission_id"]
+ assert agent["submissions"][0]["is_active"] is True
+ assert SOURCE not in created.text
+ assert "source_path" not in created.text
+
+ versioned = client.post(
+ f"/api/agents/{agent['id']}/submissions",
+ json={"source": f"{SOURCE}\n# immutable version 2\n"},
+ )
+ assert versioned.status_code == 201
+ agent = versioned.json()
+ submission_id = agent["active_submission_id"]
+ assert submission_id != first_submission_id
+ assert [item["number"] for item in agent["submissions"]] == [2, 1]
+ assert agent["submissions"][0]["is_active"] is True
+ assert agent["submissions"][1]["is_active"] is False
+
+ practice = client.post(
+ "/api/practice",
+ json={"source": SOURCE, "baseline": "greedy", "seed": 17},
+ )
+ assert practice.status_code == 200
+ assert practice.json()["status"] == "complete"
+ assert practice.json()["replay"]
+
+ round_result = client.post("/api/competition/run-round")
+ assert round_result.status_code == 200
+ fixture_ids = round_result.json()["fixture_ids"]
+ assert len(fixture_ids) == 10
+
+ fixture = client.get(f"/api/fixtures/{fixture_ids[0]}").json()
+ assert fixture["status"] == "complete"
+ assert fixture["points_a"] in {0, 1, 3}
+ assert len(fixture["games"]) == 2
+ assert all(game["replay"] for game in fixture["games"])
+
+ table = client.get("/api/leaderboard").json()
+ smoke = next(row for row in table if row["submission_id"] == submission_id)
+ assert smoke["fixtures"] == 4
+ assert smoke["points"] >= 0
+ assert smoke["qualified"] is True
+ assert smoke["rank"] is not None
+ assert smoke["points_per_fixture"] == smoke["points"] / smoke["fixtures"]
+
+ history = client.get(f"/api/agents/{agent['id']}/fixtures")
+ assert history.status_code == 200
+ assert len(history.json()) == 4
+ assert {item["submission_number"] for item in history.json()} == {2}
+ assert all(item["opponent_name"] for item in history.json())
+
+ featured = client.get("/api/fixtures/featured")
+ assert featured.status_code == 200
+ assert len(featured.json()["games"]) == 2
diff --git a/Projects/3_Adversarial Search/tests/integration/test_docker_smoke.py b/Projects/3_Adversarial Search/tests/integration/test_docker_smoke.py
new file mode 100644
index 00000000..c90eacd4
--- /dev/null
+++ b/Projects/3_Adversarial Search/tests/integration/test_docker_smoke.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+import pytest
+from agent_arena_api.config import Settings
+from agent_arena_api.submissions import prepare_submission
+from agent_runner import DockerAgent
+
+SOURCE = """class CustomPlayer:
+ def get_action(self, state):
+ actions = state.actions()
+ return actions[0] if actions else None
+"""
+
+
+@pytest.mark.docker
+def test_real_docker_submission_when_runner_is_available(tmp_path) -> None:
+ settings = Settings(
+ database_url=f"sqlite:///{(tmp_path / 'docker.db').as_posix()}",
+ source_root=tmp_path / "sources",
+ temp_root=tmp_path / "tmp",
+ )
+ diagnostic = DockerAgent.diagnostic(settings.runner_image)
+ if not diagnostic["available"]:
+ pytest.skip(diagnostic["message"])
+ prepared = prepare_submission(SOURCE, settings)
+ assert prepared.accepted, prepared.validation