nilshoehing commited on
Commit
67acd34
·
verified ·
1 Parent(s): 1f50695

Upload TopoBench Space app

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. .gitignore +28 -0
  3. Dockerfile +27 -0
  4. README.md +162 -5
  5. docker/Dockerfile +14 -0
  6. evals/prompts/bridges.txt +58 -0
  7. evals/prompts/bridges_intformat.txt +65 -0
  8. evals/prompts/bridges_intformat_json.txt +59 -0
  9. evals/prompts/flow_free.txt +53 -0
  10. evals/prompts/flow_free_intformat.txt +53 -0
  11. evals/prompts/flow_free_intformat_json.txt +43 -0
  12. evals/prompts/galaxies.txt +67 -0
  13. evals/prompts/galaxies_intformat.txt +69 -0
  14. evals/prompts/galaxies_intformat_json.txt +51 -0
  15. evals/prompts/loopy.txt +73 -0
  16. evals/prompts/loopy_intformat.txt +78 -0
  17. evals/prompts/loopy_intformat_json.txt +59 -0
  18. evals/prompts/pattern.txt +64 -0
  19. evals/prompts/pattern_intformat.txt +71 -0
  20. evals/prompts/pattern_intformat_json.txt +48 -0
  21. evals/prompts/undead.txt +61 -0
  22. evals/prompts/undead_intformat.txt +82 -0
  23. evals/prompts/undead_intformat_json.txt +68 -0
  24. evals/src/clients.py +275 -0
  25. evals/src/evals.py +195 -0
  26. evals/src/evals_verifier.py +348 -0
  27. evals/src/main.py +150 -0
  28. evals/src/puzzles.py +140 -0
  29. figure1.png +3 -0
  30. frontend/.gitignore +24 -0
  31. frontend/README.md +73 -0
  32. frontend/eslint.config.js +22 -0
  33. frontend/index.html +13 -0
  34. frontend/package-lock.json +2793 -0
  35. frontend/package.json +31 -0
  36. frontend/public/favicon.svg +1 -0
  37. frontend/public/icons.svg +24 -0
  38. frontend/src/App.tsx +17 -0
  39. frontend/src/assets/hero.png +0 -0
  40. frontend/src/assets/react.svg +1 -0
  41. frontend/src/assets/vite.svg +1 -0
  42. frontend/src/components/PuzzleEditor.tsx +866 -0
  43. frontend/src/index.css +652 -0
  44. frontend/src/lib/api.ts +84 -0
  45. frontend/src/main.tsx +11 -0
  46. frontend/src/routes/DonePage.tsx +64 -0
  47. frontend/src/routes/HomePage.tsx +124 -0
  48. frontend/src/routes/PlayPage.tsx +210 -0
  49. frontend/tsconfig.app.json +25 -0
  50. frontend/tsconfig.json +7 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ figure1.png filter=lfs diff=lfs merge=lfs -text
37
+ submodules/rlp/puzzles/puzzles.hlp filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ .hfvenv/
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ node_modules/
8
+ frontend/node_modules/
9
+ frontend/dist/
10
+ .npm-cache/
11
+ frontend/.npm-cache/
12
+ dist/
13
+ data/
14
+ results/
15
+ submodules/rlp/build/
16
+ submodules/rlp/rlp.egg-info/
17
+ submodules/rlp/rlp/constants*.so
18
+ submodules/rlp/rlp/lib/
19
+ submodules/rlp/rlp/lib/*.so
20
+ submodules/rlp/rlp/lib/CMakeCache.txt
21
+ submodules/rlp/rlp/lib/CMakeFiles/
22
+ submodules/rlp/rlp/lib/Makefile
23
+ submodules/rlp/rlp/lib/cmake_install.cmake
24
+ submodules/rlp/rlp/lib/**/CMakeFiles/
25
+ submodules/rlp/rlp/lib/**/Makefile
26
+ submodules/rlp/rlp/lib/**/cmake_install.cmake
27
+ submodules/flowfree/flowfree_all_solutions
28
+ uv.lock
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-bookworm-slim AS frontend-build
2
+
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package.json frontend/package-lock.json* ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
+
9
+ FROM python:3.12-slim-bookworm
10
+
11
+ WORKDIR /app
12
+
13
+ COPY . /app
14
+
15
+ ARG DEBIAN_MIRROR=https://ftp.us.debian.org/debian
16
+ ARG DEBIAN_SECURITY_MIRROR=https://security.debian.org/debian-security
17
+ ENV DEBIAN_MIRROR=${DEBIAN_MIRROR}
18
+ ENV DEBIAN_SECURITY_MIRROR=${DEBIAN_SECURITY_MIRROR}
19
+ ENV PYTHONUNBUFFERED=1
20
+ ENV TOPOBENCH_FRONTEND_DIST=/app/frontend/dist
21
+
22
+ RUN bash scripts/install.sh
23
+ COPY --from=frontend-build /app/frontend/dist /app/frontend/dist
24
+
25
+ EXPOSE 7860
26
+
27
+ CMD ["uvicorn", "space_app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,167 @@
1
  ---
2
- title: Testspace
3
- emoji: 🏢
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TopoBench Space
3
+ emoji: 🧩
4
+ colorFrom: green
5
+ colorTo: yellow
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # TopoBench
12
+
13
+ TopoBench now includes a Hugging Face Space app for playing, timing, and verifying
14
+ the benchmark puzzles in addition to the original benchmark runner.
15
+
16
+ ## Space App
17
+
18
+ The Space uses:
19
+
20
+ - `FastAPI` for the backend API and static asset serving
21
+ - `React + Vite` for the frontend
22
+ - `SQLite` under `/data/topobench.sqlite` for session storage
23
+ - The existing native puzzle verifiers from this repo for correctness checking
24
+
25
+ ### Local development
26
+
27
+ Backend dependencies:
28
+
29
+ ```bash
30
+ python3 -m pip install -e .
31
+ ```
32
+
33
+ Frontend dependencies:
34
+
35
+ ```bash
36
+ cd frontend
37
+ npm install
38
+ npm run build
39
+ ```
40
+
41
+ Run the app:
42
+
43
+ ```bash
44
+ uvicorn space_app.main:app --host 0.0.0.0 --port 7860
45
+ ```
46
+
47
+ Optional environment variables:
48
+
49
+ - `ADMIN_API_TOKEN`
50
+ - `TOPOBENCH_DATA_DIR`
51
+ - `TOPOBENCH_DATABASE_PATH`
52
+ - `HF_HOME`
53
+
54
+ Admin exports:
55
+
56
+ - `GET /api/admin/solves`
57
+ - `GET /api/admin/aggregates`
58
+
59
+ Both require `Authorization: Bearer <ADMIN_API_TOKEN>`.
60
+
61
+ ## Benchmark Runner
62
+
63
+ ![TopoBench](figure1.png)
64
+
65
+ This repo provides the code to run the main TopoBench benchmark with different input formats.
66
+
67
+ Dataset Links:
68
+
69
+ - [Plain](https://huggingface.co/datasets/topobench/topobench)
70
+ - [Intformat](https://huggingface.co/datasets/topobench/topobench_intformat)
71
+ - [Intformat_json](https://huggingface.co/datasets/topobench/topobench_intformat_json)
72
+
73
+ ## Setup (docker required)
74
+
75
+ Build the container:
76
+
77
+ ```bash
78
+ docker build -t topobench -f docker/Dockerfile . \
79
+ --build-arg DEBIAN_MIRROR=https://ftp.us.debian.org/debian \
80
+ --build-arg DEBIAN_SECURITY_MIRROR=https://security.debian.org/debian-security
81
+ ```
82
+
83
+ Troubleshooting Docker builds:
84
+
85
+ - If the build fails while fetching Debian packages, you might want to change the mirrors
86
+
87
+ Run it:
88
+
89
+ ```bash
90
+ docker run --rm -it \
91
+ -e OPENROUTER_KEY=your_key_here \
92
+ topobench \
93
+ python evals/src/main.py run-and-verify \
94
+ --provider openrouter \
95
+ --model inception/mercury-2 \
96
+ --variant intformat_json \
97
+ --difficulty easy \
98
+ --puzzle bridges \
99
+ --limit 1
100
+ ```
101
+
102
+ Options for keys (only set the keys you need for the provider you plan to use):
103
+
104
+ - `OPENAI_API_KEY`
105
+ - `OPENROUTER_API_KEY`
106
+ - `DEEPSEEK_API_KEY`
107
+ - `ANTHROPIC_API_KEY`
108
+ - `GOOGLE_API_KEY`
109
+
110
+ Run all six puzzles on the plain release:
111
+
112
+ ```bash
113
+ python evals/src/main.py run \
114
+ --provider openai \
115
+ --model gpt-5-mini \
116
+ --variant plain \
117
+ --difficulty all
118
+ --limit 50
119
+ ```
120
+
121
+ Run only bridges on intformat_json and immediately verify in one command:
122
+
123
+ ```bash
124
+ python evals/src/main.py run-and-verify \
125
+ --provider openrouter \
126
+ --model inception/mercury-2 \
127
+ --variant intformat_json \
128
+ --difficulty easy \
129
+ --puzzle bridges \
130
+ --limit 50
131
+ ```
132
+
133
+ Format options:
134
+
135
+ - plain
136
+ - intformat
137
+ - intformat_json
138
+
139
+ Puzzle options:
140
+
141
+ - bridges
142
+ - flow_free
143
+ - galaxies
144
+ - loopy
145
+ - pattern
146
+ - undead
147
+
148
+ ## Verify Existing Runs
149
+
150
+ Each run is saved under `results/runs/<run-name>/` with:
151
+
152
+ - `manifest.json`
153
+ - `responses.jsonl`
154
+
155
+ Verify a run and export CSV summaries:
156
+
157
+ ```bash
158
+ python evals/src/main.py verify \
159
+ --run-dir results/runs/<run-name>
160
+ ```
161
+
162
+ Verification writes:
163
+
164
+ - `results/reports/<run-name>_details.csv`
165
+ - `results/reports/<run-name>_summary.csv`
166
+
167
+ The verifier also prints a summary table to the terminal.
docker/Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim-bookworm
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app
6
+
7
+ ARG DEBIAN_MIRROR=https://ftp.us.debian.org/debian
8
+ ARG DEBIAN_SECURITY_MIRROR=https://security.debian.org/debian-security
9
+ ENV DEBIAN_MIRROR=${DEBIAN_MIRROR}
10
+ ENV DEBIAN_SECURITY_MIRROR=${DEBIAN_SECURITY_MIRROR}
11
+
12
+ RUN bash scripts/install.sh
13
+
14
+ CMD ["python", "/bin/bash", "--help"]
evals/prompts/bridges.txt ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Bridges (also known as Hashi or Hashiwokakero)
2
+
3
+ Solve the following Bridges puzzle by connecting islands with bridges. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ A grid where each cell is either:
8
+ - A number (1–8) representing an island with that many required bridges.
9
+ - A dot (.) representing an empty cell/water.
10
+
11
+ Provide the completed grid with bridges represented by:
12
+ - `-` for horizontal bridges.
13
+ - `|` for vertical bridges.
14
+ - `=` for horizontal double bridges.
15
+ - `"` for vertical double bridges.
16
+
17
+ Rules:
18
+
19
+ Connect all of the islands with bridges such that:
20
+ The number of bridges connected to each island matches the number on that island.
21
+ Bridges only run horizontally or vertically.
22
+ Bridges must not cross other bridges or islands.
23
+ A maximum of two bridges can connect any pair of islands.
24
+ All islands must be part of a single connected group.
25
+
26
+ Think step by step then output only the solved board in json format as shown below.
27
+
28
+ Output Format:
29
+
30
+ Return your final answer exactly like this:
31
+
32
+ ```json
33
+ {"response": "{final board state}"}
34
+ ```
35
+
36
+ Example Puzzle:
37
+
38
+ Input:
39
+
40
+ 3..1.
41
+ ..4.3
42
+ .....
43
+ .....
44
+ 4.4.1
45
+
46
+ Solution:
47
+
48
+ ```json
49
+ {"response": "
50
+ 3--1.
51
+ ".4=3
52
+ ".".|
53
+ ".".|
54
+ 4=4.1
55
+ "}
56
+ ```
57
+
58
+ Now solve this puzzle:
evals/prompts/bridges_intformat.txt ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Bridges (also known as Hashi or Hashiwokakero)
2
+
3
+ Solve the following Bridges puzzle by connecting islands with bridges. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
8
+
9
+ - 0 = space
10
+ - 2 = double vertical bridge
11
+ - 3 = single horizontal bridge
12
+ - 4 = water/empty
13
+ - 5 = 1 (island with value 1)
14
+ - 6 = 2 (island with value 2)
15
+ - 7 = 3 (island with value 3)
16
+ - 8 = 4 (island with value 4)
17
+ - 9 = 5 (island with value 5)
18
+ - A = 6 (island with value 6)
19
+ - B = 7 (island with value 7)
20
+ - C = 8 (island with value 8)
21
+ - D = double horizontal bridge
22
+ - E = single vertical bridge
23
+
24
+ Rules:
25
+
26
+ Connect all of the islands with bridges such that:
27
+ The number of bridges connected to each island matches the number on that island.
28
+ Bridges only run horizontally or vertically.
29
+ Bridges must not cross other bridges or islands.
30
+ A maximum of two bridges can connect any pair of islands.
31
+ All islands must be part of a single connected group.
32
+
33
+ Think step by step then output only the solved board in json format as shown below.
34
+
35
+ Output Format:
36
+
37
+ Return your final answer exactly like this:
38
+
39
+ ```json
40
+ {"response": "{final board state}"}
41
+ ```
42
+
43
+ Example Puzzle:
44
+
45
+ Input:
46
+
47
+ 7,4,4,4,7
48
+ 4,4,4,4,4
49
+ 4,5,4,4,7
50
+ 4,4,4,4,4
51
+ 8,4,8,4,6
52
+
53
+ Solution:
54
+
55
+ ```json
56
+ {"response": "
57
+ 7,3,3,3,7
58
+ 2,4,4,4,2
59
+ 2,5,3,3,7
60
+ 2,4,4,4,4
61
+ 8,D,8,D,6
62
+ "}
63
+ ```
64
+
65
+ Now solve this puzzle:
evals/prompts/bridges_intformat_json.txt ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```plaintext
2
+ Game: Bridges (also known as Hashi or Hashiwokakero)
3
+
4
+ Solve the following Bridges puzzle by connecting islands with bridges. You are given a 2D ASCII board representation in JSON array format.
5
+
6
+ Legend:
7
+
8
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
9
+
10
+ - "0" = space
11
+ - "2" = double vertical bridge
12
+ - "3" = single horizontal bridge
13
+ - "4" = water/empty
14
+ - "5" = 1 (island with value 1)
15
+ - "6" = 2 (island with value 2)
16
+ - "7" = 3 (island with value 3)
17
+ - "8" = 4 (island with value 4)
18
+ - "9" = 5 (island with value 5)
19
+ - "A" = 6 (island with value 6)
20
+ - "B" = 7 (island with value 7)
21
+ - "C" = 8 (island with value 8)
22
+ - "D" = double horizontal bridge
23
+ - "E" = single vertical bridge
24
+
25
+ Rules:
26
+
27
+ Connect all of the islands with bridges such that:
28
+ The number of bridges connected to each island matches the number on that island.
29
+ Bridges only run horizontally or vertically.
30
+ Bridges must not cross other bridges or islands.
31
+ A maximum of two bridges can connect any pair of islands.
32
+ All islands must be part of a single connected group.
33
+
34
+ Think step by step then output only the solved board in json format as shown below.
35
+
36
+ Output Format:
37
+
38
+ Return your final answer exactly like this:
39
+
40
+ ```json
41
+ {"response": "{final board state}"}
42
+ ```
43
+
44
+ Example Puzzle:
45
+
46
+ Input:
47
+
48
+ [["7", "4", "4", "4", "7"], ["4", "4", "4", "4", "4"], ["4", "5", "4", "4", "7"], ["4", "4", "4", "4", "4"], ["8", "4", "8", "4", "6"]]
49
+
50
+ Solution:
51
+
52
+ ```json
53
+ {"response": "
54
+ [["7", "3", "3", "3", "7"], ["2", "4", "4", "4", "2"], ["2", "5", "3", "3", "7"], ["2", "4", "4", "4", "4"], ["8", "D", "8", "D", "6"]]
55
+ "}
56
+ ```
57
+
58
+ Now solve this puzzle:
59
+ ```
evals/prompts/flow_free.txt ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Flow Free
2
+
3
+ Solve the following Flow Free puzzle by connecting the same letters. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ Letters (A-Z) = colored dots that need to be connected
8
+ . = empty space that can be filled with letters to create paths
9
+
10
+ Rules:
11
+
12
+ 1. You may only fill in dots (.) with letters that already exist on the board.
13
+ 2. You cannot modify or move any of the existing letters.
14
+ 3. Each pair of identical letters must be connected with a continuous, unbroken path of the same letter.
15
+ 4. Paths cannot cross or overlap each other.
16
+ 5. When solved, no dots should remain — the board must be completely filled with letters.
17
+ 6. A path cannot be adjacent to itself horizontally or vertically.
18
+
19
+ Think step by step then output only the solved board in json format as shown below.
20
+
21
+ Output Format:
22
+
23
+ Return your final answer exactly like this:
24
+
25
+ ```json
26
+ {"response": "{final board state}"}
27
+ ```
28
+
29
+ Example Puzzle:
30
+
31
+ Input:
32
+
33
+ BA...A
34
+ ..E...
35
+ ..D.F.
36
+ ..F..D
37
+ ..C.CE
38
+ B.....
39
+
40
+ Solution:
41
+
42
+ ```json
43
+ {"response": "
44
+ BAAAAA
45
+ BEEDDD
46
+ BEDDFD
47
+ BEFFFD
48
+ BECCCE
49
+ BEEEEE
50
+ "}
51
+ ```
52
+
53
+ Now solve this puzzle:
evals/prompts/flow_free_intformat.txt ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Flow Free
2
+
3
+ Solve the following Flow Free puzzle by connecting the same letters. You are given a 2D ASCII board representation in a tokenizer optimized integer format.
4
+
5
+ Legend:
6
+ Each board cell is delimited by a comma. Each new row is on a new line.
7
+ '2' = empty space that can be filled with letters to create paths
8
+ '3' to '9' and 'A' to 'C' = colored dots that need to be connected
9
+
10
+ Rules:
11
+
12
+ 1. You may only fill in empty space ('2') with colours that already exist on the board. (i.e., numbers/letters from '3' to '9' and 'A' to 'C' that are already present).
13
+ 2. You cannot modify or move any of the existing letters.
14
+ 3. Each pair of identical colours must be connected with a continuous, unbroken path of the same colour.
15
+ 4. Paths cannot cross or overlap each other.
16
+ 5. When solved, no empty spaces ('2') should remain — the board must be completely filled with colours ('3' to '9' and 'A' to 'C').
17
+ 6. A path cannot be adjacent to itself horizontally or vertically.
18
+
19
+ Think step by step then output only the solved board in json format as shown below.
20
+
21
+ Output Format:
22
+
23
+ Return your final answer exactly like this:
24
+
25
+ ```json
26
+ {"response": "{final board state}"}
27
+ ```
28
+
29
+ Example Puzzle:
30
+
31
+ Input:
32
+
33
+ 5,2,4,2,2,2
34
+ 3,2,2,2,2,2
35
+ 2,2,2,3,2,2
36
+ 2,6,2,2,2,2
37
+ 2,5,2,4,2,2
38
+ 2,2,2,2,2,6
39
+
40
+ Solution:
41
+
42
+ ```json
43
+ {"response": "
44
+ 5,5,4,4,4,4
45
+ 3,5,5,5,5,4
46
+ 3,3,3,3,5,4
47
+ 6,6,5,5,5,4
48
+ 6,5,5,4,4,4
49
+ 6,6,6,6,6,6
50
+ "}
51
+ ```
52
+
53
+ Now solve this puzzle:
evals/prompts/flow_free_intformat_json.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Flow Free
2
+
3
+ Solve the following Flow Free puzzle by connecting the same letters. You are given a 2D ASCII board representation in a tokenizer optimized integer format.
4
+
5
+ Legend:
6
+ Each board cell is delimited by a comma. Each new row is on a new line.
7
+ '2' = empty space that can be filled with letters to create paths
8
+ '3' to '9' and 'A' to 'C' = colored dots that need to be connected
9
+
10
+ Rules:
11
+
12
+ 1. You may only fill in empty space ('2') with colours that already exist on the board. (i.e., numbers/letters from '3' to '9' and 'A' to 'C' that are already present).
13
+ 2. You cannot modify or move any of the existing letters.
14
+ 3. Each pair of identical colours must be connected with a continuous, unbroken path of the same colour.
15
+ 4. Paths cannot cross or overlap each other.
16
+ 5. When solved, no empty spaces ('2') should remain — the board must be completely filled with colours ('3' to '9' and 'A' to 'C').
17
+ 6. A path cannot be adjacent to itself horizontally or vertically.
18
+
19
+ Think step by step then output only the solved board in json format as shown below.
20
+
21
+ Output Format:
22
+
23
+ Return your final answer exactly like this:
24
+
25
+ ```json
26
+ {"response": "{final board state}"}
27
+ ```
28
+
29
+ Example Puzzle:
30
+
31
+ Input:
32
+
33
+ [["2", "2", "2", "2", "2", "2", "2", "2"], ["4", "2", "2", "2", "2", "2", "2", "2"], ["2", "2", "2", "2", "2", "2", "2", "2"], ["2", "2", "2", "2", "3", "2", "8", "2"], ["8", "2", "3", "2", "2", "2", "2", "2"], ["7", "2", "9", "2", "2", "6", "2", "5"], ["2", "2", "2", "9", "6", "2", "2", "2"], ["2", "2", "2", "2", "7", "4", "2", "5"]]
34
+
35
+ Solution:
36
+
37
+ ```json
38
+ {"response": "
39
+ [["4", "4", "4", "4", "4", "4", "4", "4"], ["4", "8", "8", "8", "8", "8", "8", "4"], ["8", "8", "9", "9", "9", "9", "8", "4"], ["8", "9", "9", "3", "3", "9", "8", "4"], ["8", "9", "3", "3", "9", "9", "4", "4"], ["7", "9", "9", "9", "9", "6", "4", "5"], ["7", "9", "9", "9", "6", "6", "4", "5"], ["7", "7", "7", "7", "7", "4", "4", "5"]]
40
+ "}
41
+ ```
42
+
43
+ Now solve this puzzle:
evals/prompts/galaxies.txt ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Galaxies (also known as Spiral Galaxies / Tentai Show)
2
+
3
+ Solve the following Galaxies puzzle by partitioning the grid into rotationally symmetric regions. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ Initial grid:
8
+ o = dot
9
+ + = grid vertex
10
+ - = horizontal grid line
11
+ | = vertical grid line
12
+ space ( ) = empty square or non-existent grid line
13
+
14
+ Grid orientation: rows are written top-to-bottom; columns left-to-right. Each row is a string of characters; rows are separated by newline characters (\n).
15
+
16
+ Rules:
17
+
18
+ Partition the rectangular grid into connected regions of squares so that:
19
+ Every region is 180° rotationally symmetric.
20
+ Each region contains exactly one dot, and that dot is the region’s centre of symmetry. The dot may be on a square, on an edge between two squares, or at a vertex where four squares meet.
21
+ Regions are formed by drawing edges on grid lines; the puzzle is complete when the drawn edges separate every pair of squares that belong to different regions.
22
+ Do not modify or move any of the existing dots or grid lines and vertexes. Only add new hoizontal and veritcal edges to the grid lines.
23
+
24
+ Think step by step then output only the solved board in json format as shown below.
25
+
26
+ Output Format
27
+ Return your final answer exactly like this:
28
+
29
+ ```json
30
+ {"response": "{final board state}"}
31
+ ```
32
+
33
+ Example Puzzle:
34
+
35
+ Input:
36
+
37
+ +-+-+-+-+-+
38
+ |o o |
39
+ + + + + + +
40
+ | o |
41
+ + + + + + +
42
+ | o|
43
+ + + + + + +
44
+ | o |
45
+ +o+o+ + o +
46
+ | |
47
+ +-+-+-+-+-+
48
+
49
+ Solution:
50
+
51
+ ```json
52
+ {"response": "
53
+ +-+-+-+-+-+
54
+ |o| |o| |
55
+ +-+ +-+ + +
56
+ | o |
57
+ + + +-+ +-+
58
+ | | | |o|
59
+ +-+-+ +-+-+
60
+ | | |o| |
61
+ +o+o+ + o +
62
+ | | | | |
63
+ +-+-+-+-+-+
64
+ "}
65
+ ```
66
+
67
+ Now solve this puzzle:
evals/prompts/galaxies_intformat.txt ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Galaxies (also known as Spiral Galaxies / Tentai Show)
2
+
3
+ Solve the following Galaxies puzzle by partitioning the grid into rotationally symmetric regions. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
8
+
9
+ Initial grid:
10
+ - 0 = space (empty square or non-existent grid line)
11
+ - 2 = + (grid vertex)
12
+ - 3 = - (horizontal grid line)
13
+ - 4 = o (dot)
14
+ - 5 = | (vertical grid line)
15
+
16
+ Grid orientation: rows are written top-to-bottom; columns left-to-right. Each row is a string of characters; rows are separated by newline characters (\n).
17
+
18
+ Rules:
19
+
20
+ Partition the rectangular grid into connected regions of squares so that:
21
+ Every region is 180° rotationally symmetric.
22
+ Each region contains exactly one dot, and that dot is the region’s centre of symmetry. The dot may be on a square, on an edge between two squares, or at a vertex where four squares meet.
23
+ Regions are formed by drawing edges on grid lines; the puzzle is complete when the drawn edges separate every pair of squares that belong to different regions.
24
+ Do not modify or move any of the existing dots or grid lines and vertexes. Only add new hoizontal and veritcal edges to the grid lines.
25
+
26
+ Think step by step then output only the solved board in json format as shown below.
27
+
28
+ Output Format
29
+ Return your final answer exactly like this:
30
+
31
+ ```json
32
+ {"response": "{final board state}"}
33
+ ```
34
+
35
+ Example Puzzle:
36
+
37
+ Input:
38
+
39
+ 2,3,2,3,2,3,2,3,2,3,2
40
+ 5,0,0,0,0,0,0,0,4,0,5
41
+ 2,0,2,4,2,0,2,0,2,0,2
42
+ 5,0,0,0,0,0,0,0,0,4,5
43
+ 2,0,2,0,2,0,2,0,2,0,2
44
+ 5,0,0,0,0,0,0,4,0,0,5
45
+ 2,0,2,0,2,0,2,0,2,0,2
46
+ 5,0,0,4,0,0,0,0,0,0,5
47
+ 2,0,2,0,2,0,2,0,2,4,2
48
+ 5,4,0,0,0,0,0,4,0,0,5
49
+ 2,3,2,3,2,3,2,3,2,3,2
50
+
51
+ Solution:
52
+
53
+ ```json
54
+ {"response": "
55
+ 2,3,2,3,2,3,2,3,2,3,2
56
+ 5,0,0,0,0,0,5,0,4,0,5
57
+ 2,0,2,4,2,0,2,3,2,3,2
58
+ 5,0,0,0,0,0,5,0,5,4,5
59
+ 2,3,2,3,2,3,2,0,2,3,2
60
+ 5,0,0,0,5,0,0,4,0,0,5
61
+ 2,0,2,0,2,3,2,0,2,3,2
62
+ 5,0,0,4,0,0,5,0,5,0,5
63
+ 2,3,2,0,2,0,2,3,2,4,2
64
+ 5,4,5,0,0,0,5,4,5,0,5
65
+ 2,3,2,3,2,3,2,3,2,3,2
66
+ "}
67
+ ```
68
+
69
+ Now solve this puzzle:
evals/prompts/galaxies_intformat_json.txt ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```plaintext
2
+ Game: Galaxies (also known as Spiral Galaxies / Tentai Show)
3
+
4
+ Solve the following Galaxies puzzle by partitioning the grid into rotationally symmetric regions. You are given a 2D ASCII board representation in JSON array format.
5
+
6
+ Legend:
7
+
8
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
9
+
10
+ Initial grid:
11
+ - "0" = space (empty square or non-existent grid line)
12
+ - "2" = + (grid vertex)
13
+ - "3" = - (horizontal grid line)
14
+ - "4" = o (dot)
15
+ - "5" = | (vertical grid line)
16
+
17
+ Grid orientation: rows are written top-to-bottom; columns left-to-right.
18
+
19
+ Rules:
20
+
21
+ Partition the rectangular grid into connected regions of squares so that:
22
+ Every region is 180° rotationally symmetric.
23
+ Each region contains exactly one dot, and that dot is the region's centre of symmetry. The dot may be on a square, on an edge between two squares, or at a vertex where four squares meet.
24
+ Regions are formed by drawing edges on grid lines; the puzzle is complete when the drawn edges separate every pair of squares that belong to different regions.
25
+ Do not modify or move any of the existing dots or grid lines and vertexes. Only add new hoizontal and veritcal edges to the grid lines.
26
+
27
+ Think step by step then output only the solved board in json format as shown below.
28
+
29
+ Output Format
30
+ Return your final answer exactly like this:
31
+
32
+ ```json
33
+ {"response": "{final board state}"}
34
+ ```
35
+
36
+ Example Puzzle:
37
+
38
+ Input:
39
+
40
+ [["2", "3", "2", "3", "2", "3", "2", "3", "2", "3", "2"], ["5", "0", "0", "0", "0", "0", "0", "0", "4", "0", "5"], ["2", "0", "2", "4", "2", "0", "2", "0", "2", "0", "2"], ["5", "0", "0", "0", "0", "0", "0", "0", "0", "4", "5"], ["2", "0", "2", "0", "2", "0", "2", "0", "2", "0", "2"], ["5", "0", "0", "0", "0", "0", "0", "4", "0", "0", "5"], ["2", "0", "2", "0", "2", "0", "2", "0", "2", "0", "2"], ["5", "0", "0", "4", "0", "0", "0", "0", "0", "0", "5"], ["2", "0", "2", "0", "2", "0", "2", "0", "2", "4", "2"], ["5", "4", "0", "0", "0", "0", "0", "4", "0", "0", "5"], ["2", "3", "2", "3", "2", "3", "2", "3", "2", "3", "2"]]
41
+
42
+ Solution:
43
+
44
+ ```json
45
+ {"response": "
46
+ [["2", "3", "2", "3", "2", "3", "2", "3", "2", "3", "2"], ["5", "0", "0", "0", "0", "0", "5", "0", "4", "0", "5"], ["2", "0", "2", "4", "2", "0", "2", "3", "2", "3", "2"], ["5", "0", "0", "0", "0", "0", "5", "0", "5", "4", "5"], ["2", "3", "2", "3", "2", "3", "2", "0", "2", "3", "2"], ["5", "0", "0", "0", "5", "0", "0", "4", "0", "0", "5"], ["2", "0", "2", "0", "2", "3", "2", "0", "2", "3", "2"], ["5", "0", "0", "4", "0", "0", "5", "0", "5", "0", "5"], ["2", "3", "2", "0", "2", "0", "2", "3", "2", "4", "2"], ["5", "4", "5", "0", "0", "0", "5", "4", "5", "0", "5"], ["2", "3", "2", "3", "2", "3", "2", "3", "2", "3", "2"]]
47
+ "}
48
+ ```
49
+
50
+ Now solve this puzzle:
51
+ ```
evals/prompts/loopy.txt ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Loopy (Slitherlink)
2
+
3
+ Solve the following Loopy (Slitherlink) puzzle given as a 2D ASCII grid.
4
+
5
+ Legend:
6
+
7
+ You are given numbers arranged in a grid that represent the number of loop edges that should be adjacent to their cell. Use the following symbols to solve the puzzle:
8
+ x for no edge in that position
9
+ - and | for horizontal and vertical edges
10
+ space ( ) for empty cells where no number is given
11
+
12
+ Rules:
13
+
14
+ Draw a single continuous loop using some subset of the possible edges.
15
+ The loop must:
16
+ Pass along - and | positions only.
17
+ Satisfy all cell clues.
18
+ Have no branches (each vertex used by the loop has degree 2).
19
+ In the solution:
20
+ Use - and | to show edges that are part of the loop and x to show edges that are not part of the loop.
21
+ The structure of the puzzle is as follows: empty row, row with clues, empty row, row with clues, ... ending with an empty row.
22
+ The available positions for horizontal edges (-) are in the empty rows in every second column (the columns where clues can appear).
23
+ The available positions for vertical edges (|) are in the rows with clues in the first, third, fifth, etc. columns (the columns where no clues appear).
24
+ In each of the possible positions for edges, you must determine whether to place an edge (- or |) or to mark it as not part of the loop (x).
25
+
26
+ Stay within the grid boundaries denoted as +. Do not modify the grid boundaries.
27
+
28
+ Think step by step then output only the solved board in json format as shown below.
29
+
30
+ Output Format
31
+ Return your final answer exactly like this:
32
+
33
+ ```json
34
+ {"response": "{final board state}"}
35
+ ```
36
+
37
+ Example Puzzle:
38
+
39
+ Input:
40
+
41
+ +++++++++++++
42
+ + +
43
+ + 2 1 3 +
44
+ + +
45
+ + 2 1 2 +
46
+ + +
47
+ + 3 +
48
+ + +
49
+ + 0 1 3 +
50
+ + +
51
+ + 3 +
52
+ + +
53
+ +++++++++++++
54
+
55
+ Solution:
56
+
57
+ ```json
58
+ {"response": "
59
+ +++++++++++++
60
+ + - - - x - +
61
+ +|2x1x |3| |+
62
+ + x x x - x +
63
+ +|2x x1x x2|+
64
+ + - x - - - +
65
+ +x3| | x x x+
66
+ + - x - - - +
67
+ +| x0x x1x3|+
68
+ + - x - x - +
69
+ +x |3| | | x+
70
+ + x - x - x +
71
+ +++++++++++++"}```
72
+
73
+ Now solve this puzzle:
evals/prompts/loopy_intformat.txt ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Loopy (Slitherlink)
2
+
3
+ Solve the following Loopy (Slitherlink) puzzle given as a 2D ASCII grid.
4
+
5
+ Legend:
6
+
7
+ The puzzle grid is encoded using integers 0-8 where each character maps to a specific symbol:
8
+
9
+ - 0 = space
10
+ - 2 = horizontal edge
11
+ - 3 = 0 (digit)
12
+ - 4 = 1 (digit)
13
+ - 5 = 2 (digit)
14
+ - 6 = 3 (digit)
15
+ - 7 = x (edge not in loop)
16
+ - 8 = vertical edge
17
+
18
+ You are given numbers arranged in a grid that represent the number of loop edges that should be adjacent to their cell. Use the following symbols to solve the puzzle:
19
+ 7 for no edge in that position
20
+ 2 and 8 for horizontal and vertical edges
21
+ 0 for empty cells where no number is given
22
+
23
+ Rules:
24
+
25
+ Draw a single continuous loop using some subset of the possible edges.
26
+ The loop must:
27
+ Pass along 2 and 8 positions only.
28
+ Satisfy all cell clues.
29
+ Have no branches (each vertex used by the loop has degree 2).
30
+ In the solution:
31
+ Use 2 and 8 to show edges that are part of the loop and 7 to show edges that are not part of the loop.
32
+ The structure of the puzzle is as follows: empty row, row with clues, empty row, row with clues, ... ending with an empty row.
33
+ The available positions for horizontal edges (2) are in the empty rows in every second column (the columns where clues can appear).
34
+ The available positions for vertical edges (8) are in the rows with clues in the first, third, fifth, etc. columns (the columns where no clues appear).
35
+ In each of the possible positions for edges, you must determine whether to place an edge (2 or 8) or to mark it as not part of the loop (7).
36
+
37
+ Think step by step then output only the solved board in json format as shown below.
38
+
39
+ Output Format
40
+ Return your final answer exactly like this:
41
+
42
+ ```json
43
+ {"response": "{final board state}"}
44
+ ```
45
+
46
+ Example Puzzle:
47
+
48
+ Input:
49
+
50
+ 0,0,0,0,0,0,0,0,0,0,0
51
+ 0,5,0,4,0,0,0,6,0,0,0
52
+ 0,0,0,0,0,0,0,0,0,0,0
53
+ 0,5,0,0,0,4,0,0,0,5,0
54
+ 0,0,0,0,0,0,0,0,0,0,0
55
+ 0,6,0,0,0,0,0,0,0,0,0
56
+ 0,0,0,0,0,0,0,0,0,0,0
57
+ 0,0,0,3,0,0,0,4,0,6,0
58
+ 0,0,0,0,0,0,0,0,0,0,0
59
+ 0,0,0,6,0,0,0,0,0,0,0
60
+
61
+ Solution:
62
+
63
+ ```json
64
+ {"response": "
65
+ 0,2,0,2,0,2,0,7,0,2,0
66
+ 8,5,7,4,7,0,8,6,8,0,8
67
+ 0,7,0,7,0,7,0,2,0,7,0
68
+ 8,5,7,0,7,4,7,0,7,5,8
69
+ 0,2,0,7,0,2,0,2,0,2,0
70
+ 7,6,8,0,8,0,7,0,7,0,7
71
+ 0,2,0,7,0,2,0,2,0,2,0
72
+ 8,0,7,3,7,0,7,4,7,6,8
73
+ 0,2,0,7,0,2,0,7,0,2,0
74
+ 7,0,8,6,8,0,8,0,8,0,7
75
+ 0,7,0,2,0,7,0,2,0,7,0
76
+ "}```
77
+
78
+ Now solve this puzzle:
evals/prompts/loopy_intformat_json.txt ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Loopy (Slitherlink)
2
+
3
+ Solve the following Loopy (Slitherlink) puzzle given as a 2D ASCII grid in JSON array format.
4
+
5
+ Legend:
6
+
7
+ The puzzle grid is encoded using integers 0-8 where each character maps to a specific symbol:
8
+
9
+ - "0" = space
10
+ - "2" = horizontal edge
11
+ - "3" = 0 (digit)
12
+ - "4" = 1 (digit)
13
+ - "5" = 2 (digit)
14
+ - "6" = 3 (digit)
15
+ - "7" = x (edge not in loop)
16
+ - "8" = vertical edge
17
+
18
+ You are given numbers arranged in a grid that represent the number of loop edges that should be adjacent to their cell. Use the following symbols to solve the puzzle:
19
+ "7" for no edge in that position
20
+ "2" and "8" for horizontal and vertical edges
21
+ "0" for empty cells where no number is given
22
+
23
+ Rules:
24
+
25
+ Draw a single continuous loop using some subset of the possible edges.
26
+ The loop must:
27
+ Pass along "2" and "8" positions only.
28
+ Satisfy all cell clues.
29
+ Have no branches (each vertex used by the loop has degree 2).
30
+ In the solution:
31
+ Use "2" and "8" to show edges that are part of the loop and "7" to show edges that are not part of the loop.
32
+ The structure of the puzzle is as follows: empty row, row with clues, empty row, row with clues, ... ending with an empty row.
33
+ The available positions for horizontal edges ("2") are in the empty rows in every second column (the columns where clues can appear).
34
+ The available positions for vertical edges ("8") are in the rows with clues in the first, third, fifth, etc. columns (the columns where no clues appear).
35
+ In each of the possible positions for edges, you must determine whether to place an edge ("2" or "8") or to mark it as not part of the loop ("7").
36
+
37
+ Think step by step then output only the solved board in json format as shown below.
38
+
39
+ Output Format
40
+ Return your final answer exactly like this:
41
+
42
+ ```json
43
+ {"response": "{final board state}"}
44
+ ```
45
+
46
+ Example Puzzle:
47
+
48
+ Input:
49
+
50
+ [["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "5", "0", "4", "0", "0", "0", "6", "0", "0", "0"], ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "5", "0", "0", "0", "4", "0", "0", "0", "5", "0"], ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "6", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "0", "0", "3", "0", "0", "0", "4", "0", "6", "0"], ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "0", "0", "6", "0", "0", "0", "0", "0", "0", "0"]]
51
+
52
+ Solution:
53
+
54
+ ```json
55
+ {"response": "
56
+ [["0", "2", "0", "2", "0", "2", "0", "7", "0", "2", "0"], ["8", "5", "7", "4", "7", "0", "8", "6", "8", "0", "8"], ["0", "7", "0", "7", "0", "7", "0", "2", "0", "7", "0"], ["8", "5", "7", "0", "7", "4", "7", "0", "7", "5", "8"], ["0", "2", "0", "7", "0", "2", "0", "2", "0", "2", "0"], ["7", "6", "8", "0", "8", "0", "7", "0", "7", "0", "7"], ["0", "2", "0", "7", "0", "2", "0", "2", "0", "2", "0"], ["8", "0", "7", "3", "7", "0", "7", "4", "7", "6", "8"], ["0", "2", "0", "7", "0", "2", "0", "7", "0", "2", "0"], ["7", "0", "8", "6", "8", "0", "8", "0", "8", "0", "7"], ["0", "7", "0", "2", "0", "7", "0", "2", "0", "7", "0"]]
57
+ "}```
58
+
59
+ Now solve this puzzle:
evals/prompts/pattern.txt ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Pattern
2
+
3
+ Solve the following Pattern puzzle. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+ -"+" for vertices
7
+ -"-" for horizontal edges
8
+ -"|" for vertical edges
9
+ -Spaces for empty cells
10
+ -Digits on top and left sides of the grid (called "clues") indicating how many cells in that row or column should form a connected pattern.
11
+
12
+
13
+
14
+ Rules:
15
+ Patterns are formed by placing "##" into cells that should be filled and ".." into cells that should stay empty. For example when there are two clues of "3" in a row, it means that there should be two separate groups of three connected ## cells in that row.
16
+ Only place ## and .. in empty cells (spaces). Do not modify or move any of the existing +, -, |, or numbers. Each row and column must contain exactly the number of connected ## groups as indicated by the clues. The pattern must be continuous (all ## cells must be connected horizontally or vertically).
17
+
18
+ Think step by step then output only the solved board in json format as shown below.
19
+
20
+ Output Format:
21
+
22
+ Return your final answer exactly like this:
23
+
24
+ ```json
25
+ {"response": "{final board state}"}
26
+ ```
27
+
28
+ Example Puzzle:
29
+
30
+ Input:
31
+
32
+ 2 1 4 2 4
33
+ +--+--+--+--+--+
34
+ 3| | | | | |
35
+ +--+--+--+--+--+
36
+ 3| | | | | |
37
+ +--+--+--+--+--+
38
+ 1 1| | | | | |
39
+ +--+--+--+--+--+
40
+ 1 1 1| | | | | |
41
+ +--+--+--+--+--+
42
+ 2| | | | | |
43
+ +--+--+--+--+--+
44
+
45
+ Solution:
46
+
47
+ ```json
48
+ {"response": "
49
+ 2 1 4 2 4
50
+ +--+--+--+--+--+
51
+ 3|..|..|##|##|##|
52
+ +--+--+--+--+--+
53
+ 3|..|..|##|##|##|
54
+ +--+--+--+--+--+
55
+ 1 1|..|..|##|..|##|
56
+ +--+--+--+--+--+
57
+ 1 1 1|##|..|##|..|##|
58
+ +--+--+--+--+--+
59
+ 2|##|##|..|..|..|
60
+ +--+--+--+--+--+
61
+ "}```
62
+
63
+ Now solve this puzzle:
64
+
evals/prompts/pattern_intformat.txt ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Pattern
2
+
3
+ Solve the following Pattern puzzle. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
7
+
8
+ - 0 = space
9
+ - A = . (empty cell)
10
+ - B = # (filled cell)
11
+ - X = + (vertex)
12
+ - Y = - (horizontal edge)
13
+ - Z = | (vertical edge)
14
+ - 1-9 = digits 1-9 (clues)
15
+
16
+ -Digits on top and left sides of the grid (called "clues") indicating how many cells in that row or column should form a connected pattern.
17
+
18
+
19
+ Rules:
20
+ Patterns are formed by placing B into cells that should be filled and A into cells that should stay empty. For example when there are two clues of "3" in a row, it means that there should be two separate groups of three connected B cells in that row.
21
+ Only place B and A in empty cells (zeros). Do not modify or move any of the existing X, Y, Z, or numbers. Each row and column must contain exactly the number of connected B groups as indicated by the clues. The pattern must be continuous (all B cells must be connected horizontally or vertically).
22
+
23
+ Think step by step then output only the solved board in json format as shown below.
24
+
25
+ Output Format:
26
+
27
+ Return your final answer exactly like this:
28
+
29
+ ```json
30
+ {"response": "{final board state}"}
31
+ ```
32
+
33
+ Example Puzzle:
34
+
35
+ Input:
36
+
37
+ 0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0
38
+ 0,0,0,0,0,2,0,0,3,0,0,1,0,0,3,0,0,3,0
39
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
40
+ 1,0,2,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z
41
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
42
+ 0,0,2,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z
43
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
44
+ 1,0,2,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z
45
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
46
+ 0,0,2,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z
47
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
48
+ 0,0,3,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z,0,0,Z
49
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
50
+
51
+ Solution:
52
+
53
+ ```json
54
+ {"response": "
55
+ 0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0
56
+ 0,0,0,0,0,2,0,0,3,0,0,1,0,0,3,0,0,3,0
57
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
58
+ 1,0,2,Z,A,A,Z,B,B,Z,A,A,Z,B,B,Z,B,B,Z
59
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
60
+ 0,0,2,Z,A,A,Z,A,A,Z,A,A,Z,B,B,Z,B,B,Z
61
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
62
+ 1,0,2,Z,A,A,Z,B,B,Z,A,A,Z,B,B,Z,B,B,Z
63
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
64
+ 0,0,2,Z,B,B,Z,B,B,Z,A,A,Z,A,A,Z,A,A,Z
65
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
66
+ 0,0,3,Z,B,B,Z,B,B,Z,B,B,Z,A,A,Z,A,A,Z
67
+ 0,0,0,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X,Y,Y,X
68
+ "}```
69
+
70
+ Now solve this puzzle:
71
+
evals/prompts/pattern_intformat_json.txt ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```plaintext
2
+ Game: Pattern
3
+
4
+ Solve the following Pattern puzzle. You are given a 2D ASCII board representation in JSON array format.
5
+
6
+ Legend:
7
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
8
+
9
+ - "0" = space
10
+ - "A" = . (empty cell)
11
+ - "B" = # (filled cell)
12
+ - "X" = + (vertex)
13
+ - "Y" = - (horizontal edge)
14
+ - "Z" = | (vertical edge)
15
+ - "1"-"9" = digits 1-9 (clues)
16
+
17
+ -Digits on top and left sides of the grid (called "clues") indicating how many cells in that row or column should form a connected pattern.
18
+
19
+
20
+ Rules:
21
+ Patterns are formed by placing B into cells that should be filled and A into cells that should stay empty. For example when there are two clues of "3" in a row, it means that there should be two separate groups of three connected B cells in that row.
22
+ Only place B and A in empty cells (zeros). Do not modify or move any of the existing X, Y, Z, or numbers. Each row and column must contain exactly the number of connected B groups as indicated by the clues. The pattern must be continuous (all B cells must be connected horizontally or vertically).
23
+
24
+ Think step by step then output only the solved board in json format as shown below.
25
+
26
+ Output Format:
27
+
28
+ Return your final answer exactly like this:
29
+
30
+ ```json
31
+ {"response": "{final board state}"}
32
+ ```
33
+
34
+ Example Puzzle:
35
+
36
+ Input:
37
+
38
+ [["0", "0", "0", "0", "0", "0", "0", "0", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "0", "0", "0", "0", "2", "0", "0", "3", "0", "0", "1", "0", "0", "3", "0", "0", "3", "0"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["1", "0", "2", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "2", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["1", "0", "2", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "2", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "3", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z", "0", "0", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"]]
39
+
40
+ Solution:
41
+
42
+ ```json
43
+ {"response": "
44
+ [["0", "0", "0", "0", "0", "0", "0", "0", "1", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], ["0", "0", "0", "0", "0", "2", "0", "0", "3", "0", "0", "1", "0", "0", "3", "0", "0", "3", "0"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["1", "0", "2", "Z", "A", "A", "Z", "B", "B", "Z", "A", "A", "Z", "B", "B", "Z", "B", "B", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "2", "Z", "A", "A", "Z", "A", "A", "Z", "A", "A", "Z", "B", "B", "Z", "B", "B", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["1", "0", "2", "Z", "A", "A", "Z", "B", "B", "Z", "A", "A", "Z", "B", "B", "Z", "B", "B", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "2", "Z", "B", "B", "Z", "B", "B", "Z", "A", "A", "Z", "A", "A", "Z", "A", "A", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"], ["0", "0", "3", "Z", "B", "B", "Z", "B", "B", "Z", "B", "B", "Z", "A", "A", "Z", "A", "A", "Z"], ["0", "0", "0", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X", "Y", "Y", "X"]]
45
+ "}```
46
+
47
+ Now solve this puzzle:
48
+ ```
evals/prompts/undead.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Undead
2
+
3
+ Solve the following Undead puzzle. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ - '.' for empty squares.
8
+ - '/' or '\' for diagonal mirrors that reflect monster visibility diagonally.
9
+ - Edge clues (numbers) indicating visible monsters from that direction.
10
+ - Fill the grid with letters: G, V, or Z.
11
+
12
+ Rules:
13
+
14
+ 1. The puzzle is a grid of squares. Some squares contain diagonal mirrors ('/' or '\') which cannot hold monsters.
15
+ 2. All non-mirror squares must be filled with exactly one monster:
16
+ - Ghost (G): visible only in mirrors.
17
+ - Vampire (V): visible only directly.
18
+ - Zombie (Z): visible in both direct view and mirrors.
19
+ 3. Total counts of each monster type are provided.
20
+ 4. Numbers around the edges of the grid indicate how many monsters are visible along that row or column from that position, counting diagonal reflections.
21
+ 5. If a reflected line of sight crosses the same monster multiple times, count each occurrence.
22
+ 6. Mirrors reflect light in both directions.
23
+
24
+ Think step by step then output only the solved board in json format as shown below.
25
+
26
+ Output Format:
27
+
28
+ Return your final answer exactly like this:
29
+
30
+ ```json
31
+ {"response": "{final board state}"}
32
+ ```
33
+
34
+ Example Puzzle:
35
+
36
+ Input:
37
+
38
+ G: 2 V: 6 Z: 0
39
+
40
+ 2 0 0 0
41
+ 2 . \ \ / 0
42
+ 3 . . . . 3
43
+ 0 \ \ \ \ 0
44
+ 1 . \ . . 2
45
+ 1 0 0 1
46
+
47
+ Solution:
48
+
49
+ ```json
50
+ {"response": "
51
+ G: 2 V: 6 Z: 0
52
+
53
+ 2 0 0 0
54
+ 2 V \ \ / 0
55
+ 3 G V V V 3
56
+ 0 \ \ \ \ 0
57
+ 1 V \ G V 2
58
+ 1 0 0 1
59
+ "}```
60
+
61
+ Now solve this puzzle:
evals/prompts/undead_intformat.txt ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Game: Undead
2
+
3
+ Solve the following Undead puzzle. You are given a 2D ASCII board representation.
4
+
5
+ Legend:
6
+
7
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
8
+
9
+ - 0 = space
10
+ - 2 = . (empty square)
11
+ - 3 = / (diagonal mirror)
12
+ - 4 = 0 (digit)
13
+ - 5 = 1 (digit)
14
+ - 6 = 2 (digit)
15
+ - 7 = 3 (digit)
16
+ - 8 = 4 (digit)
17
+ - 9 = 5 (digit)
18
+ - A = 6 (digit)
19
+ - B = 7 (digit)
20
+ - C = 8 (digit)
21
+ - D = 9 (digit)
22
+ - E = : (colon)
23
+ - F = Ghost
24
+ - G = Vampire
25
+ - H = Zombie
26
+ - I = \ (backslash diagonal mirror)
27
+
28
+ - 4-9, A-D are edge clues (digits) indicating visible monsters from that direction.
29
+ - Fill the grid with letters: Ghost (F), Vampire (G), or Zombie (H).
30
+
31
+ Rules:
32
+
33
+ 1. The puzzle is a grid of squares. Some squares contain diagonal mirrors (3 or I) which cannot hold monsters.
34
+ 2. All non-mirror squares must be filled with exactly one monster:
35
+ - Ghost (F): visible only in mirrors.
36
+ - Vampire (G): visible only directly.
37
+ - Zombie (H): visible in both direct view and mirrors.
38
+ 3. Total counts of each monster type are provided in the first line of the puzzle.
39
+ 4. Numbers around the edges of the grid indicate how many monsters are visible along that row or column from that position, counting diagonal reflections.
40
+ 5. If a reflected line of sight crosses the same monster multiple times, count each occurrence.
41
+ 6. Mirrors reflect light in both directions.
42
+
43
+ Think step by step then output only the solved board in json format as shown below.
44
+
45
+ Output Format:
46
+
47
+ Return your final answer exactly like this:
48
+
49
+ ```json
50
+ {"response": "{final board state}"}
51
+ ```
52
+
53
+ Example Puzzle:
54
+
55
+ Input:
56
+
57
+ F,E,0,8,0,G,E,0,8,0,H,E,0,9
58
+
59
+ 0,0,0,4,0,6,0,8,0,7,0,4,0,0
60
+ 0,4,0,3,0,2,0,2,0,3,0,I,0,4
61
+ 0,6,0,2,0,2,0,2,0,I,0,2,0,6
62
+ 0,6,0,2,0,I,0,3,0,3,0,I,0,6
63
+ 0,6,0,2,0,I,0,I,0,3,0,2,0,6
64
+ 0,7,0,3,0,2,0,2,0,2,0,2,0,6
65
+ 0,0,0,7,0,4,0,6,0,4,0,9,0,0
66
+
67
+ Solution:
68
+
69
+ ```json
70
+ {"response": "
71
+ F,E,0,8,0,G,E,0,8,0,H,E,0,9
72
+
73
+ 0,0,0,4,0,6,0,8,0,7,0,4,0,0
74
+ 0,4,0,3,0,F,0,G,0,3,0,I,0,4
75
+ 0,6,0,F,0,H,0,H,0,I,0,H,0,6
76
+ 0,6,0,H,0,I,0,3,0,3,0,I,0,6
77
+ 0,6,0,G,0,I,0,I,0,3,0,G,0,6
78
+ 0,7,0,3,0,F,0,H,0,F,0,G,0,6
79
+ 0,0,0,7,0,4,0,6,0,4,0,9,0,0
80
+ "}```
81
+
82
+ Now solve this puzzle:
evals/prompts/undead_intformat_json.txt ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```plaintext
2
+ Game: Undead
3
+
4
+ Solve the following Undead puzzle. You are given a 2D ASCII board representation in JSON array format.
5
+
6
+ Legend:
7
+
8
+ The puzzle grid is encoded using integers and letters (0-9, A-Z) where each character maps to a specific symbol:
9
+
10
+ - "0" = space
11
+ - "2" = . (empty square)
12
+ - "3" = / (diagonal mirror)
13
+ - "4" = 0 (digit)
14
+ - "5" = 1 (digit)
15
+ - "6" = 2 (digit)
16
+ - "7" = 3 (digit)
17
+ - "8" = 4 (digit)
18
+ - "9" = 5 (digit)
19
+ - "A" = 6 (digit)
20
+ - "B" = 7 (digit)
21
+ - "C" = 8 (digit)
22
+ - "D" = 9 (digit)
23
+ - "E" = : (colon)
24
+ - "F" = Ghost
25
+ - "G" = Vampire
26
+ - "H" = Zombie
27
+ - "I" = \ (backslash diagonal mirror)
28
+
29
+ - 4-9, A-D are edge clues (digits) indicating visible monsters from that direction.
30
+ - Fill the grid with letters: Ghost (F), Vampire (G), or Zombie (H).
31
+
32
+ Rules:
33
+
34
+ 1. The puzzle is a grid of squares. Some squares contain diagonal mirrors (3 or I) which cannot hold monsters.
35
+ 2. All non-mirror squares must be filled with exactly one monster:
36
+ - Ghost (F): visible only in mirrors.
37
+ - Vampire (G): visible only directly.
38
+ - Zombie (H): visible in both direct view and mirrors.
39
+ 3. Total counts of each monster type are provided in the first line of the puzzle.
40
+ 4. Numbers around the edges of the grid indicate how many monsters are visible along that row or column from that position, counting diagonal reflections.
41
+ 5. If a reflected line of sight crosses the same monster multiple times, count each occurrence.
42
+ 6. Mirrors reflect light in both directions.
43
+
44
+ Think step by step then output only the solved board in json format as shown below.
45
+
46
+ Output Format:
47
+
48
+ Return your final answer exactly like this:
49
+
50
+ ```json
51
+ {"response": "{final board state}"}
52
+ ```
53
+
54
+ Example Puzzle:
55
+
56
+ Input:
57
+
58
+ [["F", "E", "0", "8", "0", "G", "E", "0", "8", "0", "H", "E", "0", "9"], ["0", "0", "0", "4", "0", "6", "0", "8", "0", "7", "0", "4", "0", "0"], ["0", "4", "0", "3", "0", "2", "0", "2", "0", "3", "0", "I", "0", "4"], ["0", "6", "0", "2", "0", "2", "0", "2", "0", "I", "0", "2", "0", "6"], ["0", "6", "0", "2", "0", "I", "0", "3", "0", "3", "0", "I", "0", "6"], ["0", "6", "0", "2", "0", "I", "0", "I", "0", "3", "0", "2", "0", "6"], ["0", "7", "0", "3", "0", "2", "0", "2", "0", "2", "0", "2", "0", "6"], ["0", "0", "0", "7", "0", "4", "0", "6", "0", "4", "0", "9", "0", "0"]]
59
+
60
+ Solution:
61
+
62
+ ```json
63
+ {"response": "
64
+ [["F", "E", "0", "8", "0", "G", "E", "0", "8", "0", "H", "E", "0", "9"], ["0", "0", "0", "4", "0", "6", "0", "8", "0", "7", "0", "4", "0", "0"], ["0", "4", "0", "3", "0", "F", "0", "G", "0", "3", "0", "I", "0", "4"], ["0", "6", "0", "F", "0", "H", "0", "H", "0", "I", "0", "H", "0", "6"], ["0", "6", "0", "H", "0", "I", "0", "3", "0", "3", "0", "I", "0", "6"], ["0", "6", "0", "G", "0", "I", "0", "I", "0", "3", "0", "G", "0", "6"], ["0", "7", "0", "3", "0", "F", "0", "H", "0", "F", "0", "G", "0", "6"], ["0", "0", "0", "7", "0", "4", "0", "6", "0", "4", "0", "9", "0", "0"]]
65
+ "}```
66
+
67
+ Now solve this puzzle:
68
+ ```
evals/src/clients.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from dotenv import load_dotenv
9
+
10
+
11
+ load_dotenv()
12
+
13
+
14
+ def _parse_extra_arg(value: str) -> Any:
15
+ """Parse CLI request args while allowing plain strings."""
16
+ try:
17
+ return json.loads(value)
18
+ except json.JSONDecodeError:
19
+ return value
20
+
21
+
22
+ def parse_request_args(pairs: list[str] | None) -> dict[str, Any]:
23
+ parsed: dict[str, Any] = {}
24
+ for pair in pairs or []:
25
+ if "=" not in pair:
26
+ raise ValueError(
27
+ f"Invalid --request-arg '{pair}'. Expected KEY=VALUE."
28
+ )
29
+ key, raw_value = pair.split("=", 1)
30
+ key = key.strip()
31
+ if not key:
32
+ raise ValueError(f"Invalid --request-arg '{pair}'. Empty key.")
33
+ parsed[key] = _parse_extra_arg(raw_value.strip())
34
+ return parsed
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class GenerationResult:
39
+ text: str
40
+ input_tokens: int | None = None
41
+ output_tokens: int | None = None
42
+ total_tokens: int | None = None
43
+ raw: dict[str, Any] | None = None
44
+
45
+
46
+ class ModelClient:
47
+ def generate(
48
+ self,
49
+ provider: str,
50
+ model: str,
51
+ prompt: str,
52
+ *,
53
+ max_output_tokens: int,
54
+ temperature: float | None,
55
+ extra_args: dict[str, Any] | None = None,
56
+ ) -> GenerationResult:
57
+ provider = provider.lower()
58
+ extra_args = dict(extra_args or {})
59
+
60
+ if provider == "openai":
61
+ return self._generate_openai(
62
+ model,
63
+ prompt,
64
+ max_output_tokens=max_output_tokens,
65
+ temperature=temperature,
66
+ extra_args=extra_args,
67
+ )
68
+ if provider == "openrouter":
69
+ return self._generate_openrouter(
70
+ model,
71
+ prompt,
72
+ max_output_tokens=max_output_tokens,
73
+ temperature=temperature,
74
+ extra_args=extra_args,
75
+ )
76
+ if provider == "deepseek":
77
+ return self._generate_deepseek(
78
+ model,
79
+ prompt,
80
+ max_output_tokens=max_output_tokens,
81
+ temperature=temperature,
82
+ extra_args=extra_args,
83
+ )
84
+ if provider == "anthropic":
85
+ return self._generate_anthropic(
86
+ model,
87
+ prompt,
88
+ max_output_tokens=max_output_tokens,
89
+ temperature=temperature,
90
+ extra_args=extra_args,
91
+ )
92
+ if provider == "google":
93
+ return self._generate_google(
94
+ model,
95
+ prompt,
96
+ temperature=temperature,
97
+ extra_args=extra_args,
98
+ )
99
+
100
+ raise ValueError(
101
+ f"Unsupported provider '{provider}'. Use openai, openrouter, "
102
+ "deepseek, anthropic, or google."
103
+ )
104
+
105
+ def _generate_openai(
106
+ self,
107
+ model: str,
108
+ prompt: str,
109
+ *,
110
+ max_output_tokens: int,
111
+ temperature: float | None,
112
+ extra_args: dict[str, Any],
113
+ ) -> GenerationResult:
114
+ from openai import OpenAI
115
+
116
+ client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
117
+ params: dict[str, Any] = {
118
+ "model": model,
119
+ "input": prompt,
120
+ "max_output_tokens": max_output_tokens,
121
+ }
122
+ if temperature is not None:
123
+ params["temperature"] = temperature
124
+ params.update(extra_args)
125
+
126
+ response = client.responses.create(**params)
127
+ usage = getattr(response, "usage", None)
128
+ return GenerationResult(
129
+ text=response.output_text or "",
130
+ input_tokens=getattr(usage, "input_tokens", None),
131
+ output_tokens=getattr(usage, "output_tokens", None),
132
+ total_tokens=getattr(usage, "total_tokens", None),
133
+ raw=response.model_dump() if hasattr(response, "model_dump") else None,
134
+ )
135
+
136
+ def _generate_openrouter(
137
+ self,
138
+ model: str,
139
+ prompt: str,
140
+ *,
141
+ max_output_tokens: int,
142
+ temperature: float | None,
143
+ extra_args: dict[str, Any],
144
+ ) -> GenerationResult:
145
+ from openai import OpenAI
146
+
147
+ client = OpenAI(
148
+ api_key=os.environ.get("OPENROUTER_API_KEY")
149
+ or os.environ.get("OPENROUTER_KEY"),
150
+ base_url="https://openrouter.ai/api/v1",
151
+ )
152
+ params: dict[str, Any] = {
153
+ "model": model,
154
+ "messages": [{"role": "user", "content": prompt}],
155
+ "max_tokens": max_output_tokens,
156
+ }
157
+ if temperature is not None:
158
+ params["temperature"] = temperature
159
+ params.update(extra_args)
160
+
161
+ response = client.chat.completions.create(**params)
162
+ usage = getattr(response, "usage", None)
163
+ message = response.choices[0].message
164
+ return GenerationResult(
165
+ text=message.content or "",
166
+ input_tokens=getattr(usage, "prompt_tokens", None),
167
+ output_tokens=getattr(usage, "completion_tokens", None),
168
+ total_tokens=getattr(usage, "total_tokens", None),
169
+ raw=response.model_dump() if hasattr(response, "model_dump") else None,
170
+ )
171
+
172
+ def _generate_deepseek(
173
+ self,
174
+ model: str,
175
+ prompt: str,
176
+ *,
177
+ max_output_tokens: int,
178
+ temperature: float | None,
179
+ extra_args: dict[str, Any],
180
+ ) -> GenerationResult:
181
+ from openai import OpenAI
182
+
183
+ client = OpenAI(
184
+ api_key=os.environ.get("DEEPSEEK_API_KEY"),
185
+ base_url="https://api.deepseek.com",
186
+ )
187
+ params: dict[str, Any] = {
188
+ "model": model,
189
+ "messages": [{"role": "user", "content": prompt}],
190
+ "max_tokens": max_output_tokens,
191
+ }
192
+ if temperature is not None:
193
+ params["temperature"] = temperature
194
+ params.update(extra_args)
195
+
196
+ response = client.chat.completions.create(**params)
197
+ usage = getattr(response, "usage", None)
198
+ message = response.choices[0].message
199
+ return GenerationResult(
200
+ text=message.content or "",
201
+ input_tokens=getattr(usage, "prompt_tokens", None),
202
+ output_tokens=getattr(usage, "completion_tokens", None),
203
+ total_tokens=getattr(usage, "total_tokens", None),
204
+ raw=response.model_dump() if hasattr(response, "model_dump") else None,
205
+ )
206
+
207
+ def _generate_anthropic(
208
+ self,
209
+ model: str,
210
+ prompt: str,
211
+ *,
212
+ max_output_tokens: int,
213
+ temperature: float | None,
214
+ extra_args: dict[str, Any],
215
+ ) -> GenerationResult:
216
+ from anthropic import Anthropic
217
+
218
+ client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
219
+ params: dict[str, Any] = {
220
+ "model": model,
221
+ "max_tokens": max_output_tokens,
222
+ "messages": [{"role": "user", "content": prompt}],
223
+ }
224
+ if temperature is not None:
225
+ params["temperature"] = temperature
226
+ params.update(extra_args)
227
+
228
+ response = client.messages.create(**params)
229
+ text_parts = [part.text for part in response.content if getattr(part, "text", "")]
230
+ usage = getattr(response, "usage", None)
231
+ input_tokens = getattr(usage, "input_tokens", None)
232
+ output_tokens = getattr(usage, "output_tokens", None)
233
+ total_tokens = None
234
+ if input_tokens is not None and output_tokens is not None:
235
+ total_tokens = input_tokens + output_tokens
236
+ return GenerationResult(
237
+ text="".join(text_parts),
238
+ input_tokens=input_tokens,
239
+ output_tokens=output_tokens,
240
+ total_tokens=total_tokens,
241
+ raw=response.model_dump() if hasattr(response, "model_dump") else None,
242
+ )
243
+
244
+ def _generate_google(
245
+ self,
246
+ model: str,
247
+ prompt: str,
248
+ *,
249
+ temperature: float | None,
250
+ extra_args: dict[str, Any],
251
+ ) -> GenerationResult:
252
+ from google import genai
253
+
254
+ client = genai.Client(api_key=os.environ.get("GOOGLE_API_KEY"))
255
+ config = dict(extra_args)
256
+ if temperature is not None:
257
+ config["temperature"] = temperature
258
+ response = client.models.generate_content(
259
+ model=model,
260
+ contents=prompt,
261
+ config=config or None,
262
+ )
263
+ text = getattr(response, "text", None) or ""
264
+ usage = getattr(response, "usage_metadata", None)
265
+ input_tokens = getattr(usage, "prompt_token_count", None)
266
+ output_tokens = getattr(usage, "candidates_token_count", None)
267
+ total_tokens = getattr(usage, "total_token_count", None)
268
+ raw = response.model_dump() if hasattr(response, "model_dump") else None
269
+ return GenerationResult(
270
+ text=text,
271
+ input_tokens=input_tokens,
272
+ output_tokens=output_tokens,
273
+ total_tokens=total_tokens,
274
+ raw=raw,
275
+ )
evals/src/evals.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from tqdm import tqdm
10
+
11
+ from clients import ModelClient
12
+ from puzzles import DATASET_REPOS, RUNS_DIR, PuzzleSpec, expand_puzzles, load_dataset_frame, load_prompt
13
+
14
+
15
+ def slugify(value: str) -> str:
16
+ cleaned = []
17
+ for ch in value:
18
+ if ch.isalnum():
19
+ cleaned.append(ch.lower())
20
+ elif ch in {"-", "_", "."}:
21
+ cleaned.append("-")
22
+ slug = "".join(cleaned).strip("-")
23
+ return slug or "run"
24
+
25
+
26
+ def timestamp_slug() -> str:
27
+ return datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
28
+
29
+
30
+ def build_run_name(
31
+ *,
32
+ model: str,
33
+ variant: str,
34
+ difficulty: str,
35
+ puzzles: list[PuzzleSpec],
36
+ ) -> str:
37
+ puzzle_part = "all" if len(puzzles) == 6 else "-".join(p.key for p in puzzles)
38
+ return f"{timestamp_slug()}-{slugify(model)}-{variant}-{difficulty}-{puzzle_part}"
39
+
40
+
41
+ def write_json(path: Path, payload: Any) -> None:
42
+ path.parent.mkdir(parents=True, exist_ok=True)
43
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
44
+
45
+
46
+ def append_jsonl(path: Path, row: dict[str, Any]) -> None:
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ with path.open("a", encoding="utf-8") as handle:
49
+ handle.write(json.dumps(row, ensure_ascii=False) + "\n")
50
+
51
+
52
+ def load_json(path: Path) -> Any:
53
+ return json.loads(path.read_text(encoding="utf-8"))
54
+
55
+
56
+ def load_jsonl(path: Path) -> list[dict[str, Any]]:
57
+ if not path.exists():
58
+ return []
59
+ rows = []
60
+ with path.open("r", encoding="utf-8") as handle:
61
+ for line in handle:
62
+ line = line.strip()
63
+ if line:
64
+ rows.append(json.loads(line))
65
+ return rows
66
+
67
+
68
+ def create_run_manifest(
69
+ *,
70
+ run_name: str,
71
+ provider: str,
72
+ model: str,
73
+ variant: str,
74
+ difficulty: str,
75
+ limit: int | None,
76
+ max_output_tokens: int,
77
+ temperature: float | None,
78
+ request_args: dict[str, Any],
79
+ puzzles: list[PuzzleSpec],
80
+ ) -> dict[str, Any]:
81
+ return {
82
+ "run_name": run_name,
83
+ "created_at": datetime.now(UTC).isoformat(),
84
+ "provider": provider,
85
+ "model": model,
86
+ "variant": variant,
87
+ "dataset_repo": DATASET_REPOS[variant],
88
+ "difficulty": difficulty,
89
+ "limit": limit,
90
+ "max_output_tokens": max_output_tokens,
91
+ "temperature": temperature,
92
+ "request_args": request_args,
93
+ "puzzles": [asdict(puzzle) for puzzle in puzzles],
94
+ }
95
+
96
+
97
+ def run_benchmarks(
98
+ *,
99
+ provider: str,
100
+ model: str,
101
+ variant: str,
102
+ difficulty: str,
103
+ puzzle_names: list[str],
104
+ limit: int | None,
105
+ max_output_tokens: int,
106
+ temperature: float | None,
107
+ request_args: dict[str, Any],
108
+ run_name: str | None,
109
+ overwrite: bool,
110
+ ) -> Path:
111
+ puzzles = expand_puzzles(puzzle_names)
112
+ resolved_run_name = run_name or build_run_name(
113
+ model=model,
114
+ variant=variant,
115
+ difficulty=difficulty,
116
+ puzzles=puzzles,
117
+ )
118
+ run_dir = RUNS_DIR / resolved_run_name
119
+ responses_path = run_dir / "responses.jsonl"
120
+ manifest_path = run_dir / "manifest.json"
121
+
122
+ if run_dir.exists() and not overwrite:
123
+ raise FileExistsError(
124
+ f"Run directory already exists: {run_dir}. "
125
+ "Pass --overwrite or choose --run-name."
126
+ )
127
+ if overwrite and run_dir.exists():
128
+ for path in run_dir.glob("*"):
129
+ if path.is_file():
130
+ path.unlink()
131
+ else:
132
+ raise RuntimeError(
133
+ f"Refusing to overwrite unexpected directory inside {run_dir}: {path}"
134
+ )
135
+
136
+ client = ModelClient()
137
+ run_dir.mkdir(parents=True, exist_ok=True)
138
+
139
+ manifest = create_run_manifest(
140
+ run_name=resolved_run_name,
141
+ provider=provider,
142
+ model=model,
143
+ variant=variant,
144
+ difficulty=difficulty,
145
+ limit=limit,
146
+ max_output_tokens=max_output_tokens,
147
+ temperature=temperature,
148
+ request_args=request_args,
149
+ puzzles=puzzles,
150
+ )
151
+ write_json(manifest_path, manifest)
152
+
153
+ for puzzle in puzzles:
154
+ dataset = load_dataset_frame(
155
+ puzzle,
156
+ variant=variant,
157
+ difficulty=difficulty,
158
+ limit=limit,
159
+ )
160
+ prompt = load_prompt(puzzle, variant)
161
+ progress = tqdm(
162
+ dataset.itertuples(index=False),
163
+ total=len(dataset),
164
+ desc=f"{puzzle.key}:{difficulty}",
165
+ )
166
+
167
+ for row in progress:
168
+ prompt_text = f"{prompt.rstrip()}\n\n{row.problem}"
169
+ generation = client.generate(
170
+ provider=provider,
171
+ model=model,
172
+ prompt=prompt_text,
173
+ max_output_tokens=max_output_tokens,
174
+ temperature=temperature,
175
+ extra_args=request_args,
176
+ )
177
+ record = {
178
+ "filename": row.filename,
179
+ "puzzlename": row.puzzlename,
180
+ "difficulty": row.difficulty,
181
+ "args": getattr(row, "args", None),
182
+ "variant": variant,
183
+ "provider": provider,
184
+ "model": model,
185
+ "prompt_file": puzzle.prompt_path(variant).name,
186
+ "dataset_repo": DATASET_REPOS[variant],
187
+ "response_text": generation.text,
188
+ "input_tokens": generation.input_tokens,
189
+ "output_tokens": generation.output_tokens,
190
+ "total_tokens": generation.total_tokens,
191
+ "created_at": datetime.now(UTC).isoformat(),
192
+ }
193
+ append_jsonl(responses_path, record)
194
+
195
+ return run_dir
evals/src/evals_verifier.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import importlib.util
5
+ import json
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import pandas as pd
12
+ from json_repair import repair_json
13
+
14
+ from evals import load_json, load_jsonl, write_json
15
+ from puzzles import MAPPINGS_DIR, REPORTS_DIR, SUBMODULES_DIR, PUZZLES, build_row_lookup, load_dataset_frame
16
+
17
+
18
+ RLP_PATH = SUBMODULES_DIR / "rlp"
19
+ FLOWFREE_PATH = SUBMODULES_DIR / "flowfree"
20
+ sys.path.insert(0, str(RLP_PATH))
21
+
22
+ from rlp.ascii_parser import ( # type: ignore
23
+ check_bridges_structural_validity,
24
+ check_galaxies_structural_validity,
25
+ check_undead_structural_validity,
26
+ )
27
+ from rlp.puzzle import Puzzle as RLPPuzzle # type: ignore
28
+ from verifier import verify_ascii_state # type: ignore
29
+
30
+
31
+ MAPPING_FILES = {
32
+ "bridges": "bridges.json",
33
+ "flow_free": "flow_free.json",
34
+ "galaxies": "galaxies.json",
35
+ "loopy": "loopy.json",
36
+ "pattern": "pattern.json",
37
+ "undead": "undead.json",
38
+ }
39
+
40
+ _mapping_cache: dict[str, dict[str, str]] = {}
41
+ _puzzle_cache: dict[tuple[str, str], RLPPuzzle] = {}
42
+
43
+
44
+ def _flowfree_verifier_module():
45
+ spec = importlib.util.spec_from_file_location(
46
+ "flowfree_verifier",
47
+ str(FLOWFREE_PATH / "verifier.py"),
48
+ )
49
+ if spec is None or spec.loader is None:
50
+ raise RuntimeError("Could not load FlowFree verifier module.")
51
+ module = importlib.util.module_from_spec(spec)
52
+ spec.loader.exec_module(module)
53
+ return module
54
+
55
+
56
+ def load_mapping(puzzle_type: str) -> dict[str, str]:
57
+ if puzzle_type not in _mapping_cache:
58
+ payload = json.loads(
59
+ (MAPPINGS_DIR / MAPPING_FILES[puzzle_type]).read_text(encoding="utf-8")
60
+ )
61
+ _mapping_cache[puzzle_type] = payload["char_map"]
62
+ return _mapping_cache[puzzle_type]
63
+
64
+
65
+ def decode_intformat_board(encoded_board: str, puzzle_type: str) -> str:
66
+ reverse_map = {value: key for key, value in load_mapping(puzzle_type).items()}
67
+ rows = []
68
+ for line in encoded_board.strip().splitlines():
69
+ cells = [cell.strip() for cell in line.split(",")]
70
+ rows.append("".join(reverse_map.get(cell, "?") for cell in cells))
71
+ return "\n".join(rows)
72
+
73
+
74
+ def decode_intformat_json_board(encoded_board: str, puzzle_type: str) -> str:
75
+ reverse_map = {value: key for key, value in load_mapping(puzzle_type).items()}
76
+ try:
77
+ grid = json.loads(encoded_board)
78
+ except json.JSONDecodeError:
79
+ try:
80
+ grid = ast.literal_eval(encoded_board)
81
+ except (SyntaxError, ValueError):
82
+ return encoded_board
83
+ if not isinstance(grid, list):
84
+ return encoded_board
85
+ rows = []
86
+ for row in grid:
87
+ if not isinstance(row, list):
88
+ return encoded_board
89
+ rows.append("".join(reverse_map.get(str(cell), "?") for cell in row))
90
+ return "\n".join(rows)
91
+
92
+
93
+ def extract_board(response_text: str) -> str | None:
94
+ if not response_text:
95
+ return None
96
+
97
+ candidates: list[str] = []
98
+
99
+ stripped = response_text.strip()
100
+ if stripped.startswith("{") or stripped.startswith("["):
101
+ candidates.append(stripped)
102
+
103
+ fenced = re.findall(r"```(?:json)?\s*(.*?)\s*```", response_text, re.DOTALL)
104
+ candidates.extend(fenced)
105
+
106
+ object_matches = re.findall(r"(\{.*?\})", response_text, re.DOTALL)
107
+ candidates.extend(object_matches)
108
+
109
+ for candidate in reversed(candidates):
110
+ try:
111
+ fixed = repair_json(candidate)
112
+ parsed = json.loads(fixed)
113
+ except Exception:
114
+ continue
115
+
116
+ if isinstance(parsed, dict) and "response" in parsed:
117
+ value = parsed["response"]
118
+ return value if isinstance(value, str) else json.dumps(value)
119
+ if isinstance(parsed, list):
120
+ return json.dumps(parsed)
121
+
122
+ return None
123
+
124
+
125
+ def verify_flow_free(problem_ascii: str, extracted_board: str) -> dict[str, Any]:
126
+ verifier = _flowfree_verifier_module()
127
+ solver_src = str(FLOWFREE_PATH / "flowfree_all_solutions.c")
128
+ solver_bin = str(FLOWFREE_PATH / "flowfree_all_solutions")
129
+ result = {
130
+ "board_exists": bool(extracted_board),
131
+ "board_valid": False,
132
+ "board_modified": False,
133
+ "correct": False,
134
+ }
135
+ if not extracted_board:
136
+ return result
137
+
138
+ is_valid, _ = verifier.verify_solution(
139
+ problem_ascii,
140
+ extracted_board,
141
+ solver_src=solver_src,
142
+ solver_bin=solver_bin,
143
+ print_solutions=False,
144
+ )
145
+ result["correct"] = bool(is_valid)
146
+
147
+ problem_lines = [line for line in problem_ascii.strip().splitlines() if line]
148
+ solution_lines = [line for line in extracted_board.strip().splitlines() if line]
149
+ if len(problem_lines) == len(solution_lines) and problem_lines:
150
+ result["board_valid"] = all(
151
+ len(problem_row) == len(solution_row)
152
+ for problem_row, solution_row in zip(problem_lines, solution_lines)
153
+ )
154
+ modified = False
155
+ for problem_row, solution_row in zip(problem_lines, solution_lines):
156
+ for start_cell, end_cell in zip(problem_row, solution_row):
157
+ if start_cell != "." and start_cell != end_cell:
158
+ modified = True
159
+ break
160
+ if modified:
161
+ break
162
+ result["board_modified"] = modified
163
+ return result
164
+
165
+
166
+ def get_or_create_rlp_puzzle(puzzle_type: str, args: str) -> RLPPuzzle:
167
+ cache_key = (puzzle_type, args)
168
+ if cache_key not in _puzzle_cache:
169
+ try:
170
+ puzzle = RLPPuzzle(puzzle_type, arg=args, headless=True)
171
+ except OSError as exc:
172
+ raise RuntimeError(
173
+ "RLP verifier libraries are not built yet. "
174
+ "Run 'bash scripts/install.sh' first."
175
+ ) from exc
176
+ puzzle.new_game()
177
+ _puzzle_cache[cache_key] = puzzle
178
+ return _puzzle_cache[cache_key]
179
+
180
+
181
+ def verify_with_rlp(
182
+ *,
183
+ puzzle_type: str,
184
+ problem_ascii: str,
185
+ extracted_board: str,
186
+ args: str,
187
+ ) -> dict[str, Any]:
188
+ result = {
189
+ "board_exists": bool(extracted_board),
190
+ "board_valid": False,
191
+ "board_modified": False,
192
+ "correct": False,
193
+ }
194
+ if not extracted_board:
195
+ return result
196
+
197
+ if puzzle_type == "bridges":
198
+ result["board_valid"] = bool(
199
+ check_bridges_structural_validity(extracted_board, problem_ascii)
200
+ )
201
+ elif puzzle_type == "galaxies":
202
+ result["board_valid"] = bool(
203
+ check_galaxies_structural_validity(extracted_board, problem_ascii)
204
+ )
205
+ elif puzzle_type == "undead":
206
+ result["board_valid"] = bool(check_undead_structural_validity(extracted_board))
207
+ else:
208
+ result["board_valid"] = True
209
+
210
+ if not result["board_valid"]:
211
+ result["board_modified"] = True
212
+ return result
213
+
214
+ puzzle = get_or_create_rlp_puzzle(puzzle_type, args)
215
+ result["correct"] = (
216
+ verify_ascii_state(
217
+ puzzle,
218
+ extracted_board,
219
+ problem_ascii=problem_ascii,
220
+ )
221
+ == "SOLVED"
222
+ )
223
+ return result
224
+
225
+
226
+ def decode_board_for_variant(board: str | None, variant: str, puzzle_type: str) -> str | None:
227
+ if board is None:
228
+ return None
229
+ if variant == "plain":
230
+ return board
231
+ if variant == "intformat":
232
+ return decode_intformat_board(board, puzzle_type)
233
+ if variant == "intformat_json":
234
+ return decode_intformat_json_board(board, puzzle_type)
235
+ raise ValueError(f"Unknown variant '{variant}'")
236
+
237
+
238
+ def summarize(frame: pd.DataFrame) -> pd.DataFrame:
239
+ summary = (
240
+ frame.groupby(["provider", "model", "variant", "puzzlename", "difficulty"], dropna=False)
241
+ .agg(
242
+ total=("filename", "count"),
243
+ board_exists=("board_exists", "sum"),
244
+ board_valid=("board_valid", "sum"),
245
+ correct=("correct", "sum"),
246
+ avg_output_tokens=("output_tokens", "mean"),
247
+ avg_total_tokens=("total_tokens", "mean"),
248
+ )
249
+ .reset_index()
250
+ )
251
+ summary["accuracy"] = summary["correct"] / summary["total"]
252
+ return summary.sort_values(
253
+ ["puzzlename", "difficulty", "provider", "model"]
254
+ ).reset_index(drop=True)
255
+
256
+
257
+ def verify_run(run_dir: Path) -> dict[str, Path]:
258
+ manifest = load_json(run_dir / "manifest.json")
259
+ records = load_jsonl(run_dir / "responses.jsonl")
260
+ variant = manifest["variant"]
261
+
262
+ puzzle_frames = {
263
+ key: load_dataset_frame(
264
+ spec,
265
+ variant=variant,
266
+ difficulty="all",
267
+ limit=None,
268
+ )
269
+ for key, spec in PUZZLES.items()
270
+ }
271
+ row_lookups = {
272
+ key: build_row_lookup(frame)
273
+ for key, frame in puzzle_frames.items()
274
+ }
275
+
276
+ detail_rows: list[dict[str, Any]] = []
277
+ for record in records:
278
+ puzzlename = record["puzzlename"]
279
+ spec = PUZZLES[puzzlename]
280
+ row = row_lookups[puzzlename][record["filename"]]
281
+ extracted_board = extract_board(record.get("response_text", ""))
282
+ decoded_problem = decode_board_for_variant(
283
+ row["problem"],
284
+ variant,
285
+ spec.verifier_type,
286
+ )
287
+ decoded_board = decode_board_for_variant(
288
+ extracted_board,
289
+ variant,
290
+ spec.verifier_type,
291
+ )
292
+ args = row.get("args") or record.get("args") or spec.default_args
293
+
294
+ if spec.verifier_type == "flow_free":
295
+ verification = verify_flow_free(decoded_problem or "", decoded_board or "")
296
+ else:
297
+ verification = verify_with_rlp(
298
+ puzzle_type=spec.verifier_type,
299
+ problem_ascii=decoded_problem or "",
300
+ extracted_board=decoded_board or "",
301
+ args=args,
302
+ )
303
+
304
+ detail_rows.append(
305
+ {
306
+ "filename": record["filename"],
307
+ "puzzlename": puzzlename,
308
+ "difficulty": row["difficulty"],
309
+ "provider": record["provider"],
310
+ "model": record["model"],
311
+ "variant": variant,
312
+ "args": args,
313
+ "prompt_file": record["prompt_file"],
314
+ "board_exists": verification["board_exists"],
315
+ "board_valid": verification["board_valid"],
316
+ "board_modified": verification["board_modified"],
317
+ "correct": verification["correct"],
318
+ "input_tokens": record.get("input_tokens"),
319
+ "output_tokens": record.get("output_tokens"),
320
+ "total_tokens": record.get("total_tokens"),
321
+ "response_text": record.get("response_text", ""),
322
+ "decoded_problem": decoded_problem,
323
+ "decoded_board": decoded_board,
324
+ }
325
+ )
326
+
327
+ detail_frame = pd.DataFrame(detail_rows)
328
+ summary_frame = summarize(detail_frame)
329
+
330
+ REPORTS_DIR.mkdir(parents=True, exist_ok=True)
331
+ detail_csv = REPORTS_DIR / f"{run_dir.name}_details.csv"
332
+ summary_csv = REPORTS_DIR / f"{run_dir.name}_summary.csv"
333
+ detail_frame.to_csv(detail_csv, index=False)
334
+ summary_frame.to_csv(summary_csv, index=False)
335
+ write_json(
336
+ REPORTS_DIR / f"{run_dir.name}_summary.json",
337
+ {
338
+ "run_dir": str(run_dir),
339
+ "rows": len(detail_frame),
340
+ "summary_rows": len(summary_frame),
341
+ },
342
+ )
343
+
344
+ print(summary_frame.to_string(index=False))
345
+ print(f"\nDetailed CSV: {detail_csv}")
346
+ print(f"Summary CSV: {summary_csv}")
347
+
348
+ return {"detail_csv": detail_csv, "summary_csv": summary_csv}
evals/src/main.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from clients import parse_request_args
7
+ from evals import run_benchmarks
8
+ from puzzles import RUNS_DIR, list_puzzle_names
9
+
10
+
11
+ def build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(
13
+ description="Run TopoBench benchmark evaluations and verify them locally."
14
+ )
15
+ subparsers = parser.add_subparsers(dest="command", required=True)
16
+
17
+ run_parser = subparsers.add_parser("run", help="Run one or more puzzle benchmarks.")
18
+ add_run_arguments(run_parser)
19
+
20
+ verify_parser = subparsers.add_parser(
21
+ "verify",
22
+ help="Verify a completed run using the copied submodule verifiers.",
23
+ )
24
+ verify_parser.add_argument(
25
+ "--run-dir",
26
+ required=True,
27
+ type=Path,
28
+ help="Run directory under results/runs/ or an absolute path.",
29
+ )
30
+
31
+ combined_parser = subparsers.add_parser(
32
+ "run-and-verify",
33
+ help="Run the benchmark and immediately verify it.",
34
+ )
35
+ add_run_arguments(combined_parser)
36
+
37
+ return parser
38
+
39
+
40
+ def add_run_arguments(parser: argparse.ArgumentParser) -> None:
41
+ parser.add_argument(
42
+ "--provider",
43
+ required=True,
44
+ choices=["openai", "openrouter", "deepseek", "anthropic", "google"],
45
+ help="Which API provider client to use.",
46
+ )
47
+ parser.add_argument(
48
+ "--model",
49
+ required=True,
50
+ help="Provider-specific model id.",
51
+ )
52
+ parser.add_argument(
53
+ "--variant",
54
+ default="plain",
55
+ choices=["plain", "intformat", "intformat_json"],
56
+ help="Which published dataset/prompt variant to use.",
57
+ )
58
+ parser.add_argument(
59
+ "--difficulty",
60
+ default="all",
61
+ choices=["all", "easy", "medium", "hard"],
62
+ help="Filter benchmark rows by difficulty.",
63
+ )
64
+ parser.add_argument(
65
+ "--puzzle",
66
+ action="append",
67
+ choices=["all", *list_puzzle_names()],
68
+ help="Puzzle(s) to run. Repeat the flag to select several. Defaults to all.",
69
+ )
70
+ parser.add_argument(
71
+ "--limit",
72
+ type=int,
73
+ default=None,
74
+ help="Maximum number of rows per selected puzzle after filtering.",
75
+ )
76
+ parser.add_argument(
77
+ "--max-output-tokens",
78
+ type=int,
79
+ default=8192,
80
+ help="Provider output token limit.",
81
+ )
82
+ parser.add_argument(
83
+ "--temperature",
84
+ type=float,
85
+ default=None,
86
+ help="Optional sampling temperature.",
87
+ )
88
+ parser.add_argument(
89
+ "--request-arg",
90
+ action="append",
91
+ default=[],
92
+ help="Extra provider request arg in KEY=VALUE form. VALUE may be JSON.",
93
+ )
94
+ parser.add_argument(
95
+ "--run-name",
96
+ default=None,
97
+ help="Optional custom run directory name.",
98
+ )
99
+ parser.add_argument(
100
+ "--overwrite",
101
+ action="store_true",
102
+ help="Overwrite an existing run directory of the same name.",
103
+ )
104
+
105
+
106
+ def resolve_run_dir(path: Path) -> Path:
107
+ if path.is_absolute():
108
+ return path
109
+ return RUNS_DIR / path
110
+
111
+
112
+ def main() -> None:
113
+ parser = build_parser()
114
+ args = parser.parse_args()
115
+
116
+ if args.command == "verify":
117
+ from evals_verifier import verify_run
118
+
119
+ verify_run(resolve_run_dir(args.run_dir))
120
+ return
121
+
122
+ request_args = parse_request_args(args.request_arg)
123
+ run_dir = run_benchmarks(
124
+ provider=args.provider,
125
+ model=args.model,
126
+ variant=args.variant,
127
+ difficulty=args.difficulty,
128
+ puzzle_names=args.puzzle or ["all"],
129
+ limit=args.limit,
130
+ max_output_tokens=args.max_output_tokens,
131
+ temperature=args.temperature,
132
+ request_args=request_args,
133
+ run_name=args.run_name,
134
+ overwrite=args.overwrite,
135
+ )
136
+ print(f"Run saved to: {run_dir}")
137
+
138
+ if args.command == "run-and-verify":
139
+ from evals_verifier import verify_run
140
+
141
+ verify_run(run_dir)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ try:
146
+ main()
147
+ except KeyboardInterrupt:
148
+ raise SystemExit("Interrupted.")
149
+ except Exception as exc:
150
+ raise SystemExit(str(exc))
evals/src/puzzles.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Iterable
6
+
7
+ import pandas as pd
8
+ from datasets import load_dataset
9
+
10
+
11
+ ROOT_DIR = Path(__file__).resolve().parents[2]
12
+ EVALS_DIR = ROOT_DIR / "evals"
13
+ PROMPTS_DIR = EVALS_DIR / "prompts"
14
+ RESULTS_DIR = ROOT_DIR / "results"
15
+ RUNS_DIR = RESULTS_DIR / "runs"
16
+ REPORTS_DIR = RESULTS_DIR / "reports"
17
+ MAPPINGS_DIR = ROOT_DIR / "mappings" / "topobench_mappings"
18
+ SUBMODULES_DIR = ROOT_DIR / "submodules"
19
+
20
+ DATASET_REPOS = {
21
+ "plain": "topobench/topobench",
22
+ "intformat": "topobench/topobench_intformat",
23
+ "intformat_json": "topobench/topobench_intformat_json",
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PuzzleSpec:
29
+ key: str
30
+ dataset_name: str
31
+ prompt_stem: str
32
+ verifier_type: str
33
+ default_args: str
34
+
35
+ def prompt_path(self, variant: str) -> Path:
36
+ suffix = {
37
+ "plain": ".txt",
38
+ "intformat": "_intformat.txt",
39
+ "intformat_json": "_intformat_json.txt",
40
+ }[variant]
41
+ return PROMPTS_DIR / f"{self.prompt_stem}{suffix}"
42
+
43
+
44
+ PUZZLES: dict[str, PuzzleSpec] = {
45
+ "bridges": PuzzleSpec(
46
+ key="bridges",
47
+ dataset_name="bridges",
48
+ prompt_stem="bridges",
49
+ verifier_type="bridges",
50
+ default_args="5x5deL",
51
+ ),
52
+ "flow_free": PuzzleSpec(
53
+ key="flow_free",
54
+ dataset_name="flow_free",
55
+ prompt_stem="flow_free",
56
+ verifier_type="flow_free",
57
+ default_args="5x5",
58
+ ),
59
+ "galaxies": PuzzleSpec(
60
+ key="galaxies",
61
+ dataset_name="galaxies",
62
+ prompt_stem="galaxies",
63
+ verifier_type="galaxies",
64
+ default_args="4x4",
65
+ ),
66
+ "loopy": PuzzleSpec(
67
+ key="loopy",
68
+ dataset_name="loopy",
69
+ prompt_stem="loopy",
70
+ verifier_type="loopy",
71
+ default_args="5x5t0",
72
+ ),
73
+ "pattern": PuzzleSpec(
74
+ key="pattern",
75
+ dataset_name="pattern",
76
+ prompt_stem="pattern",
77
+ verifier_type="pattern",
78
+ default_args="5x5",
79
+ ),
80
+ "undead": PuzzleSpec(
81
+ key="undead",
82
+ dataset_name="undead",
83
+ prompt_stem="undead",
84
+ verifier_type="undead",
85
+ default_args="4x4",
86
+ ),
87
+ }
88
+
89
+
90
+ def get_puzzle(name: str) -> PuzzleSpec:
91
+ try:
92
+ return PUZZLES[name]
93
+ except KeyError as exc:
94
+ valid = ", ".join(sorted(PUZZLES))
95
+ raise ValueError(f"Unknown puzzle '{name}'. Valid values: {valid}") from exc
96
+
97
+
98
+ def list_puzzle_names() -> list[str]:
99
+ return sorted(PUZZLES)
100
+
101
+
102
+ def expand_puzzles(names: Iterable[str]) -> list[PuzzleSpec]:
103
+ items = list(names)
104
+ if not items or items == ["all"]:
105
+ return [PUZZLES[name] for name in list_puzzle_names()]
106
+ return [get_puzzle(name) for name in items]
107
+
108
+
109
+ def load_prompt(puzzle: PuzzleSpec, variant: str) -> str:
110
+ return puzzle.prompt_path(variant).read_text(encoding="utf-8")
111
+
112
+
113
+ def load_dataset_frame(
114
+ puzzle: PuzzleSpec,
115
+ *,
116
+ variant: str,
117
+ difficulty: str,
118
+ limit: int | None,
119
+ split: str = "test",
120
+ ) -> pd.DataFrame:
121
+ repo_id = DATASET_REPOS[variant]
122
+ frame = load_dataset(repo_id, split=split).to_pandas()
123
+ frame = frame[frame["puzzlename"] == puzzle.dataset_name].copy()
124
+
125
+ if "include" in frame.columns:
126
+ frame = frame[frame["include"].fillna(False)]
127
+
128
+ if difficulty != "all":
129
+ frame = frame[frame["difficulty"] == difficulty]
130
+
131
+ frame = frame.sort_values(["difficulty", "filename"]).reset_index(drop=True)
132
+
133
+ if limit is not None:
134
+ frame = frame.head(limit).copy()
135
+
136
+ return frame.reset_index(drop=True)
137
+
138
+
139
+ def build_row_lookup(frame: pd.DataFrame) -> dict[str, pd.Series]:
140
+ return {row["filename"]: row for _, row in frame.iterrows()}
figure1.png ADDED

Git LFS Details

  • SHA256: 76ad0c523cacc973ec409f29f50cf058b028d6927dd0782e6f6a11f9f96d294b
  • Pointer size: 132 Bytes
  • Size of remote file: 4.49 MB
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x'
51
+ import reactDom from 'eslint-plugin-react-dom'
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ])
73
+ ```
frontend/eslint.config.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ globals: globals.browser,
20
+ },
21
+ },
22
+ ])
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>frontend</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "frontend",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "react": "^19.2.6",
12
+ "react-dom": "^19.2.6",
13
+ "react-router-dom": "^7.9.6"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/js": "^10.0.1",
17
+ "@types/node": "^24.12.3",
18
+ "@types/react": "^19.2.14",
19
+ "@types/react-dom": "^19.2.3",
20
+ "@vitejs/plugin-react": "^6.0.1",
21
+ "eslint": "^10.3.0",
22
+ "eslint-plugin-react-hooks": "^7.1.1",
23
+ "eslint-plugin-react-refresh": "^0.5.2",
24
+ "globals": "^17.6.0",
25
+ "typescript": "~6.0.2",
26
+ "typescript-eslint": "^8.59.2",
27
+ "vite": "^8.0.12"
28
+ }
29
+ },
30
+ "node_modules/@babel/code-frame": {
31
+ "version": "7.29.0",
32
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
33
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
34
+ "dev": true,
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@babel/helper-validator-identifier": "^7.28.5",
38
+ "js-tokens": "^4.0.0",
39
+ "picocolors": "^1.1.1"
40
+ },
41
+ "engines": {
42
+ "node": ">=6.9.0"
43
+ }
44
+ },
45
+ "node_modules/@babel/compat-data": {
46
+ "version": "7.29.3",
47
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
48
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
49
+ "dev": true,
50
+ "license": "MIT",
51
+ "engines": {
52
+ "node": ">=6.9.0"
53
+ }
54
+ },
55
+ "node_modules/@babel/core": {
56
+ "version": "7.29.0",
57
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
58
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
59
+ "dev": true,
60
+ "license": "MIT",
61
+ "dependencies": {
62
+ "@babel/code-frame": "^7.29.0",
63
+ "@babel/generator": "^7.29.0",
64
+ "@babel/helper-compilation-targets": "^7.28.6",
65
+ "@babel/helper-module-transforms": "^7.28.6",
66
+ "@babel/helpers": "^7.28.6",
67
+ "@babel/parser": "^7.29.0",
68
+ "@babel/template": "^7.28.6",
69
+ "@babel/traverse": "^7.29.0",
70
+ "@babel/types": "^7.29.0",
71
+ "@jridgewell/remapping": "^2.3.5",
72
+ "convert-source-map": "^2.0.0",
73
+ "debug": "^4.1.0",
74
+ "gensync": "^1.0.0-beta.2",
75
+ "json5": "^2.2.3",
76
+ "semver": "^6.3.1"
77
+ },
78
+ "engines": {
79
+ "node": ">=6.9.0"
80
+ },
81
+ "funding": {
82
+ "type": "opencollective",
83
+ "url": "https://opencollective.com/babel"
84
+ }
85
+ },
86
+ "node_modules/@babel/generator": {
87
+ "version": "7.29.1",
88
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
89
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
90
+ "dev": true,
91
+ "license": "MIT",
92
+ "dependencies": {
93
+ "@babel/parser": "^7.29.0",
94
+ "@babel/types": "^7.29.0",
95
+ "@jridgewell/gen-mapping": "^0.3.12",
96
+ "@jridgewell/trace-mapping": "^0.3.28",
97
+ "jsesc": "^3.0.2"
98
+ },
99
+ "engines": {
100
+ "node": ">=6.9.0"
101
+ }
102
+ },
103
+ "node_modules/@babel/helper-compilation-targets": {
104
+ "version": "7.28.6",
105
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
106
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
107
+ "dev": true,
108
+ "license": "MIT",
109
+ "dependencies": {
110
+ "@babel/compat-data": "^7.28.6",
111
+ "@babel/helper-validator-option": "^7.27.1",
112
+ "browserslist": "^4.24.0",
113
+ "lru-cache": "^5.1.1",
114
+ "semver": "^6.3.1"
115
+ },
116
+ "engines": {
117
+ "node": ">=6.9.0"
118
+ }
119
+ },
120
+ "node_modules/@babel/helper-globals": {
121
+ "version": "7.28.0",
122
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
123
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
124
+ "dev": true,
125
+ "license": "MIT",
126
+ "engines": {
127
+ "node": ">=6.9.0"
128
+ }
129
+ },
130
+ "node_modules/@babel/helper-module-imports": {
131
+ "version": "7.28.6",
132
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
133
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
134
+ "dev": true,
135
+ "license": "MIT",
136
+ "dependencies": {
137
+ "@babel/traverse": "^7.28.6",
138
+ "@babel/types": "^7.28.6"
139
+ },
140
+ "engines": {
141
+ "node": ">=6.9.0"
142
+ }
143
+ },
144
+ "node_modules/@babel/helper-module-transforms": {
145
+ "version": "7.28.6",
146
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
147
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
148
+ "dev": true,
149
+ "license": "MIT",
150
+ "dependencies": {
151
+ "@babel/helper-module-imports": "^7.28.6",
152
+ "@babel/helper-validator-identifier": "^7.28.5",
153
+ "@babel/traverse": "^7.28.6"
154
+ },
155
+ "engines": {
156
+ "node": ">=6.9.0"
157
+ },
158
+ "peerDependencies": {
159
+ "@babel/core": "^7.0.0"
160
+ }
161
+ },
162
+ "node_modules/@babel/helper-string-parser": {
163
+ "version": "7.27.1",
164
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
165
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
166
+ "dev": true,
167
+ "license": "MIT",
168
+ "engines": {
169
+ "node": ">=6.9.0"
170
+ }
171
+ },
172
+ "node_modules/@babel/helper-validator-identifier": {
173
+ "version": "7.28.5",
174
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
175
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
176
+ "dev": true,
177
+ "license": "MIT",
178
+ "engines": {
179
+ "node": ">=6.9.0"
180
+ }
181
+ },
182
+ "node_modules/@babel/helper-validator-option": {
183
+ "version": "7.27.1",
184
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
185
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
186
+ "dev": true,
187
+ "license": "MIT",
188
+ "engines": {
189
+ "node": ">=6.9.0"
190
+ }
191
+ },
192
+ "node_modules/@babel/helpers": {
193
+ "version": "7.29.2",
194
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
195
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
196
+ "dev": true,
197
+ "license": "MIT",
198
+ "dependencies": {
199
+ "@babel/template": "^7.28.6",
200
+ "@babel/types": "^7.29.0"
201
+ },
202
+ "engines": {
203
+ "node": ">=6.9.0"
204
+ }
205
+ },
206
+ "node_modules/@babel/parser": {
207
+ "version": "7.29.3",
208
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
209
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
210
+ "dev": true,
211
+ "license": "MIT",
212
+ "dependencies": {
213
+ "@babel/types": "^7.29.0"
214
+ },
215
+ "bin": {
216
+ "parser": "bin/babel-parser.js"
217
+ },
218
+ "engines": {
219
+ "node": ">=6.0.0"
220
+ }
221
+ },
222
+ "node_modules/@babel/template": {
223
+ "version": "7.28.6",
224
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
225
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
226
+ "dev": true,
227
+ "license": "MIT",
228
+ "dependencies": {
229
+ "@babel/code-frame": "^7.28.6",
230
+ "@babel/parser": "^7.28.6",
231
+ "@babel/types": "^7.28.6"
232
+ },
233
+ "engines": {
234
+ "node": ">=6.9.0"
235
+ }
236
+ },
237
+ "node_modules/@babel/traverse": {
238
+ "version": "7.29.0",
239
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
240
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
241
+ "dev": true,
242
+ "license": "MIT",
243
+ "dependencies": {
244
+ "@babel/code-frame": "^7.29.0",
245
+ "@babel/generator": "^7.29.0",
246
+ "@babel/helper-globals": "^7.28.0",
247
+ "@babel/parser": "^7.29.0",
248
+ "@babel/template": "^7.28.6",
249
+ "@babel/types": "^7.29.0",
250
+ "debug": "^4.3.1"
251
+ },
252
+ "engines": {
253
+ "node": ">=6.9.0"
254
+ }
255
+ },
256
+ "node_modules/@babel/types": {
257
+ "version": "7.29.0",
258
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
259
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
260
+ "dev": true,
261
+ "license": "MIT",
262
+ "dependencies": {
263
+ "@babel/helper-string-parser": "^7.27.1",
264
+ "@babel/helper-validator-identifier": "^7.28.5"
265
+ },
266
+ "engines": {
267
+ "node": ">=6.9.0"
268
+ }
269
+ },
270
+ "node_modules/@emnapi/core": {
271
+ "version": "1.10.0",
272
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
273
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
274
+ "dev": true,
275
+ "license": "MIT",
276
+ "optional": true,
277
+ "dependencies": {
278
+ "@emnapi/wasi-threads": "1.2.1",
279
+ "tslib": "^2.4.0"
280
+ }
281
+ },
282
+ "node_modules/@emnapi/runtime": {
283
+ "version": "1.10.0",
284
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
285
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
286
+ "dev": true,
287
+ "license": "MIT",
288
+ "optional": true,
289
+ "dependencies": {
290
+ "tslib": "^2.4.0"
291
+ }
292
+ },
293
+ "node_modules/@emnapi/wasi-threads": {
294
+ "version": "1.2.1",
295
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
296
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
297
+ "dev": true,
298
+ "license": "MIT",
299
+ "optional": true,
300
+ "dependencies": {
301
+ "tslib": "^2.4.0"
302
+ }
303
+ },
304
+ "node_modules/@eslint-community/eslint-utils": {
305
+ "version": "4.9.1",
306
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
307
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
308
+ "dev": true,
309
+ "license": "MIT",
310
+ "dependencies": {
311
+ "eslint-visitor-keys": "^3.4.3"
312
+ },
313
+ "engines": {
314
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
315
+ },
316
+ "funding": {
317
+ "url": "https://opencollective.com/eslint"
318
+ },
319
+ "peerDependencies": {
320
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
321
+ }
322
+ },
323
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
324
+ "version": "3.4.3",
325
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
326
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
327
+ "dev": true,
328
+ "license": "Apache-2.0",
329
+ "engines": {
330
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
331
+ },
332
+ "funding": {
333
+ "url": "https://opencollective.com/eslint"
334
+ }
335
+ },
336
+ "node_modules/@eslint-community/regexpp": {
337
+ "version": "4.12.2",
338
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
339
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
340
+ "dev": true,
341
+ "license": "MIT",
342
+ "engines": {
343
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
344
+ }
345
+ },
346
+ "node_modules/@eslint/config-array": {
347
+ "version": "0.23.5",
348
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
349
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
350
+ "dev": true,
351
+ "license": "Apache-2.0",
352
+ "dependencies": {
353
+ "@eslint/object-schema": "^3.0.5",
354
+ "debug": "^4.3.1",
355
+ "minimatch": "^10.2.4"
356
+ },
357
+ "engines": {
358
+ "node": "^20.19.0 || ^22.13.0 || >=24"
359
+ }
360
+ },
361
+ "node_modules/@eslint/config-helpers": {
362
+ "version": "0.6.0",
363
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz",
364
+ "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==",
365
+ "dev": true,
366
+ "license": "Apache-2.0",
367
+ "dependencies": {
368
+ "@eslint/core": "^1.2.1"
369
+ },
370
+ "engines": {
371
+ "node": "^20.19.0 || ^22.13.0 || >=24"
372
+ }
373
+ },
374
+ "node_modules/@eslint/core": {
375
+ "version": "1.2.1",
376
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
377
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
378
+ "dev": true,
379
+ "license": "Apache-2.0",
380
+ "dependencies": {
381
+ "@types/json-schema": "^7.0.15"
382
+ },
383
+ "engines": {
384
+ "node": "^20.19.0 || ^22.13.0 || >=24"
385
+ }
386
+ },
387
+ "node_modules/@eslint/js": {
388
+ "version": "10.0.1",
389
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
390
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
391
+ "dev": true,
392
+ "license": "MIT",
393
+ "engines": {
394
+ "node": "^20.19.0 || ^22.13.0 || >=24"
395
+ },
396
+ "funding": {
397
+ "url": "https://eslint.org/donate"
398
+ },
399
+ "peerDependencies": {
400
+ "eslint": "^10.0.0"
401
+ },
402
+ "peerDependenciesMeta": {
403
+ "eslint": {
404
+ "optional": true
405
+ }
406
+ }
407
+ },
408
+ "node_modules/@eslint/object-schema": {
409
+ "version": "3.0.5",
410
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
411
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
412
+ "dev": true,
413
+ "license": "Apache-2.0",
414
+ "engines": {
415
+ "node": "^20.19.0 || ^22.13.0 || >=24"
416
+ }
417
+ },
418
+ "node_modules/@eslint/plugin-kit": {
419
+ "version": "0.7.1",
420
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
421
+ "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
422
+ "dev": true,
423
+ "license": "Apache-2.0",
424
+ "dependencies": {
425
+ "@eslint/core": "^1.2.1",
426
+ "levn": "^0.4.1"
427
+ },
428
+ "engines": {
429
+ "node": "^20.19.0 || ^22.13.0 || >=24"
430
+ }
431
+ },
432
+ "node_modules/@humanfs/core": {
433
+ "version": "0.19.2",
434
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
435
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
436
+ "dev": true,
437
+ "license": "Apache-2.0",
438
+ "dependencies": {
439
+ "@humanfs/types": "^0.15.0"
440
+ },
441
+ "engines": {
442
+ "node": ">=18.18.0"
443
+ }
444
+ },
445
+ "node_modules/@humanfs/node": {
446
+ "version": "0.16.8",
447
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
448
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
449
+ "dev": true,
450
+ "license": "Apache-2.0",
451
+ "dependencies": {
452
+ "@humanfs/core": "^0.19.2",
453
+ "@humanfs/types": "^0.15.0",
454
+ "@humanwhocodes/retry": "^0.4.0"
455
+ },
456
+ "engines": {
457
+ "node": ">=18.18.0"
458
+ }
459
+ },
460
+ "node_modules/@humanfs/types": {
461
+ "version": "0.15.0",
462
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
463
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
464
+ "dev": true,
465
+ "license": "Apache-2.0",
466
+ "engines": {
467
+ "node": ">=18.18.0"
468
+ }
469
+ },
470
+ "node_modules/@humanwhocodes/module-importer": {
471
+ "version": "1.0.1",
472
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
473
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
474
+ "dev": true,
475
+ "license": "Apache-2.0",
476
+ "engines": {
477
+ "node": ">=12.22"
478
+ },
479
+ "funding": {
480
+ "type": "github",
481
+ "url": "https://github.com/sponsors/nzakas"
482
+ }
483
+ },
484
+ "node_modules/@humanwhocodes/retry": {
485
+ "version": "0.4.3",
486
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
487
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
488
+ "dev": true,
489
+ "license": "Apache-2.0",
490
+ "engines": {
491
+ "node": ">=18.18"
492
+ },
493
+ "funding": {
494
+ "type": "github",
495
+ "url": "https://github.com/sponsors/nzakas"
496
+ }
497
+ },
498
+ "node_modules/@jridgewell/gen-mapping": {
499
+ "version": "0.3.13",
500
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
501
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
502
+ "dev": true,
503
+ "license": "MIT",
504
+ "dependencies": {
505
+ "@jridgewell/sourcemap-codec": "^1.5.0",
506
+ "@jridgewell/trace-mapping": "^0.3.24"
507
+ }
508
+ },
509
+ "node_modules/@jridgewell/remapping": {
510
+ "version": "2.3.5",
511
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
512
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
513
+ "dev": true,
514
+ "license": "MIT",
515
+ "dependencies": {
516
+ "@jridgewell/gen-mapping": "^0.3.5",
517
+ "@jridgewell/trace-mapping": "^0.3.24"
518
+ }
519
+ },
520
+ "node_modules/@jridgewell/resolve-uri": {
521
+ "version": "3.1.2",
522
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
523
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
524
+ "dev": true,
525
+ "license": "MIT",
526
+ "engines": {
527
+ "node": ">=6.0.0"
528
+ }
529
+ },
530
+ "node_modules/@jridgewell/sourcemap-codec": {
531
+ "version": "1.5.5",
532
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
533
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
534
+ "dev": true,
535
+ "license": "MIT"
536
+ },
537
+ "node_modules/@jridgewell/trace-mapping": {
538
+ "version": "0.3.31",
539
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
540
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
541
+ "dev": true,
542
+ "license": "MIT",
543
+ "dependencies": {
544
+ "@jridgewell/resolve-uri": "^3.1.0",
545
+ "@jridgewell/sourcemap-codec": "^1.4.14"
546
+ }
547
+ },
548
+ "node_modules/@napi-rs/wasm-runtime": {
549
+ "version": "1.1.4",
550
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
551
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
552
+ "dev": true,
553
+ "license": "MIT",
554
+ "optional": true,
555
+ "dependencies": {
556
+ "@tybys/wasm-util": "^0.10.1"
557
+ },
558
+ "funding": {
559
+ "type": "github",
560
+ "url": "https://github.com/sponsors/Brooooooklyn"
561
+ },
562
+ "peerDependencies": {
563
+ "@emnapi/core": "^1.7.1",
564
+ "@emnapi/runtime": "^1.7.1"
565
+ }
566
+ },
567
+ "node_modules/@oxc-project/types": {
568
+ "version": "0.130.0",
569
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
570
+ "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
571
+ "dev": true,
572
+ "license": "MIT",
573
+ "funding": {
574
+ "url": "https://github.com/sponsors/Boshen"
575
+ }
576
+ },
577
+ "node_modules/@rolldown/binding-android-arm64": {
578
+ "version": "1.0.1",
579
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
580
+ "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
581
+ "cpu": [
582
+ "arm64"
583
+ ],
584
+ "dev": true,
585
+ "license": "MIT",
586
+ "optional": true,
587
+ "os": [
588
+ "android"
589
+ ],
590
+ "engines": {
591
+ "node": "^20.19.0 || >=22.12.0"
592
+ }
593
+ },
594
+ "node_modules/@rolldown/binding-darwin-arm64": {
595
+ "version": "1.0.1",
596
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
597
+ "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
598
+ "cpu": [
599
+ "arm64"
600
+ ],
601
+ "dev": true,
602
+ "license": "MIT",
603
+ "optional": true,
604
+ "os": [
605
+ "darwin"
606
+ ],
607
+ "engines": {
608
+ "node": "^20.19.0 || >=22.12.0"
609
+ }
610
+ },
611
+ "node_modules/@rolldown/binding-darwin-x64": {
612
+ "version": "1.0.1",
613
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
614
+ "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
615
+ "cpu": [
616
+ "x64"
617
+ ],
618
+ "dev": true,
619
+ "license": "MIT",
620
+ "optional": true,
621
+ "os": [
622
+ "darwin"
623
+ ],
624
+ "engines": {
625
+ "node": "^20.19.0 || >=22.12.0"
626
+ }
627
+ },
628
+ "node_modules/@rolldown/binding-freebsd-x64": {
629
+ "version": "1.0.1",
630
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
631
+ "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
632
+ "cpu": [
633
+ "x64"
634
+ ],
635
+ "dev": true,
636
+ "license": "MIT",
637
+ "optional": true,
638
+ "os": [
639
+ "freebsd"
640
+ ],
641
+ "engines": {
642
+ "node": "^20.19.0 || >=22.12.0"
643
+ }
644
+ },
645
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
646
+ "version": "1.0.1",
647
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
648
+ "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
649
+ "cpu": [
650
+ "arm"
651
+ ],
652
+ "dev": true,
653
+ "license": "MIT",
654
+ "optional": true,
655
+ "os": [
656
+ "linux"
657
+ ],
658
+ "engines": {
659
+ "node": "^20.19.0 || >=22.12.0"
660
+ }
661
+ },
662
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
663
+ "version": "1.0.1",
664
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
665
+ "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
666
+ "cpu": [
667
+ "arm64"
668
+ ],
669
+ "dev": true,
670
+ "license": "MIT",
671
+ "optional": true,
672
+ "os": [
673
+ "linux"
674
+ ],
675
+ "engines": {
676
+ "node": "^20.19.0 || >=22.12.0"
677
+ }
678
+ },
679
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
680
+ "version": "1.0.1",
681
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
682
+ "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
683
+ "cpu": [
684
+ "arm64"
685
+ ],
686
+ "dev": true,
687
+ "license": "MIT",
688
+ "optional": true,
689
+ "os": [
690
+ "linux"
691
+ ],
692
+ "engines": {
693
+ "node": "^20.19.0 || >=22.12.0"
694
+ }
695
+ },
696
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
697
+ "version": "1.0.1",
698
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
699
+ "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
700
+ "cpu": [
701
+ "ppc64"
702
+ ],
703
+ "dev": true,
704
+ "license": "MIT",
705
+ "optional": true,
706
+ "os": [
707
+ "linux"
708
+ ],
709
+ "engines": {
710
+ "node": "^20.19.0 || >=22.12.0"
711
+ }
712
+ },
713
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
714
+ "version": "1.0.1",
715
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
716
+ "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
717
+ "cpu": [
718
+ "s390x"
719
+ ],
720
+ "dev": true,
721
+ "license": "MIT",
722
+ "optional": true,
723
+ "os": [
724
+ "linux"
725
+ ],
726
+ "engines": {
727
+ "node": "^20.19.0 || >=22.12.0"
728
+ }
729
+ },
730
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
731
+ "version": "1.0.1",
732
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
733
+ "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
734
+ "cpu": [
735
+ "x64"
736
+ ],
737
+ "dev": true,
738
+ "license": "MIT",
739
+ "optional": true,
740
+ "os": [
741
+ "linux"
742
+ ],
743
+ "engines": {
744
+ "node": "^20.19.0 || >=22.12.0"
745
+ }
746
+ },
747
+ "node_modules/@rolldown/binding-linux-x64-musl": {
748
+ "version": "1.0.1",
749
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
750
+ "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
751
+ "cpu": [
752
+ "x64"
753
+ ],
754
+ "dev": true,
755
+ "license": "MIT",
756
+ "optional": true,
757
+ "os": [
758
+ "linux"
759
+ ],
760
+ "engines": {
761
+ "node": "^20.19.0 || >=22.12.0"
762
+ }
763
+ },
764
+ "node_modules/@rolldown/binding-openharmony-arm64": {
765
+ "version": "1.0.1",
766
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
767
+ "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
768
+ "cpu": [
769
+ "arm64"
770
+ ],
771
+ "dev": true,
772
+ "license": "MIT",
773
+ "optional": true,
774
+ "os": [
775
+ "openharmony"
776
+ ],
777
+ "engines": {
778
+ "node": "^20.19.0 || >=22.12.0"
779
+ }
780
+ },
781
+ "node_modules/@rolldown/binding-wasm32-wasi": {
782
+ "version": "1.0.1",
783
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
784
+ "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
785
+ "cpu": [
786
+ "wasm32"
787
+ ],
788
+ "dev": true,
789
+ "license": "MIT",
790
+ "optional": true,
791
+ "dependencies": {
792
+ "@emnapi/core": "1.10.0",
793
+ "@emnapi/runtime": "1.10.0",
794
+ "@napi-rs/wasm-runtime": "^1.1.4"
795
+ },
796
+ "engines": {
797
+ "node": "^20.19.0 || >=22.12.0"
798
+ }
799
+ },
800
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
801
+ "version": "1.0.1",
802
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
803
+ "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
804
+ "cpu": [
805
+ "arm64"
806
+ ],
807
+ "dev": true,
808
+ "license": "MIT",
809
+ "optional": true,
810
+ "os": [
811
+ "win32"
812
+ ],
813
+ "engines": {
814
+ "node": "^20.19.0 || >=22.12.0"
815
+ }
816
+ },
817
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
818
+ "version": "1.0.1",
819
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
820
+ "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
821
+ "cpu": [
822
+ "x64"
823
+ ],
824
+ "dev": true,
825
+ "license": "MIT",
826
+ "optional": true,
827
+ "os": [
828
+ "win32"
829
+ ],
830
+ "engines": {
831
+ "node": "^20.19.0 || >=22.12.0"
832
+ }
833
+ },
834
+ "node_modules/@rolldown/pluginutils": {
835
+ "version": "1.0.1",
836
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
837
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
838
+ "dev": true,
839
+ "license": "MIT"
840
+ },
841
+ "node_modules/@tybys/wasm-util": {
842
+ "version": "0.10.2",
843
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
844
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
845
+ "dev": true,
846
+ "license": "MIT",
847
+ "optional": true,
848
+ "dependencies": {
849
+ "tslib": "^2.4.0"
850
+ }
851
+ },
852
+ "node_modules/@types/esrecurse": {
853
+ "version": "4.3.1",
854
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
855
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
856
+ "dev": true,
857
+ "license": "MIT"
858
+ },
859
+ "node_modules/@types/estree": {
860
+ "version": "1.0.9",
861
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
862
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
863
+ "dev": true,
864
+ "license": "MIT"
865
+ },
866
+ "node_modules/@types/json-schema": {
867
+ "version": "7.0.15",
868
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
869
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
870
+ "dev": true,
871
+ "license": "MIT"
872
+ },
873
+ "node_modules/@types/node": {
874
+ "version": "24.12.4",
875
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
876
+ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
877
+ "dev": true,
878
+ "license": "MIT",
879
+ "dependencies": {
880
+ "undici-types": "~7.16.0"
881
+ }
882
+ },
883
+ "node_modules/@types/react": {
884
+ "version": "19.2.15",
885
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
886
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
887
+ "dev": true,
888
+ "license": "MIT",
889
+ "dependencies": {
890
+ "csstype": "^3.2.2"
891
+ }
892
+ },
893
+ "node_modules/@types/react-dom": {
894
+ "version": "19.2.3",
895
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
896
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
897
+ "dev": true,
898
+ "license": "MIT",
899
+ "peerDependencies": {
900
+ "@types/react": "^19.2.0"
901
+ }
902
+ },
903
+ "node_modules/@typescript-eslint/eslint-plugin": {
904
+ "version": "8.59.4",
905
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz",
906
+ "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==",
907
+ "dev": true,
908
+ "license": "MIT",
909
+ "dependencies": {
910
+ "@eslint-community/regexpp": "^4.12.2",
911
+ "@typescript-eslint/scope-manager": "8.59.4",
912
+ "@typescript-eslint/type-utils": "8.59.4",
913
+ "@typescript-eslint/utils": "8.59.4",
914
+ "@typescript-eslint/visitor-keys": "8.59.4",
915
+ "ignore": "^7.0.5",
916
+ "natural-compare": "^1.4.0",
917
+ "ts-api-utils": "^2.5.0"
918
+ },
919
+ "engines": {
920
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
921
+ },
922
+ "funding": {
923
+ "type": "opencollective",
924
+ "url": "https://opencollective.com/typescript-eslint"
925
+ },
926
+ "peerDependencies": {
927
+ "@typescript-eslint/parser": "^8.59.4",
928
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
929
+ "typescript": ">=4.8.4 <6.1.0"
930
+ }
931
+ },
932
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
933
+ "version": "7.0.5",
934
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
935
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
936
+ "dev": true,
937
+ "license": "MIT",
938
+ "engines": {
939
+ "node": ">= 4"
940
+ }
941
+ },
942
+ "node_modules/@typescript-eslint/parser": {
943
+ "version": "8.59.4",
944
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz",
945
+ "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==",
946
+ "dev": true,
947
+ "license": "MIT",
948
+ "dependencies": {
949
+ "@typescript-eslint/scope-manager": "8.59.4",
950
+ "@typescript-eslint/types": "8.59.4",
951
+ "@typescript-eslint/typescript-estree": "8.59.4",
952
+ "@typescript-eslint/visitor-keys": "8.59.4",
953
+ "debug": "^4.4.3"
954
+ },
955
+ "engines": {
956
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
957
+ },
958
+ "funding": {
959
+ "type": "opencollective",
960
+ "url": "https://opencollective.com/typescript-eslint"
961
+ },
962
+ "peerDependencies": {
963
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
964
+ "typescript": ">=4.8.4 <6.1.0"
965
+ }
966
+ },
967
+ "node_modules/@typescript-eslint/project-service": {
968
+ "version": "8.59.4",
969
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz",
970
+ "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==",
971
+ "dev": true,
972
+ "license": "MIT",
973
+ "dependencies": {
974
+ "@typescript-eslint/tsconfig-utils": "^8.59.4",
975
+ "@typescript-eslint/types": "^8.59.4",
976
+ "debug": "^4.4.3"
977
+ },
978
+ "engines": {
979
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
980
+ },
981
+ "funding": {
982
+ "type": "opencollective",
983
+ "url": "https://opencollective.com/typescript-eslint"
984
+ },
985
+ "peerDependencies": {
986
+ "typescript": ">=4.8.4 <6.1.0"
987
+ }
988
+ },
989
+ "node_modules/@typescript-eslint/scope-manager": {
990
+ "version": "8.59.4",
991
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz",
992
+ "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==",
993
+ "dev": true,
994
+ "license": "MIT",
995
+ "dependencies": {
996
+ "@typescript-eslint/types": "8.59.4",
997
+ "@typescript-eslint/visitor-keys": "8.59.4"
998
+ },
999
+ "engines": {
1000
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1001
+ },
1002
+ "funding": {
1003
+ "type": "opencollective",
1004
+ "url": "https://opencollective.com/typescript-eslint"
1005
+ }
1006
+ },
1007
+ "node_modules/@typescript-eslint/tsconfig-utils": {
1008
+ "version": "8.59.4",
1009
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz",
1010
+ "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==",
1011
+ "dev": true,
1012
+ "license": "MIT",
1013
+ "engines": {
1014
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1015
+ },
1016
+ "funding": {
1017
+ "type": "opencollective",
1018
+ "url": "https://opencollective.com/typescript-eslint"
1019
+ },
1020
+ "peerDependencies": {
1021
+ "typescript": ">=4.8.4 <6.1.0"
1022
+ }
1023
+ },
1024
+ "node_modules/@typescript-eslint/type-utils": {
1025
+ "version": "8.59.4",
1026
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz",
1027
+ "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==",
1028
+ "dev": true,
1029
+ "license": "MIT",
1030
+ "dependencies": {
1031
+ "@typescript-eslint/types": "8.59.4",
1032
+ "@typescript-eslint/typescript-estree": "8.59.4",
1033
+ "@typescript-eslint/utils": "8.59.4",
1034
+ "debug": "^4.4.3",
1035
+ "ts-api-utils": "^2.5.0"
1036
+ },
1037
+ "engines": {
1038
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1039
+ },
1040
+ "funding": {
1041
+ "type": "opencollective",
1042
+ "url": "https://opencollective.com/typescript-eslint"
1043
+ },
1044
+ "peerDependencies": {
1045
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
1046
+ "typescript": ">=4.8.4 <6.1.0"
1047
+ }
1048
+ },
1049
+ "node_modules/@typescript-eslint/types": {
1050
+ "version": "8.59.4",
1051
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz",
1052
+ "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==",
1053
+ "dev": true,
1054
+ "license": "MIT",
1055
+ "engines": {
1056
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1057
+ },
1058
+ "funding": {
1059
+ "type": "opencollective",
1060
+ "url": "https://opencollective.com/typescript-eslint"
1061
+ }
1062
+ },
1063
+ "node_modules/@typescript-eslint/typescript-estree": {
1064
+ "version": "8.59.4",
1065
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz",
1066
+ "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==",
1067
+ "dev": true,
1068
+ "license": "MIT",
1069
+ "dependencies": {
1070
+ "@typescript-eslint/project-service": "8.59.4",
1071
+ "@typescript-eslint/tsconfig-utils": "8.59.4",
1072
+ "@typescript-eslint/types": "8.59.4",
1073
+ "@typescript-eslint/visitor-keys": "8.59.4",
1074
+ "debug": "^4.4.3",
1075
+ "minimatch": "^10.2.2",
1076
+ "semver": "^7.7.3",
1077
+ "tinyglobby": "^0.2.15",
1078
+ "ts-api-utils": "^2.5.0"
1079
+ },
1080
+ "engines": {
1081
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1082
+ },
1083
+ "funding": {
1084
+ "type": "opencollective",
1085
+ "url": "https://opencollective.com/typescript-eslint"
1086
+ },
1087
+ "peerDependencies": {
1088
+ "typescript": ">=4.8.4 <6.1.0"
1089
+ }
1090
+ },
1091
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
1092
+ "version": "7.8.0",
1093
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
1094
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
1095
+ "dev": true,
1096
+ "license": "ISC",
1097
+ "bin": {
1098
+ "semver": "bin/semver.js"
1099
+ },
1100
+ "engines": {
1101
+ "node": ">=10"
1102
+ }
1103
+ },
1104
+ "node_modules/@typescript-eslint/utils": {
1105
+ "version": "8.59.4",
1106
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz",
1107
+ "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==",
1108
+ "dev": true,
1109
+ "license": "MIT",
1110
+ "dependencies": {
1111
+ "@eslint-community/eslint-utils": "^4.9.1",
1112
+ "@typescript-eslint/scope-manager": "8.59.4",
1113
+ "@typescript-eslint/types": "8.59.4",
1114
+ "@typescript-eslint/typescript-estree": "8.59.4"
1115
+ },
1116
+ "engines": {
1117
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1118
+ },
1119
+ "funding": {
1120
+ "type": "opencollective",
1121
+ "url": "https://opencollective.com/typescript-eslint"
1122
+ },
1123
+ "peerDependencies": {
1124
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
1125
+ "typescript": ">=4.8.4 <6.1.0"
1126
+ }
1127
+ },
1128
+ "node_modules/@typescript-eslint/visitor-keys": {
1129
+ "version": "8.59.4",
1130
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz",
1131
+ "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==",
1132
+ "dev": true,
1133
+ "license": "MIT",
1134
+ "dependencies": {
1135
+ "@typescript-eslint/types": "8.59.4",
1136
+ "eslint-visitor-keys": "^5.0.0"
1137
+ },
1138
+ "engines": {
1139
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1140
+ },
1141
+ "funding": {
1142
+ "type": "opencollective",
1143
+ "url": "https://opencollective.com/typescript-eslint"
1144
+ }
1145
+ },
1146
+ "node_modules/@vitejs/plugin-react": {
1147
+ "version": "6.0.2",
1148
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
1149
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
1150
+ "dev": true,
1151
+ "license": "MIT",
1152
+ "dependencies": {
1153
+ "@rolldown/pluginutils": "^1.0.0"
1154
+ },
1155
+ "engines": {
1156
+ "node": "^20.19.0 || >=22.12.0"
1157
+ },
1158
+ "peerDependencies": {
1159
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
1160
+ "babel-plugin-react-compiler": "^1.0.0",
1161
+ "vite": "^8.0.0"
1162
+ },
1163
+ "peerDependenciesMeta": {
1164
+ "@rolldown/plugin-babel": {
1165
+ "optional": true
1166
+ },
1167
+ "babel-plugin-react-compiler": {
1168
+ "optional": true
1169
+ }
1170
+ }
1171
+ },
1172
+ "node_modules/acorn": {
1173
+ "version": "8.16.0",
1174
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
1175
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
1176
+ "dev": true,
1177
+ "license": "MIT",
1178
+ "bin": {
1179
+ "acorn": "bin/acorn"
1180
+ },
1181
+ "engines": {
1182
+ "node": ">=0.4.0"
1183
+ }
1184
+ },
1185
+ "node_modules/acorn-jsx": {
1186
+ "version": "5.3.2",
1187
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
1188
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
1189
+ "dev": true,
1190
+ "license": "MIT",
1191
+ "peerDependencies": {
1192
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1193
+ }
1194
+ },
1195
+ "node_modules/ajv": {
1196
+ "version": "6.15.0",
1197
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
1198
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
1199
+ "dev": true,
1200
+ "license": "MIT",
1201
+ "dependencies": {
1202
+ "fast-deep-equal": "^3.1.1",
1203
+ "fast-json-stable-stringify": "^2.0.0",
1204
+ "json-schema-traverse": "^0.4.1",
1205
+ "uri-js": "^4.2.2"
1206
+ },
1207
+ "funding": {
1208
+ "type": "github",
1209
+ "url": "https://github.com/sponsors/epoberezkin"
1210
+ }
1211
+ },
1212
+ "node_modules/balanced-match": {
1213
+ "version": "4.0.4",
1214
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
1215
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
1216
+ "dev": true,
1217
+ "license": "MIT",
1218
+ "engines": {
1219
+ "node": "18 || 20 || >=22"
1220
+ }
1221
+ },
1222
+ "node_modules/baseline-browser-mapping": {
1223
+ "version": "2.10.31",
1224
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
1225
+ "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==",
1226
+ "dev": true,
1227
+ "license": "Apache-2.0",
1228
+ "bin": {
1229
+ "baseline-browser-mapping": "dist/cli.cjs"
1230
+ },
1231
+ "engines": {
1232
+ "node": ">=6.0.0"
1233
+ }
1234
+ },
1235
+ "node_modules/brace-expansion": {
1236
+ "version": "5.0.6",
1237
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
1238
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
1239
+ "dev": true,
1240
+ "license": "MIT",
1241
+ "dependencies": {
1242
+ "balanced-match": "^4.0.2"
1243
+ },
1244
+ "engines": {
1245
+ "node": "18 || 20 || >=22"
1246
+ }
1247
+ },
1248
+ "node_modules/browserslist": {
1249
+ "version": "4.28.2",
1250
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1251
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1252
+ "dev": true,
1253
+ "funding": [
1254
+ {
1255
+ "type": "opencollective",
1256
+ "url": "https://opencollective.com/browserslist"
1257
+ },
1258
+ {
1259
+ "type": "tidelift",
1260
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1261
+ },
1262
+ {
1263
+ "type": "github",
1264
+ "url": "https://github.com/sponsors/ai"
1265
+ }
1266
+ ],
1267
+ "license": "MIT",
1268
+ "dependencies": {
1269
+ "baseline-browser-mapping": "^2.10.12",
1270
+ "caniuse-lite": "^1.0.30001782",
1271
+ "electron-to-chromium": "^1.5.328",
1272
+ "node-releases": "^2.0.36",
1273
+ "update-browserslist-db": "^1.2.3"
1274
+ },
1275
+ "bin": {
1276
+ "browserslist": "cli.js"
1277
+ },
1278
+ "engines": {
1279
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1280
+ }
1281
+ },
1282
+ "node_modules/caniuse-lite": {
1283
+ "version": "1.0.30001793",
1284
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz",
1285
+ "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==",
1286
+ "dev": true,
1287
+ "funding": [
1288
+ {
1289
+ "type": "opencollective",
1290
+ "url": "https://opencollective.com/browserslist"
1291
+ },
1292
+ {
1293
+ "type": "tidelift",
1294
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1295
+ },
1296
+ {
1297
+ "type": "github",
1298
+ "url": "https://github.com/sponsors/ai"
1299
+ }
1300
+ ],
1301
+ "license": "CC-BY-4.0"
1302
+ },
1303
+ "node_modules/convert-source-map": {
1304
+ "version": "2.0.0",
1305
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1306
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1307
+ "dev": true,
1308
+ "license": "MIT"
1309
+ },
1310
+ "node_modules/cookie": {
1311
+ "version": "1.1.1",
1312
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
1313
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
1314
+ "license": "MIT",
1315
+ "engines": {
1316
+ "node": ">=18"
1317
+ },
1318
+ "funding": {
1319
+ "type": "opencollective",
1320
+ "url": "https://opencollective.com/express"
1321
+ }
1322
+ },
1323
+ "node_modules/cross-spawn": {
1324
+ "version": "7.0.6",
1325
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1326
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1327
+ "dev": true,
1328
+ "license": "MIT",
1329
+ "dependencies": {
1330
+ "path-key": "^3.1.0",
1331
+ "shebang-command": "^2.0.0",
1332
+ "which": "^2.0.1"
1333
+ },
1334
+ "engines": {
1335
+ "node": ">= 8"
1336
+ }
1337
+ },
1338
+ "node_modules/csstype": {
1339
+ "version": "3.2.3",
1340
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1341
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1342
+ "dev": true,
1343
+ "license": "MIT"
1344
+ },
1345
+ "node_modules/debug": {
1346
+ "version": "4.4.3",
1347
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1348
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1349
+ "dev": true,
1350
+ "license": "MIT",
1351
+ "dependencies": {
1352
+ "ms": "^2.1.3"
1353
+ },
1354
+ "engines": {
1355
+ "node": ">=6.0"
1356
+ },
1357
+ "peerDependenciesMeta": {
1358
+ "supports-color": {
1359
+ "optional": true
1360
+ }
1361
+ }
1362
+ },
1363
+ "node_modules/deep-is": {
1364
+ "version": "0.1.4",
1365
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
1366
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
1367
+ "dev": true,
1368
+ "license": "MIT"
1369
+ },
1370
+ "node_modules/detect-libc": {
1371
+ "version": "2.1.2",
1372
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1373
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1374
+ "dev": true,
1375
+ "license": "Apache-2.0",
1376
+ "engines": {
1377
+ "node": ">=8"
1378
+ }
1379
+ },
1380
+ "node_modules/electron-to-chromium": {
1381
+ "version": "1.5.359",
1382
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.359.tgz",
1383
+ "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==",
1384
+ "dev": true,
1385
+ "license": "ISC"
1386
+ },
1387
+ "node_modules/escalade": {
1388
+ "version": "3.2.0",
1389
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1390
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1391
+ "dev": true,
1392
+ "license": "MIT",
1393
+ "engines": {
1394
+ "node": ">=6"
1395
+ }
1396
+ },
1397
+ "node_modules/escape-string-regexp": {
1398
+ "version": "4.0.0",
1399
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
1400
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
1401
+ "dev": true,
1402
+ "license": "MIT",
1403
+ "engines": {
1404
+ "node": ">=10"
1405
+ },
1406
+ "funding": {
1407
+ "url": "https://github.com/sponsors/sindresorhus"
1408
+ }
1409
+ },
1410
+ "node_modules/eslint": {
1411
+ "version": "10.4.0",
1412
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
1413
+ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
1414
+ "dev": true,
1415
+ "license": "MIT",
1416
+ "dependencies": {
1417
+ "@eslint-community/eslint-utils": "^4.8.0",
1418
+ "@eslint-community/regexpp": "^4.12.2",
1419
+ "@eslint/config-array": "^0.23.5",
1420
+ "@eslint/config-helpers": "^0.6.0",
1421
+ "@eslint/core": "^1.2.1",
1422
+ "@eslint/plugin-kit": "^0.7.1",
1423
+ "@humanfs/node": "^0.16.6",
1424
+ "@humanwhocodes/module-importer": "^1.0.1",
1425
+ "@humanwhocodes/retry": "^0.4.2",
1426
+ "@types/estree": "^1.0.6",
1427
+ "ajv": "^6.14.0",
1428
+ "cross-spawn": "^7.0.6",
1429
+ "debug": "^4.3.2",
1430
+ "escape-string-regexp": "^4.0.0",
1431
+ "eslint-scope": "^9.1.2",
1432
+ "eslint-visitor-keys": "^5.0.1",
1433
+ "espree": "^11.2.0",
1434
+ "esquery": "^1.7.0",
1435
+ "esutils": "^2.0.2",
1436
+ "fast-deep-equal": "^3.1.3",
1437
+ "file-entry-cache": "^8.0.0",
1438
+ "find-up": "^5.0.0",
1439
+ "glob-parent": "^6.0.2",
1440
+ "ignore": "^5.2.0",
1441
+ "imurmurhash": "^0.1.4",
1442
+ "is-glob": "^4.0.0",
1443
+ "json-stable-stringify-without-jsonify": "^1.0.1",
1444
+ "minimatch": "^10.2.4",
1445
+ "natural-compare": "^1.4.0",
1446
+ "optionator": "^0.9.3"
1447
+ },
1448
+ "bin": {
1449
+ "eslint": "bin/eslint.js"
1450
+ },
1451
+ "engines": {
1452
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1453
+ },
1454
+ "funding": {
1455
+ "url": "https://eslint.org/donate"
1456
+ },
1457
+ "peerDependencies": {
1458
+ "jiti": "*"
1459
+ },
1460
+ "peerDependenciesMeta": {
1461
+ "jiti": {
1462
+ "optional": true
1463
+ }
1464
+ }
1465
+ },
1466
+ "node_modules/eslint-plugin-react-hooks": {
1467
+ "version": "7.1.1",
1468
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
1469
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
1470
+ "dev": true,
1471
+ "license": "MIT",
1472
+ "dependencies": {
1473
+ "@babel/core": "^7.24.4",
1474
+ "@babel/parser": "^7.24.4",
1475
+ "hermes-parser": "^0.25.1",
1476
+ "zod": "^3.25.0 || ^4.0.0",
1477
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
1478
+ },
1479
+ "engines": {
1480
+ "node": ">=18"
1481
+ },
1482
+ "peerDependencies": {
1483
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
1484
+ }
1485
+ },
1486
+ "node_modules/eslint-plugin-react-refresh": {
1487
+ "version": "0.5.2",
1488
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
1489
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
1490
+ "dev": true,
1491
+ "license": "MIT",
1492
+ "peerDependencies": {
1493
+ "eslint": "^9 || ^10"
1494
+ }
1495
+ },
1496
+ "node_modules/eslint-scope": {
1497
+ "version": "9.1.2",
1498
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
1499
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
1500
+ "dev": true,
1501
+ "license": "BSD-2-Clause",
1502
+ "dependencies": {
1503
+ "@types/esrecurse": "^4.3.1",
1504
+ "@types/estree": "^1.0.8",
1505
+ "esrecurse": "^4.3.0",
1506
+ "estraverse": "^5.2.0"
1507
+ },
1508
+ "engines": {
1509
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1510
+ },
1511
+ "funding": {
1512
+ "url": "https://opencollective.com/eslint"
1513
+ }
1514
+ },
1515
+ "node_modules/eslint-visitor-keys": {
1516
+ "version": "5.0.1",
1517
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
1518
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
1519
+ "dev": true,
1520
+ "license": "Apache-2.0",
1521
+ "engines": {
1522
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1523
+ },
1524
+ "funding": {
1525
+ "url": "https://opencollective.com/eslint"
1526
+ }
1527
+ },
1528
+ "node_modules/espree": {
1529
+ "version": "11.2.0",
1530
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
1531
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
1532
+ "dev": true,
1533
+ "license": "BSD-2-Clause",
1534
+ "dependencies": {
1535
+ "acorn": "^8.16.0",
1536
+ "acorn-jsx": "^5.3.2",
1537
+ "eslint-visitor-keys": "^5.0.1"
1538
+ },
1539
+ "engines": {
1540
+ "node": "^20.19.0 || ^22.13.0 || >=24"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://opencollective.com/eslint"
1544
+ }
1545
+ },
1546
+ "node_modules/esquery": {
1547
+ "version": "1.7.0",
1548
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
1549
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
1550
+ "dev": true,
1551
+ "license": "BSD-3-Clause",
1552
+ "dependencies": {
1553
+ "estraverse": "^5.1.0"
1554
+ },
1555
+ "engines": {
1556
+ "node": ">=0.10"
1557
+ }
1558
+ },
1559
+ "node_modules/esrecurse": {
1560
+ "version": "4.3.0",
1561
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
1562
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
1563
+ "dev": true,
1564
+ "license": "BSD-2-Clause",
1565
+ "dependencies": {
1566
+ "estraverse": "^5.2.0"
1567
+ },
1568
+ "engines": {
1569
+ "node": ">=4.0"
1570
+ }
1571
+ },
1572
+ "node_modules/estraverse": {
1573
+ "version": "5.3.0",
1574
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
1575
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
1576
+ "dev": true,
1577
+ "license": "BSD-2-Clause",
1578
+ "engines": {
1579
+ "node": ">=4.0"
1580
+ }
1581
+ },
1582
+ "node_modules/esutils": {
1583
+ "version": "2.0.3",
1584
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
1585
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
1586
+ "dev": true,
1587
+ "license": "BSD-2-Clause",
1588
+ "engines": {
1589
+ "node": ">=0.10.0"
1590
+ }
1591
+ },
1592
+ "node_modules/fast-deep-equal": {
1593
+ "version": "3.1.3",
1594
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
1595
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
1596
+ "dev": true,
1597
+ "license": "MIT"
1598
+ },
1599
+ "node_modules/fast-json-stable-stringify": {
1600
+ "version": "2.1.0",
1601
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
1602
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
1603
+ "dev": true,
1604
+ "license": "MIT"
1605
+ },
1606
+ "node_modules/fast-levenshtein": {
1607
+ "version": "2.0.6",
1608
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
1609
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
1610
+ "dev": true,
1611
+ "license": "MIT"
1612
+ },
1613
+ "node_modules/fdir": {
1614
+ "version": "6.5.0",
1615
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1616
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1617
+ "dev": true,
1618
+ "license": "MIT",
1619
+ "engines": {
1620
+ "node": ">=12.0.0"
1621
+ },
1622
+ "peerDependencies": {
1623
+ "picomatch": "^3 || ^4"
1624
+ },
1625
+ "peerDependenciesMeta": {
1626
+ "picomatch": {
1627
+ "optional": true
1628
+ }
1629
+ }
1630
+ },
1631
+ "node_modules/file-entry-cache": {
1632
+ "version": "8.0.0",
1633
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
1634
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
1635
+ "dev": true,
1636
+ "license": "MIT",
1637
+ "dependencies": {
1638
+ "flat-cache": "^4.0.0"
1639
+ },
1640
+ "engines": {
1641
+ "node": ">=16.0.0"
1642
+ }
1643
+ },
1644
+ "node_modules/find-up": {
1645
+ "version": "5.0.0",
1646
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
1647
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
1648
+ "dev": true,
1649
+ "license": "MIT",
1650
+ "dependencies": {
1651
+ "locate-path": "^6.0.0",
1652
+ "path-exists": "^4.0.0"
1653
+ },
1654
+ "engines": {
1655
+ "node": ">=10"
1656
+ },
1657
+ "funding": {
1658
+ "url": "https://github.com/sponsors/sindresorhus"
1659
+ }
1660
+ },
1661
+ "node_modules/flat-cache": {
1662
+ "version": "4.0.1",
1663
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
1664
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
1665
+ "dev": true,
1666
+ "license": "MIT",
1667
+ "dependencies": {
1668
+ "flatted": "^3.2.9",
1669
+ "keyv": "^4.5.4"
1670
+ },
1671
+ "engines": {
1672
+ "node": ">=16"
1673
+ }
1674
+ },
1675
+ "node_modules/flatted": {
1676
+ "version": "3.4.2",
1677
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
1678
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
1679
+ "dev": true,
1680
+ "license": "ISC"
1681
+ },
1682
+ "node_modules/fsevents": {
1683
+ "version": "2.3.3",
1684
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1685
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1686
+ "dev": true,
1687
+ "hasInstallScript": true,
1688
+ "license": "MIT",
1689
+ "optional": true,
1690
+ "os": [
1691
+ "darwin"
1692
+ ],
1693
+ "engines": {
1694
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1695
+ }
1696
+ },
1697
+ "node_modules/gensync": {
1698
+ "version": "1.0.0-beta.2",
1699
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1700
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1701
+ "dev": true,
1702
+ "license": "MIT",
1703
+ "engines": {
1704
+ "node": ">=6.9.0"
1705
+ }
1706
+ },
1707
+ "node_modules/glob-parent": {
1708
+ "version": "6.0.2",
1709
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
1710
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
1711
+ "dev": true,
1712
+ "license": "ISC",
1713
+ "dependencies": {
1714
+ "is-glob": "^4.0.3"
1715
+ },
1716
+ "engines": {
1717
+ "node": ">=10.13.0"
1718
+ }
1719
+ },
1720
+ "node_modules/globals": {
1721
+ "version": "17.6.0",
1722
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
1723
+ "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
1724
+ "dev": true,
1725
+ "license": "MIT",
1726
+ "engines": {
1727
+ "node": ">=18"
1728
+ },
1729
+ "funding": {
1730
+ "url": "https://github.com/sponsors/sindresorhus"
1731
+ }
1732
+ },
1733
+ "node_modules/hermes-estree": {
1734
+ "version": "0.25.1",
1735
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
1736
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
1737
+ "dev": true,
1738
+ "license": "MIT"
1739
+ },
1740
+ "node_modules/hermes-parser": {
1741
+ "version": "0.25.1",
1742
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
1743
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
1744
+ "dev": true,
1745
+ "license": "MIT",
1746
+ "dependencies": {
1747
+ "hermes-estree": "0.25.1"
1748
+ }
1749
+ },
1750
+ "node_modules/ignore": {
1751
+ "version": "5.3.2",
1752
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
1753
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
1754
+ "dev": true,
1755
+ "license": "MIT",
1756
+ "engines": {
1757
+ "node": ">= 4"
1758
+ }
1759
+ },
1760
+ "node_modules/imurmurhash": {
1761
+ "version": "0.1.4",
1762
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
1763
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
1764
+ "dev": true,
1765
+ "license": "MIT",
1766
+ "engines": {
1767
+ "node": ">=0.8.19"
1768
+ }
1769
+ },
1770
+ "node_modules/is-extglob": {
1771
+ "version": "2.1.1",
1772
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
1773
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
1774
+ "dev": true,
1775
+ "license": "MIT",
1776
+ "engines": {
1777
+ "node": ">=0.10.0"
1778
+ }
1779
+ },
1780
+ "node_modules/is-glob": {
1781
+ "version": "4.0.3",
1782
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
1783
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
1784
+ "dev": true,
1785
+ "license": "MIT",
1786
+ "dependencies": {
1787
+ "is-extglob": "^2.1.1"
1788
+ },
1789
+ "engines": {
1790
+ "node": ">=0.10.0"
1791
+ }
1792
+ },
1793
+ "node_modules/isexe": {
1794
+ "version": "2.0.0",
1795
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1796
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1797
+ "dev": true,
1798
+ "license": "ISC"
1799
+ },
1800
+ "node_modules/js-tokens": {
1801
+ "version": "4.0.0",
1802
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1803
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1804
+ "dev": true,
1805
+ "license": "MIT"
1806
+ },
1807
+ "node_modules/jsesc": {
1808
+ "version": "3.1.0",
1809
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1810
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1811
+ "dev": true,
1812
+ "license": "MIT",
1813
+ "bin": {
1814
+ "jsesc": "bin/jsesc"
1815
+ },
1816
+ "engines": {
1817
+ "node": ">=6"
1818
+ }
1819
+ },
1820
+ "node_modules/json-buffer": {
1821
+ "version": "3.0.1",
1822
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
1823
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
1824
+ "dev": true,
1825
+ "license": "MIT"
1826
+ },
1827
+ "node_modules/json-schema-traverse": {
1828
+ "version": "0.4.1",
1829
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1830
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
1831
+ "dev": true,
1832
+ "license": "MIT"
1833
+ },
1834
+ "node_modules/json-stable-stringify-without-jsonify": {
1835
+ "version": "1.0.1",
1836
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
1837
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
1838
+ "dev": true,
1839
+ "license": "MIT"
1840
+ },
1841
+ "node_modules/json5": {
1842
+ "version": "2.2.3",
1843
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1844
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1845
+ "dev": true,
1846
+ "license": "MIT",
1847
+ "bin": {
1848
+ "json5": "lib/cli.js"
1849
+ },
1850
+ "engines": {
1851
+ "node": ">=6"
1852
+ }
1853
+ },
1854
+ "node_modules/keyv": {
1855
+ "version": "4.5.4",
1856
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
1857
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
1858
+ "dev": true,
1859
+ "license": "MIT",
1860
+ "dependencies": {
1861
+ "json-buffer": "3.0.1"
1862
+ }
1863
+ },
1864
+ "node_modules/levn": {
1865
+ "version": "0.4.1",
1866
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
1867
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
1868
+ "dev": true,
1869
+ "license": "MIT",
1870
+ "dependencies": {
1871
+ "prelude-ls": "^1.2.1",
1872
+ "type-check": "~0.4.0"
1873
+ },
1874
+ "engines": {
1875
+ "node": ">= 0.8.0"
1876
+ }
1877
+ },
1878
+ "node_modules/lightningcss": {
1879
+ "version": "1.32.0",
1880
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
1881
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
1882
+ "dev": true,
1883
+ "license": "MPL-2.0",
1884
+ "dependencies": {
1885
+ "detect-libc": "^2.0.3"
1886
+ },
1887
+ "engines": {
1888
+ "node": ">= 12.0.0"
1889
+ },
1890
+ "funding": {
1891
+ "type": "opencollective",
1892
+ "url": "https://opencollective.com/parcel"
1893
+ },
1894
+ "optionalDependencies": {
1895
+ "lightningcss-android-arm64": "1.32.0",
1896
+ "lightningcss-darwin-arm64": "1.32.0",
1897
+ "lightningcss-darwin-x64": "1.32.0",
1898
+ "lightningcss-freebsd-x64": "1.32.0",
1899
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
1900
+ "lightningcss-linux-arm64-gnu": "1.32.0",
1901
+ "lightningcss-linux-arm64-musl": "1.32.0",
1902
+ "lightningcss-linux-x64-gnu": "1.32.0",
1903
+ "lightningcss-linux-x64-musl": "1.32.0",
1904
+ "lightningcss-win32-arm64-msvc": "1.32.0",
1905
+ "lightningcss-win32-x64-msvc": "1.32.0"
1906
+ }
1907
+ },
1908
+ "node_modules/lightningcss-android-arm64": {
1909
+ "version": "1.32.0",
1910
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
1911
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
1912
+ "cpu": [
1913
+ "arm64"
1914
+ ],
1915
+ "dev": true,
1916
+ "license": "MPL-2.0",
1917
+ "optional": true,
1918
+ "os": [
1919
+ "android"
1920
+ ],
1921
+ "engines": {
1922
+ "node": ">= 12.0.0"
1923
+ },
1924
+ "funding": {
1925
+ "type": "opencollective",
1926
+ "url": "https://opencollective.com/parcel"
1927
+ }
1928
+ },
1929
+ "node_modules/lightningcss-darwin-arm64": {
1930
+ "version": "1.32.0",
1931
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
1932
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
1933
+ "cpu": [
1934
+ "arm64"
1935
+ ],
1936
+ "dev": true,
1937
+ "license": "MPL-2.0",
1938
+ "optional": true,
1939
+ "os": [
1940
+ "darwin"
1941
+ ],
1942
+ "engines": {
1943
+ "node": ">= 12.0.0"
1944
+ },
1945
+ "funding": {
1946
+ "type": "opencollective",
1947
+ "url": "https://opencollective.com/parcel"
1948
+ }
1949
+ },
1950
+ "node_modules/lightningcss-darwin-x64": {
1951
+ "version": "1.32.0",
1952
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
1953
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
1954
+ "cpu": [
1955
+ "x64"
1956
+ ],
1957
+ "dev": true,
1958
+ "license": "MPL-2.0",
1959
+ "optional": true,
1960
+ "os": [
1961
+ "darwin"
1962
+ ],
1963
+ "engines": {
1964
+ "node": ">= 12.0.0"
1965
+ },
1966
+ "funding": {
1967
+ "type": "opencollective",
1968
+ "url": "https://opencollective.com/parcel"
1969
+ }
1970
+ },
1971
+ "node_modules/lightningcss-freebsd-x64": {
1972
+ "version": "1.32.0",
1973
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
1974
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
1975
+ "cpu": [
1976
+ "x64"
1977
+ ],
1978
+ "dev": true,
1979
+ "license": "MPL-2.0",
1980
+ "optional": true,
1981
+ "os": [
1982
+ "freebsd"
1983
+ ],
1984
+ "engines": {
1985
+ "node": ">= 12.0.0"
1986
+ },
1987
+ "funding": {
1988
+ "type": "opencollective",
1989
+ "url": "https://opencollective.com/parcel"
1990
+ }
1991
+ },
1992
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
1993
+ "version": "1.32.0",
1994
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
1995
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
1996
+ "cpu": [
1997
+ "arm"
1998
+ ],
1999
+ "dev": true,
2000
+ "license": "MPL-2.0",
2001
+ "optional": true,
2002
+ "os": [
2003
+ "linux"
2004
+ ],
2005
+ "engines": {
2006
+ "node": ">= 12.0.0"
2007
+ },
2008
+ "funding": {
2009
+ "type": "opencollective",
2010
+ "url": "https://opencollective.com/parcel"
2011
+ }
2012
+ },
2013
+ "node_modules/lightningcss-linux-arm64-gnu": {
2014
+ "version": "1.32.0",
2015
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
2016
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
2017
+ "cpu": [
2018
+ "arm64"
2019
+ ],
2020
+ "dev": true,
2021
+ "license": "MPL-2.0",
2022
+ "optional": true,
2023
+ "os": [
2024
+ "linux"
2025
+ ],
2026
+ "engines": {
2027
+ "node": ">= 12.0.0"
2028
+ },
2029
+ "funding": {
2030
+ "type": "opencollective",
2031
+ "url": "https://opencollective.com/parcel"
2032
+ }
2033
+ },
2034
+ "node_modules/lightningcss-linux-arm64-musl": {
2035
+ "version": "1.32.0",
2036
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
2037
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
2038
+ "cpu": [
2039
+ "arm64"
2040
+ ],
2041
+ "dev": true,
2042
+ "license": "MPL-2.0",
2043
+ "optional": true,
2044
+ "os": [
2045
+ "linux"
2046
+ ],
2047
+ "engines": {
2048
+ "node": ">= 12.0.0"
2049
+ },
2050
+ "funding": {
2051
+ "type": "opencollective",
2052
+ "url": "https://opencollective.com/parcel"
2053
+ }
2054
+ },
2055
+ "node_modules/lightningcss-linux-x64-gnu": {
2056
+ "version": "1.32.0",
2057
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
2058
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
2059
+ "cpu": [
2060
+ "x64"
2061
+ ],
2062
+ "dev": true,
2063
+ "license": "MPL-2.0",
2064
+ "optional": true,
2065
+ "os": [
2066
+ "linux"
2067
+ ],
2068
+ "engines": {
2069
+ "node": ">= 12.0.0"
2070
+ },
2071
+ "funding": {
2072
+ "type": "opencollective",
2073
+ "url": "https://opencollective.com/parcel"
2074
+ }
2075
+ },
2076
+ "node_modules/lightningcss-linux-x64-musl": {
2077
+ "version": "1.32.0",
2078
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
2079
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
2080
+ "cpu": [
2081
+ "x64"
2082
+ ],
2083
+ "dev": true,
2084
+ "license": "MPL-2.0",
2085
+ "optional": true,
2086
+ "os": [
2087
+ "linux"
2088
+ ],
2089
+ "engines": {
2090
+ "node": ">= 12.0.0"
2091
+ },
2092
+ "funding": {
2093
+ "type": "opencollective",
2094
+ "url": "https://opencollective.com/parcel"
2095
+ }
2096
+ },
2097
+ "node_modules/lightningcss-win32-arm64-msvc": {
2098
+ "version": "1.32.0",
2099
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
2100
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
2101
+ "cpu": [
2102
+ "arm64"
2103
+ ],
2104
+ "dev": true,
2105
+ "license": "MPL-2.0",
2106
+ "optional": true,
2107
+ "os": [
2108
+ "win32"
2109
+ ],
2110
+ "engines": {
2111
+ "node": ">= 12.0.0"
2112
+ },
2113
+ "funding": {
2114
+ "type": "opencollective",
2115
+ "url": "https://opencollective.com/parcel"
2116
+ }
2117
+ },
2118
+ "node_modules/lightningcss-win32-x64-msvc": {
2119
+ "version": "1.32.0",
2120
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
2121
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
2122
+ "cpu": [
2123
+ "x64"
2124
+ ],
2125
+ "dev": true,
2126
+ "license": "MPL-2.0",
2127
+ "optional": true,
2128
+ "os": [
2129
+ "win32"
2130
+ ],
2131
+ "engines": {
2132
+ "node": ">= 12.0.0"
2133
+ },
2134
+ "funding": {
2135
+ "type": "opencollective",
2136
+ "url": "https://opencollective.com/parcel"
2137
+ }
2138
+ },
2139
+ "node_modules/locate-path": {
2140
+ "version": "6.0.0",
2141
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
2142
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
2143
+ "dev": true,
2144
+ "license": "MIT",
2145
+ "dependencies": {
2146
+ "p-locate": "^5.0.0"
2147
+ },
2148
+ "engines": {
2149
+ "node": ">=10"
2150
+ },
2151
+ "funding": {
2152
+ "url": "https://github.com/sponsors/sindresorhus"
2153
+ }
2154
+ },
2155
+ "node_modules/lru-cache": {
2156
+ "version": "5.1.1",
2157
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2158
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2159
+ "dev": true,
2160
+ "license": "ISC",
2161
+ "dependencies": {
2162
+ "yallist": "^3.0.2"
2163
+ }
2164
+ },
2165
+ "node_modules/minimatch": {
2166
+ "version": "10.2.5",
2167
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
2168
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
2169
+ "dev": true,
2170
+ "license": "BlueOak-1.0.0",
2171
+ "dependencies": {
2172
+ "brace-expansion": "^5.0.5"
2173
+ },
2174
+ "engines": {
2175
+ "node": "18 || 20 || >=22"
2176
+ },
2177
+ "funding": {
2178
+ "url": "https://github.com/sponsors/isaacs"
2179
+ }
2180
+ },
2181
+ "node_modules/ms": {
2182
+ "version": "2.1.3",
2183
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
2184
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
2185
+ "dev": true,
2186
+ "license": "MIT"
2187
+ },
2188
+ "node_modules/nanoid": {
2189
+ "version": "3.3.12",
2190
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
2191
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
2192
+ "dev": true,
2193
+ "funding": [
2194
+ {
2195
+ "type": "github",
2196
+ "url": "https://github.com/sponsors/ai"
2197
+ }
2198
+ ],
2199
+ "license": "MIT",
2200
+ "bin": {
2201
+ "nanoid": "bin/nanoid.cjs"
2202
+ },
2203
+ "engines": {
2204
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2205
+ }
2206
+ },
2207
+ "node_modules/natural-compare": {
2208
+ "version": "1.4.0",
2209
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
2210
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
2211
+ "dev": true,
2212
+ "license": "MIT"
2213
+ },
2214
+ "node_modules/node-releases": {
2215
+ "version": "2.0.44",
2216
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
2217
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
2218
+ "dev": true,
2219
+ "license": "MIT"
2220
+ },
2221
+ "node_modules/optionator": {
2222
+ "version": "0.9.4",
2223
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
2224
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
2225
+ "dev": true,
2226
+ "license": "MIT",
2227
+ "dependencies": {
2228
+ "deep-is": "^0.1.3",
2229
+ "fast-levenshtein": "^2.0.6",
2230
+ "levn": "^0.4.1",
2231
+ "prelude-ls": "^1.2.1",
2232
+ "type-check": "^0.4.0",
2233
+ "word-wrap": "^1.2.5"
2234
+ },
2235
+ "engines": {
2236
+ "node": ">= 0.8.0"
2237
+ }
2238
+ },
2239
+ "node_modules/p-limit": {
2240
+ "version": "3.1.0",
2241
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
2242
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
2243
+ "dev": true,
2244
+ "license": "MIT",
2245
+ "dependencies": {
2246
+ "yocto-queue": "^0.1.0"
2247
+ },
2248
+ "engines": {
2249
+ "node": ">=10"
2250
+ },
2251
+ "funding": {
2252
+ "url": "https://github.com/sponsors/sindresorhus"
2253
+ }
2254
+ },
2255
+ "node_modules/p-locate": {
2256
+ "version": "5.0.0",
2257
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
2258
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
2259
+ "dev": true,
2260
+ "license": "MIT",
2261
+ "dependencies": {
2262
+ "p-limit": "^3.0.2"
2263
+ },
2264
+ "engines": {
2265
+ "node": ">=10"
2266
+ },
2267
+ "funding": {
2268
+ "url": "https://github.com/sponsors/sindresorhus"
2269
+ }
2270
+ },
2271
+ "node_modules/path-exists": {
2272
+ "version": "4.0.0",
2273
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
2274
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
2275
+ "dev": true,
2276
+ "license": "MIT",
2277
+ "engines": {
2278
+ "node": ">=8"
2279
+ }
2280
+ },
2281
+ "node_modules/path-key": {
2282
+ "version": "3.1.1",
2283
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
2284
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
2285
+ "dev": true,
2286
+ "license": "MIT",
2287
+ "engines": {
2288
+ "node": ">=8"
2289
+ }
2290
+ },
2291
+ "node_modules/picocolors": {
2292
+ "version": "1.1.1",
2293
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2294
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2295
+ "dev": true,
2296
+ "license": "ISC"
2297
+ },
2298
+ "node_modules/picomatch": {
2299
+ "version": "4.0.4",
2300
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
2301
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
2302
+ "dev": true,
2303
+ "license": "MIT",
2304
+ "engines": {
2305
+ "node": ">=12"
2306
+ },
2307
+ "funding": {
2308
+ "url": "https://github.com/sponsors/jonschlinkert"
2309
+ }
2310
+ },
2311
+ "node_modules/postcss": {
2312
+ "version": "8.5.15",
2313
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
2314
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
2315
+ "dev": true,
2316
+ "funding": [
2317
+ {
2318
+ "type": "opencollective",
2319
+ "url": "https://opencollective.com/postcss/"
2320
+ },
2321
+ {
2322
+ "type": "tidelift",
2323
+ "url": "https://tidelift.com/funding/github/npm/postcss"
2324
+ },
2325
+ {
2326
+ "type": "github",
2327
+ "url": "https://github.com/sponsors/ai"
2328
+ }
2329
+ ],
2330
+ "license": "MIT",
2331
+ "dependencies": {
2332
+ "nanoid": "^3.3.12",
2333
+ "picocolors": "^1.1.1",
2334
+ "source-map-js": "^1.2.1"
2335
+ },
2336
+ "engines": {
2337
+ "node": "^10 || ^12 || >=14"
2338
+ }
2339
+ },
2340
+ "node_modules/prelude-ls": {
2341
+ "version": "1.2.1",
2342
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
2343
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
2344
+ "dev": true,
2345
+ "license": "MIT",
2346
+ "engines": {
2347
+ "node": ">= 0.8.0"
2348
+ }
2349
+ },
2350
+ "node_modules/punycode": {
2351
+ "version": "2.3.1",
2352
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
2353
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
2354
+ "dev": true,
2355
+ "license": "MIT",
2356
+ "engines": {
2357
+ "node": ">=6"
2358
+ }
2359
+ },
2360
+ "node_modules/react": {
2361
+ "version": "19.2.6",
2362
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
2363
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
2364
+ "license": "MIT",
2365
+ "engines": {
2366
+ "node": ">=0.10.0"
2367
+ }
2368
+ },
2369
+ "node_modules/react-dom": {
2370
+ "version": "19.2.6",
2371
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
2372
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
2373
+ "license": "MIT",
2374
+ "dependencies": {
2375
+ "scheduler": "^0.27.0"
2376
+ },
2377
+ "peerDependencies": {
2378
+ "react": "^19.2.6"
2379
+ }
2380
+ },
2381
+ "node_modules/react-router": {
2382
+ "version": "7.15.1",
2383
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
2384
+ "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
2385
+ "license": "MIT",
2386
+ "dependencies": {
2387
+ "cookie": "^1.0.1",
2388
+ "set-cookie-parser": "^2.6.0"
2389
+ },
2390
+ "engines": {
2391
+ "node": ">=20.0.0"
2392
+ },
2393
+ "peerDependencies": {
2394
+ "react": ">=18",
2395
+ "react-dom": ">=18"
2396
+ },
2397
+ "peerDependenciesMeta": {
2398
+ "react-dom": {
2399
+ "optional": true
2400
+ }
2401
+ }
2402
+ },
2403
+ "node_modules/react-router-dom": {
2404
+ "version": "7.15.1",
2405
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
2406
+ "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
2407
+ "license": "MIT",
2408
+ "dependencies": {
2409
+ "react-router": "7.15.1"
2410
+ },
2411
+ "engines": {
2412
+ "node": ">=20.0.0"
2413
+ },
2414
+ "peerDependencies": {
2415
+ "react": ">=18",
2416
+ "react-dom": ">=18"
2417
+ }
2418
+ },
2419
+ "node_modules/rolldown": {
2420
+ "version": "1.0.1",
2421
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
2422
+ "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
2423
+ "dev": true,
2424
+ "license": "MIT",
2425
+ "dependencies": {
2426
+ "@oxc-project/types": "=0.130.0",
2427
+ "@rolldown/pluginutils": "^1.0.0"
2428
+ },
2429
+ "bin": {
2430
+ "rolldown": "bin/cli.mjs"
2431
+ },
2432
+ "engines": {
2433
+ "node": "^20.19.0 || >=22.12.0"
2434
+ },
2435
+ "optionalDependencies": {
2436
+ "@rolldown/binding-android-arm64": "1.0.1",
2437
+ "@rolldown/binding-darwin-arm64": "1.0.1",
2438
+ "@rolldown/binding-darwin-x64": "1.0.1",
2439
+ "@rolldown/binding-freebsd-x64": "1.0.1",
2440
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
2441
+ "@rolldown/binding-linux-arm64-gnu": "1.0.1",
2442
+ "@rolldown/binding-linux-arm64-musl": "1.0.1",
2443
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
2444
+ "@rolldown/binding-linux-s390x-gnu": "1.0.1",
2445
+ "@rolldown/binding-linux-x64-gnu": "1.0.1",
2446
+ "@rolldown/binding-linux-x64-musl": "1.0.1",
2447
+ "@rolldown/binding-openharmony-arm64": "1.0.1",
2448
+ "@rolldown/binding-wasm32-wasi": "1.0.1",
2449
+ "@rolldown/binding-win32-arm64-msvc": "1.0.1",
2450
+ "@rolldown/binding-win32-x64-msvc": "1.0.1"
2451
+ }
2452
+ },
2453
+ "node_modules/scheduler": {
2454
+ "version": "0.27.0",
2455
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2456
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
2457
+ "license": "MIT"
2458
+ },
2459
+ "node_modules/semver": {
2460
+ "version": "6.3.1",
2461
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2462
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2463
+ "dev": true,
2464
+ "license": "ISC",
2465
+ "bin": {
2466
+ "semver": "bin/semver.js"
2467
+ }
2468
+ },
2469
+ "node_modules/set-cookie-parser": {
2470
+ "version": "2.7.2",
2471
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
2472
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
2473
+ "license": "MIT"
2474
+ },
2475
+ "node_modules/shebang-command": {
2476
+ "version": "2.0.0",
2477
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2478
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2479
+ "dev": true,
2480
+ "license": "MIT",
2481
+ "dependencies": {
2482
+ "shebang-regex": "^3.0.0"
2483
+ },
2484
+ "engines": {
2485
+ "node": ">=8"
2486
+ }
2487
+ },
2488
+ "node_modules/shebang-regex": {
2489
+ "version": "3.0.0",
2490
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2491
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2492
+ "dev": true,
2493
+ "license": "MIT",
2494
+ "engines": {
2495
+ "node": ">=8"
2496
+ }
2497
+ },
2498
+ "node_modules/source-map-js": {
2499
+ "version": "1.2.1",
2500
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2501
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2502
+ "dev": true,
2503
+ "license": "BSD-3-Clause",
2504
+ "engines": {
2505
+ "node": ">=0.10.0"
2506
+ }
2507
+ },
2508
+ "node_modules/tinyglobby": {
2509
+ "version": "0.2.16",
2510
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
2511
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
2512
+ "dev": true,
2513
+ "license": "MIT",
2514
+ "dependencies": {
2515
+ "fdir": "^6.5.0",
2516
+ "picomatch": "^4.0.4"
2517
+ },
2518
+ "engines": {
2519
+ "node": ">=12.0.0"
2520
+ },
2521
+ "funding": {
2522
+ "url": "https://github.com/sponsors/SuperchupuDev"
2523
+ }
2524
+ },
2525
+ "node_modules/ts-api-utils": {
2526
+ "version": "2.5.0",
2527
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
2528
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
2529
+ "dev": true,
2530
+ "license": "MIT",
2531
+ "engines": {
2532
+ "node": ">=18.12"
2533
+ },
2534
+ "peerDependencies": {
2535
+ "typescript": ">=4.8.4"
2536
+ }
2537
+ },
2538
+ "node_modules/tslib": {
2539
+ "version": "2.8.1",
2540
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2541
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2542
+ "dev": true,
2543
+ "license": "0BSD",
2544
+ "optional": true
2545
+ },
2546
+ "node_modules/type-check": {
2547
+ "version": "0.4.0",
2548
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
2549
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
2550
+ "dev": true,
2551
+ "license": "MIT",
2552
+ "dependencies": {
2553
+ "prelude-ls": "^1.2.1"
2554
+ },
2555
+ "engines": {
2556
+ "node": ">= 0.8.0"
2557
+ }
2558
+ },
2559
+ "node_modules/typescript": {
2560
+ "version": "6.0.3",
2561
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
2562
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
2563
+ "dev": true,
2564
+ "license": "Apache-2.0",
2565
+ "bin": {
2566
+ "tsc": "bin/tsc",
2567
+ "tsserver": "bin/tsserver"
2568
+ },
2569
+ "engines": {
2570
+ "node": ">=14.17"
2571
+ }
2572
+ },
2573
+ "node_modules/typescript-eslint": {
2574
+ "version": "8.59.4",
2575
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz",
2576
+ "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==",
2577
+ "dev": true,
2578
+ "license": "MIT",
2579
+ "dependencies": {
2580
+ "@typescript-eslint/eslint-plugin": "8.59.4",
2581
+ "@typescript-eslint/parser": "8.59.4",
2582
+ "@typescript-eslint/typescript-estree": "8.59.4",
2583
+ "@typescript-eslint/utils": "8.59.4"
2584
+ },
2585
+ "engines": {
2586
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2587
+ },
2588
+ "funding": {
2589
+ "type": "opencollective",
2590
+ "url": "https://opencollective.com/typescript-eslint"
2591
+ },
2592
+ "peerDependencies": {
2593
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
2594
+ "typescript": ">=4.8.4 <6.1.0"
2595
+ }
2596
+ },
2597
+ "node_modules/undici-types": {
2598
+ "version": "7.16.0",
2599
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
2600
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
2601
+ "dev": true,
2602
+ "license": "MIT"
2603
+ },
2604
+ "node_modules/update-browserslist-db": {
2605
+ "version": "1.2.3",
2606
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
2607
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
2608
+ "dev": true,
2609
+ "funding": [
2610
+ {
2611
+ "type": "opencollective",
2612
+ "url": "https://opencollective.com/browserslist"
2613
+ },
2614
+ {
2615
+ "type": "tidelift",
2616
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2617
+ },
2618
+ {
2619
+ "type": "github",
2620
+ "url": "https://github.com/sponsors/ai"
2621
+ }
2622
+ ],
2623
+ "license": "MIT",
2624
+ "dependencies": {
2625
+ "escalade": "^3.2.0",
2626
+ "picocolors": "^1.1.1"
2627
+ },
2628
+ "bin": {
2629
+ "update-browserslist-db": "cli.js"
2630
+ },
2631
+ "peerDependencies": {
2632
+ "browserslist": ">= 4.21.0"
2633
+ }
2634
+ },
2635
+ "node_modules/uri-js": {
2636
+ "version": "4.4.1",
2637
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
2638
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
2639
+ "dev": true,
2640
+ "license": "BSD-2-Clause",
2641
+ "dependencies": {
2642
+ "punycode": "^2.1.0"
2643
+ }
2644
+ },
2645
+ "node_modules/vite": {
2646
+ "version": "8.0.13",
2647
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
2648
+ "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
2649
+ "dev": true,
2650
+ "license": "MIT",
2651
+ "dependencies": {
2652
+ "lightningcss": "^1.32.0",
2653
+ "picomatch": "^4.0.4",
2654
+ "postcss": "^8.5.14",
2655
+ "rolldown": "1.0.1",
2656
+ "tinyglobby": "^0.2.16"
2657
+ },
2658
+ "bin": {
2659
+ "vite": "bin/vite.js"
2660
+ },
2661
+ "engines": {
2662
+ "node": "^20.19.0 || >=22.12.0"
2663
+ },
2664
+ "funding": {
2665
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2666
+ },
2667
+ "optionalDependencies": {
2668
+ "fsevents": "~2.3.3"
2669
+ },
2670
+ "peerDependencies": {
2671
+ "@types/node": "^20.19.0 || >=22.12.0",
2672
+ "@vitejs/devtools": "^0.1.18",
2673
+ "esbuild": "^0.27.0 || ^0.28.0",
2674
+ "jiti": ">=1.21.0",
2675
+ "less": "^4.0.0",
2676
+ "sass": "^1.70.0",
2677
+ "sass-embedded": "^1.70.0",
2678
+ "stylus": ">=0.54.8",
2679
+ "sugarss": "^5.0.0",
2680
+ "terser": "^5.16.0",
2681
+ "tsx": "^4.8.1",
2682
+ "yaml": "^2.4.2"
2683
+ },
2684
+ "peerDependenciesMeta": {
2685
+ "@types/node": {
2686
+ "optional": true
2687
+ },
2688
+ "@vitejs/devtools": {
2689
+ "optional": true
2690
+ },
2691
+ "esbuild": {
2692
+ "optional": true
2693
+ },
2694
+ "jiti": {
2695
+ "optional": true
2696
+ },
2697
+ "less": {
2698
+ "optional": true
2699
+ },
2700
+ "sass": {
2701
+ "optional": true
2702
+ },
2703
+ "sass-embedded": {
2704
+ "optional": true
2705
+ },
2706
+ "stylus": {
2707
+ "optional": true
2708
+ },
2709
+ "sugarss": {
2710
+ "optional": true
2711
+ },
2712
+ "terser": {
2713
+ "optional": true
2714
+ },
2715
+ "tsx": {
2716
+ "optional": true
2717
+ },
2718
+ "yaml": {
2719
+ "optional": true
2720
+ }
2721
+ }
2722
+ },
2723
+ "node_modules/which": {
2724
+ "version": "2.0.2",
2725
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2726
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2727
+ "dev": true,
2728
+ "license": "ISC",
2729
+ "dependencies": {
2730
+ "isexe": "^2.0.0"
2731
+ },
2732
+ "bin": {
2733
+ "node-which": "bin/node-which"
2734
+ },
2735
+ "engines": {
2736
+ "node": ">= 8"
2737
+ }
2738
+ },
2739
+ "node_modules/word-wrap": {
2740
+ "version": "1.2.5",
2741
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
2742
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
2743
+ "dev": true,
2744
+ "license": "MIT",
2745
+ "engines": {
2746
+ "node": ">=0.10.0"
2747
+ }
2748
+ },
2749
+ "node_modules/yallist": {
2750
+ "version": "3.1.1",
2751
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2752
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2753
+ "dev": true,
2754
+ "license": "ISC"
2755
+ },
2756
+ "node_modules/yocto-queue": {
2757
+ "version": "0.1.0",
2758
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2759
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2760
+ "dev": true,
2761
+ "license": "MIT",
2762
+ "engines": {
2763
+ "node": ">=10"
2764
+ },
2765
+ "funding": {
2766
+ "url": "https://github.com/sponsors/sindresorhus"
2767
+ }
2768
+ },
2769
+ "node_modules/zod": {
2770
+ "version": "4.4.3",
2771
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
2772
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
2773
+ "dev": true,
2774
+ "license": "MIT",
2775
+ "funding": {
2776
+ "url": "https://github.com/sponsors/colinhacks"
2777
+ }
2778
+ },
2779
+ "node_modules/zod-validation-error": {
2780
+ "version": "4.0.2",
2781
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
2782
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
2783
+ "dev": true,
2784
+ "license": "MIT",
2785
+ "engines": {
2786
+ "node": ">=18.0.0"
2787
+ },
2788
+ "peerDependencies": {
2789
+ "zod": "^3.25.0 || ^4.0.0"
2790
+ }
2791
+ }
2792
+ }
2793
+ }
frontend/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.2.6",
14
+ "react-dom": "^19.2.6",
15
+ "react-router-dom": "^7.9.6"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^10.0.1",
19
+ "@types/node": "^24.12.3",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^10.3.0",
24
+ "eslint-plugin-react-hooks": "^7.1.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.6.0",
27
+ "typescript": "~6.0.2",
28
+ "typescript-eslint": "^8.59.2",
29
+ "vite": "^8.0.12"
30
+ }
31
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/src/App.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
2
+
3
+ import { DonePage } from "./routes/DonePage";
4
+ import { HomePage } from "./routes/HomePage";
5
+ import { PlayPage } from "./routes/PlayPage";
6
+
7
+ export default function App() {
8
+ return (
9
+ <BrowserRouter>
10
+ <Routes>
11
+ <Route path="/" element={<HomePage />} />
12
+ <Route path="/play/:sessionId" element={<PlayPage />} />
13
+ <Route path="/done/:sessionId" element={<DonePage />} />
14
+ </Routes>
15
+ </BrowserRouter>
16
+ );
17
+ }
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/PuzzleEditor.tsx ADDED
@@ -0,0 +1,866 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+
3
+ import type { PuzzleType } from "../lib/api";
4
+
5
+ type Props = {
6
+ puzzleType: PuzzleType;
7
+ problemAscii: string;
8
+ boardAscii: string;
9
+ onChange: (nextAscii: string) => void;
10
+ };
11
+
12
+ type BridgesOpportunity = {
13
+ id: string;
14
+ kind: "horizontal" | "vertical";
15
+ cells: Array<[number, number]>;
16
+ from: [number, number];
17
+ to: [number, number];
18
+ };
19
+
20
+ type LoopyEdge = {
21
+ id: string;
22
+ kind: "horizontal" | "vertical";
23
+ row: number;
24
+ col: number;
25
+ boardRow: number;
26
+ boardCol: number;
27
+ };
28
+
29
+ type GalaxyBoundary = {
30
+ id: string;
31
+ kind: "horizontal" | "vertical";
32
+ row: number;
33
+ col: number;
34
+ boardRow: number;
35
+ boardCol: number;
36
+ };
37
+
38
+ type GalaxyDot = {
39
+ rowCoord: number;
40
+ colCoord: number;
41
+ };
42
+
43
+ type PatternRow = {
44
+ prefix: string;
45
+ cells: string[];
46
+ border: string;
47
+ };
48
+
49
+ type UndeadRow = {
50
+ left: string;
51
+ cells: string[];
52
+ right: string;
53
+ };
54
+
55
+ const FLOW_FREE_COLORS = [
56
+ "#f15b5d",
57
+ "#f5a623",
58
+ "#f6df5a",
59
+ "#7ed957",
60
+ "#36cfc9",
61
+ "#4b8ef7",
62
+ "#7a5cff",
63
+ "#ff8bd1",
64
+ "#7d5a50",
65
+ "#5f6a6b",
66
+ ];
67
+
68
+ function splitLines(board: string): string[] {
69
+ return board.replace(/\r/g, "").split("\n");
70
+ }
71
+
72
+ function cloneGrid(lines: string[]): string[][] {
73
+ return lines.map((line) => [...line]);
74
+ }
75
+
76
+ function joinGrid(grid: string[][]): string {
77
+ return grid.map((row) => row.join("")).join("\n");
78
+ }
79
+
80
+ function padLines(lines: string[], width: number): string[] {
81
+ return lines.map((line) => line.padEnd(width, " "));
82
+ }
83
+
84
+ function isClueChar(char: string): boolean {
85
+ return /[0-9A-G]/.test(char);
86
+ }
87
+
88
+ function parseBridges(problemAscii: string) {
89
+ const lines = splitLines(problemAscii);
90
+ const opportunities: BridgesOpportunity[] = [];
91
+ const cellToOpportunity = new Map<string, BridgesOpportunity>();
92
+ for (let r = 0; r < lines.length; r += 1) {
93
+ for (let c = 0; c < lines[r].length; c += 1) {
94
+ if (!isClueChar(lines[r][c])) {
95
+ continue;
96
+ }
97
+
98
+ let next = c + 1;
99
+ while (next < lines[r].length && lines[r][next] === ".") {
100
+ next += 1;
101
+ }
102
+ if (next < lines[r].length && isClueChar(lines[r][next]) && next > c + 1) {
103
+ const opp: BridgesOpportunity = {
104
+ id: `h-${r}-${c}-${next}`,
105
+ kind: "horizontal",
106
+ cells: [],
107
+ from: [r, c],
108
+ to: [r, next],
109
+ };
110
+ for (let cell = c + 1; cell < next; cell += 1) {
111
+ opp.cells.push([r, cell]);
112
+ cellToOpportunity.set(`${r}:${cell}`, opp);
113
+ }
114
+ opportunities.push(opp);
115
+ }
116
+
117
+ let nextRow = r + 1;
118
+ while (nextRow < lines.length && c < lines[nextRow].length && lines[nextRow][c] === ".") {
119
+ nextRow += 1;
120
+ }
121
+ if (
122
+ nextRow < lines.length &&
123
+ c < lines[nextRow].length &&
124
+ isClueChar(lines[nextRow][c]) &&
125
+ nextRow > r + 1
126
+ ) {
127
+ const opp: BridgesOpportunity = {
128
+ id: `v-${r}-${c}-${nextRow}`,
129
+ kind: "vertical",
130
+ cells: [],
131
+ from: [r, c],
132
+ to: [nextRow, c],
133
+ };
134
+ for (let cell = r + 1; cell < nextRow; cell += 1) {
135
+ opp.cells.push([cell, c]);
136
+ cellToOpportunity.set(`${cell}:${c}`, opp);
137
+ }
138
+ opportunities.push(opp);
139
+ }
140
+ }
141
+ }
142
+ return { lines, opportunities, cellToOpportunity };
143
+ }
144
+
145
+ function bridgesBoardFromStates(
146
+ problemAscii: string,
147
+ states: Record<string, 0 | 1 | 2>,
148
+ ): string {
149
+ const parsed = parseBridges(problemAscii);
150
+ const grid = cloneGrid(parsed.lines);
151
+ for (const opportunity of parsed.opportunities) {
152
+ const level = states[opportunity.id] ?? 0;
153
+ const symbol =
154
+ opportunity.kind === "horizontal"
155
+ ? level === 2
156
+ ? "="
157
+ : level === 1
158
+ ? "-"
159
+ : "."
160
+ : level === 2
161
+ ? '"'
162
+ : level === 1
163
+ ? "|"
164
+ : ".";
165
+ for (const [r, c] of opportunity.cells) {
166
+ grid[r][c] = symbol;
167
+ }
168
+ }
169
+ return joinGrid(grid);
170
+ }
171
+
172
+ function bridgesStatesFromBoard(
173
+ problemAscii: string,
174
+ boardAscii: string,
175
+ ): Record<string, 0 | 1 | 2> {
176
+ const parsed = parseBridges(problemAscii);
177
+ const boardLines = splitLines(boardAscii);
178
+ const states: Record<string, 0 | 1 | 2> = {};
179
+ for (const opportunity of parsed.opportunities) {
180
+ const [r, c] = opportunity.cells[0];
181
+ const char = boardLines[r]?.[c] ?? ".";
182
+ if (opportunity.kind === "horizontal") {
183
+ states[opportunity.id] = char === "=" ? 2 : char === "-" ? 1 : 0;
184
+ } else {
185
+ states[opportunity.id] = char === '"' ? 2 : char === "|" ? 1 : 0;
186
+ }
187
+ }
188
+ return states;
189
+ }
190
+
191
+ function parseLoopy(problemAscii: string) {
192
+ const lines = splitLines(problemAscii);
193
+ const rows = Math.max(0, Math.floor((lines.length - 3) / 2));
194
+ const cols = Math.max(0, Math.floor(((lines[0]?.length ?? 0) - 3) / 2));
195
+ const clues = new Map<string, string>();
196
+ const horizontalEdges: LoopyEdge[] = [];
197
+ const verticalEdges: LoopyEdge[] = [];
198
+
199
+ for (let r = 0; r < rows; r += 1) {
200
+ for (let c = 0; c < cols; c += 1) {
201
+ const char = lines[2 + 2 * r]?.[2 + 2 * c] ?? " ";
202
+ if (/\d/.test(char)) {
203
+ clues.set(`${r}:${c}`, char);
204
+ }
205
+ }
206
+ }
207
+
208
+ for (let r = 0; r <= rows; r += 1) {
209
+ for (let c = 0; c < cols; c += 1) {
210
+ horizontalEdges.push({
211
+ id: `h-${r}-${c}`,
212
+ kind: "horizontal",
213
+ row: r,
214
+ col: c,
215
+ boardRow: 1 + 2 * r,
216
+ boardCol: 2 + 2 * c,
217
+ });
218
+ }
219
+ }
220
+
221
+ for (let r = 0; r < rows; r += 1) {
222
+ for (let c = 0; c <= cols; c += 1) {
223
+ verticalEdges.push({
224
+ id: `v-${r}-${c}`,
225
+ kind: "vertical",
226
+ row: r,
227
+ col: c,
228
+ boardRow: 2 + 2 * r,
229
+ boardCol: 1 + 2 * c,
230
+ });
231
+ }
232
+ }
233
+
234
+ return { lines, rows, cols, clues, horizontalEdges, verticalEdges };
235
+ }
236
+
237
+ function loopyEdgeStates(
238
+ problemAscii: string,
239
+ boardAscii: string,
240
+ ): Record<string, "unknown" | "line" | "blocked"> {
241
+ const puzzle = parseLoopy(problemAscii);
242
+ const width = puzzle.lines[0]?.length ?? 0;
243
+ const boardLines = padLines(splitLines(boardAscii), width);
244
+ const states: Record<string, "unknown" | "line" | "blocked"> = {};
245
+
246
+ for (const edge of [...puzzle.horizontalEdges, ...puzzle.verticalEdges]) {
247
+ const char = boardLines[edge.boardRow]?.[edge.boardCol] ?? " ";
248
+ states[edge.id] = char === "-" || char === "|" ? "line" : char === "x" ? "blocked" : "unknown";
249
+ }
250
+ return states;
251
+ }
252
+
253
+ function loopyBoardFromStates(
254
+ problemAscii: string,
255
+ states: Record<string, "unknown" | "line" | "blocked">,
256
+ ): string {
257
+ const puzzle = parseLoopy(problemAscii);
258
+ const width = puzzle.lines[0]?.length ?? 0;
259
+ const grid = cloneGrid(padLines(puzzle.lines, width));
260
+
261
+ for (const edge of puzzle.horizontalEdges) {
262
+ const state = states[edge.id] ?? "unknown";
263
+ grid[edge.boardRow][edge.boardCol] = state === "line" ? "-" : state === "blocked" ? "x" : " ";
264
+ }
265
+ for (const edge of puzzle.verticalEdges) {
266
+ const state = states[edge.id] ?? "unknown";
267
+ grid[edge.boardRow][edge.boardCol] = state === "line" ? "|" : state === "blocked" ? "x" : " ";
268
+ }
269
+
270
+ return joinGrid(grid);
271
+ }
272
+
273
+ function parseGalaxies(problemAscii: string) {
274
+ const lines = splitLines(problemAscii);
275
+ const rows = Math.max(0, Math.floor(((lines.length ?? 0) - 1) / 2));
276
+ const cols = Math.max(0, Math.floor(((lines[0]?.length ?? 0) - 1) / 2));
277
+ const horizontalBoundaries: GalaxyBoundary[] = [];
278
+ const verticalBoundaries: GalaxyBoundary[] = [];
279
+ const dots: GalaxyDot[] = [];
280
+
281
+ for (let boardRow = 0; boardRow < lines.length; boardRow += 1) {
282
+ const line = lines[boardRow] ?? "";
283
+ for (let boardCol = 0; boardCol < line.length; boardCol += 1) {
284
+ if (line[boardCol] === "o") {
285
+ dots.push({ rowCoord: boardRow, colCoord: boardCol });
286
+ }
287
+ }
288
+ }
289
+
290
+ for (let r = 1; r < rows; r += 1) {
291
+ for (let c = 0; c < cols; c += 1) {
292
+ horizontalBoundaries.push({
293
+ id: `h-${r}-${c}`,
294
+ kind: "horizontal",
295
+ row: r,
296
+ col: c,
297
+ boardRow: 2 * r,
298
+ boardCol: 1 + 2 * c,
299
+ });
300
+ }
301
+ }
302
+
303
+ for (let r = 0; r < rows; r += 1) {
304
+ for (let c = 1; c < cols; c += 1) {
305
+ verticalBoundaries.push({
306
+ id: `v-${r}-${c}`,
307
+ kind: "vertical",
308
+ row: r,
309
+ col: c,
310
+ boardRow: 1 + 2 * r,
311
+ boardCol: 2 * c,
312
+ });
313
+ }
314
+ }
315
+
316
+ return { lines, rows, cols, horizontalBoundaries, verticalBoundaries, dots };
317
+ }
318
+
319
+ function galaxiesBoundaryStates(
320
+ problemAscii: string,
321
+ boardAscii: string,
322
+ ): Record<string, boolean> {
323
+ const puzzle = parseGalaxies(problemAscii);
324
+ const width = puzzle.lines[0]?.length ?? 0;
325
+ const boardLines = padLines(splitLines(boardAscii), width);
326
+ const states: Record<string, boolean> = {};
327
+ for (const boundary of [...puzzle.horizontalBoundaries, ...puzzle.verticalBoundaries]) {
328
+ const char = boardLines[boundary.boardRow]?.[boundary.boardCol] ?? " ";
329
+ states[boundary.id] = char === "-" || char === "|";
330
+ }
331
+ return states;
332
+ }
333
+
334
+ function galaxiesBoardFromStates(problemAscii: string, states: Record<string, boolean>): string {
335
+ const puzzle = parseGalaxies(problemAscii);
336
+ const width = puzzle.lines[0]?.length ?? 0;
337
+ const grid = cloneGrid(padLines(puzzle.lines, width));
338
+
339
+ for (const boundary of puzzle.horizontalBoundaries) {
340
+ grid[boundary.boardRow][boundary.boardCol] = states[boundary.id] ? "-" : " ";
341
+ }
342
+ for (const boundary of puzzle.verticalBoundaries) {
343
+ grid[boundary.boardRow][boundary.boardCol] = states[boundary.id] ? "|" : " ";
344
+ }
345
+
346
+ return joinGrid(grid);
347
+ }
348
+
349
+ function parsePattern(boardAscii: string) {
350
+ const lines = splitLines(boardAscii);
351
+ const firstContentIndex = lines.findIndex((line) => line.includes("|"));
352
+ const topLines = lines.slice(0, firstContentIndex);
353
+ const rows: PatternRow[] = [];
354
+ for (let idx = firstContentIndex; idx < lines.length; idx += 2) {
355
+ const content = lines[idx];
356
+ const border = lines[idx + 1];
357
+ if (!content || !border) {
358
+ break;
359
+ }
360
+ const firstBar = content.indexOf("|");
361
+ const prefix = content.slice(0, firstBar);
362
+ const parts = content.slice(firstBar).split("|").slice(1, -1);
363
+ rows.push({ prefix, cells: parts, border });
364
+ }
365
+ return { topLines, rows };
366
+ }
367
+
368
+ function patternToAscii(template: ReturnType<typeof parsePattern>): string {
369
+ const lines = [...template.topLines];
370
+ for (const row of template.rows) {
371
+ lines.push(`${row.prefix}|${row.cells.join("|")}|`);
372
+ lines.push(row.border);
373
+ }
374
+ return lines.join("\n");
375
+ }
376
+
377
+ function parseUndead(boardAscii: string) {
378
+ const lines = splitLines(boardAscii);
379
+ const header = lines[0] ?? "";
380
+ const topLine = lines[2] ?? "";
381
+ const bottomLine = lines.at(-1) ?? "";
382
+ const rows: UndeadRow[] = lines.slice(3, -1).map((line) => {
383
+ const tokens = line.trim().split(/\s+/);
384
+ return {
385
+ left: tokens[0] ?? "",
386
+ cells: tokens.slice(1, -1),
387
+ right: tokens.at(-1) ?? "",
388
+ };
389
+ });
390
+ return { header, topLine, bottomLine, rows };
391
+ }
392
+
393
+ function undeadToAscii(template: ReturnType<typeof parseUndead>): string {
394
+ const lines = [template.header, "", template.topLine];
395
+ for (const row of template.rows) {
396
+ lines.push(` ${row.left} ${row.cells.join(" ")} ${row.right}`);
397
+ }
398
+ lines.push(template.bottomLine);
399
+ return lines.join("\n");
400
+ }
401
+
402
+ function legendColor(letter: string) {
403
+ return FLOW_FREE_COLORS[(letter.charCodeAt(0) - 65) % FLOW_FREE_COLORS.length];
404
+ }
405
+
406
+ function BridgesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
407
+ const parsed = parseBridges(problemAscii);
408
+ const states = bridgesStatesFromBoard(problemAscii, boardAscii);
409
+
410
+ function cycle(opportunity: BridgesOpportunity) {
411
+ const nextStates = { ...states };
412
+ const current = nextStates[opportunity.id] ?? 0;
413
+ nextStates[opportunity.id] = ((current + 1) % 3) as 0 | 1 | 2;
414
+ onChange(bridgesBoardFromStates(problemAscii, nextStates));
415
+ }
416
+
417
+ return (
418
+ <div className="editor-stack">
419
+ <div className="puzzle-note">Click a route between islands to cycle no bridge, single bridge, and double bridge.</div>
420
+ <div
421
+ className="board-grid bridges-grid"
422
+ style={{ gridTemplateColumns: `repeat(${parsed.lines[0]?.length ?? 0}, 40px)` }}
423
+ >
424
+ {parsed.lines.flatMap((line, r) =>
425
+ [...line].map((char, c) => {
426
+ const opportunity = parsed.cellToOpportunity.get(`${r}:${c}`);
427
+ const boardChar = splitLines(boardAscii)[r]?.[c] ?? char;
428
+ const className = isClueChar(char)
429
+ ? "board-cell fixed island-cell"
430
+ : opportunity
431
+ ? "board-cell bridge-cell"
432
+ : "board-cell water";
433
+ const display =
434
+ boardChar === "." ? "" : boardChar === '"' ? "‖" : boardChar === "=" ? "═" : boardChar;
435
+ return (
436
+ <button
437
+ key={`${r}-${c}`}
438
+ type="button"
439
+ className={className}
440
+ onClick={() => opportunity && cycle(opportunity)}
441
+ disabled={!opportunity}
442
+ >
443
+ {isClueChar(char) ? char : display}
444
+ </button>
445
+ );
446
+ }),
447
+ )}
448
+ </div>
449
+ </div>
450
+ );
451
+ }
452
+
453
+ function FlowFreeEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
454
+ const problemLines = splitLines(problemAscii);
455
+ const boardLines = splitLines(boardAscii);
456
+ const letters = [...new Set(problemAscii.replace(/[^A-Z]/g, "").split(""))].sort();
457
+ const [activeColor, setActiveColor] = useState(letters[0] ?? "A");
458
+
459
+ function updateCell(r: number, c: number) {
460
+ if (problemLines[r][c] !== ".") {
461
+ setActiveColor(problemLines[r][c]);
462
+ return;
463
+ }
464
+ const grid = cloneGrid(boardLines);
465
+ grid[r][c] = grid[r][c] === activeColor ? "." : activeColor;
466
+ onChange(joinGrid(grid));
467
+ }
468
+
469
+ return (
470
+ <div className="editor-stack">
471
+ <div className="puzzle-note">Pick a color, then fill open cells. Clicking an endpoint picks that color.</div>
472
+ <div className="palette">
473
+ {letters.map((letter) => (
474
+ <button
475
+ key={letter}
476
+ type="button"
477
+ className={activeColor === letter ? "active" : ""}
478
+ style={{ background: legendColor(letter) }}
479
+ onClick={() => setActiveColor(letter)}
480
+ >
481
+ {letter}
482
+ </button>
483
+ ))}
484
+ <button
485
+ type="button"
486
+ className={activeColor === "." ? "active erase-swatch" : "erase-swatch"}
487
+ onClick={() => setActiveColor(".")}
488
+ >
489
+ Erase
490
+ </button>
491
+ </div>
492
+ <div
493
+ className="board-grid flow-grid"
494
+ style={{ gridTemplateColumns: `repeat(${problemLines[0]?.length ?? 0}, 44px)` }}
495
+ >
496
+ {boardLines.flatMap((line, r) =>
497
+ [...line].map((char, c) => {
498
+ const fixed = problemLines[r][c] !== ".";
499
+ const letter = fixed ? problemLines[r][c] : char;
500
+ const color = letter === "." ? "transparent" : legendColor(letter);
501
+ return (
502
+ <button
503
+ key={`${r}-${c}`}
504
+ type="button"
505
+ className={`board-cell flow-cell ${fixed ? "fixed endpoint-cell" : "water"}`}
506
+ style={{ background: letter === "." ? undefined : `${color}${fixed ? "" : "d8"}` }}
507
+ onClick={() => updateCell(r, c)}
508
+ >
509
+ {letter === "." ? "" : letter}
510
+ </button>
511
+ );
512
+ }),
513
+ )}
514
+ </div>
515
+ </div>
516
+ );
517
+ }
518
+
519
+ function LoopyEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
520
+ const puzzle = parseLoopy(problemAscii);
521
+ const states = loopyEdgeStates(problemAscii, boardAscii);
522
+ const cell = 52;
523
+ const margin = 28;
524
+ const width = margin * 2 + puzzle.cols * cell;
525
+ const height = margin * 2 + puzzle.rows * cell;
526
+
527
+ function cycle(edge: LoopyEdge) {
528
+ const current = states[edge.id] ?? "unknown";
529
+ const next =
530
+ current === "unknown" ? "line" : current === "line" ? "blocked" : "unknown";
531
+ onChange(
532
+ loopyBoardFromStates(problemAscii, {
533
+ ...states,
534
+ [edge.id]: next,
535
+ }),
536
+ );
537
+ }
538
+
539
+ return (
540
+ <div className="editor-stack">
541
+ <div className="puzzle-note">Click any segment, including the outer border, to cycle blank, line, and blocked.</div>
542
+ <div className="svg-board-shell">
543
+ <svg className="svg-board" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="Loopy board">
544
+ {Array.from({ length: puzzle.rows + 1 }).map((_, row) =>
545
+ Array.from({ length: puzzle.cols + 1 }).map((__, col) => (
546
+ <circle
547
+ key={`dot-${row}-${col}`}
548
+ cx={margin + col * cell}
549
+ cy={margin + row * cell}
550
+ r={4.25}
551
+ className="svg-vertex"
552
+ />
553
+ )),
554
+ )}
555
+
556
+ {Array.from({ length: puzzle.rows }).map((_, row) =>
557
+ Array.from({ length: puzzle.cols }).map((__, col) => (
558
+ <g key={`clue-${row}-${col}`}>
559
+ <rect
560
+ x={margin + col * cell + 8}
561
+ y={margin + row * cell + 8}
562
+ width={cell - 16}
563
+ height={cell - 16}
564
+ rx={12}
565
+ className="svg-cell-bg"
566
+ />
567
+ {puzzle.clues.has(`${row}:${col}`) ? (
568
+ <text
569
+ x={margin + col * cell + cell / 2}
570
+ y={margin + row * cell + cell / 2 + 8}
571
+ className="svg-clue-text"
572
+ textAnchor="middle"
573
+ >
574
+ {puzzle.clues.get(`${row}:${col}`)}
575
+ </text>
576
+ ) : null}
577
+ </g>
578
+ )),
579
+ )}
580
+
581
+ {puzzle.horizontalEdges.map((edge) => {
582
+ const x1 = margin + edge.col * cell;
583
+ const y = margin + edge.row * cell;
584
+ const x2 = x1 + cell;
585
+ const state = states[edge.id] ?? "unknown";
586
+ return (
587
+ <g key={edge.id} className="svg-hit-target" onClick={() => cycle(edge)}>
588
+ <line x1={x1} y1={y} x2={x2} y2={y} className="svg-hit-line" />
589
+ {state === "line" ? <line x1={x1} y1={y} x2={x2} y2={y} className="svg-line active" /> : null}
590
+ {state === "unknown" ? <line x1={x1} y1={y} x2={x2} y2={y} className="svg-line ghost" /> : null}
591
+ {state === "blocked" ? (
592
+ <text x={(x1 + x2) / 2} y={y + 6} className="svg-xmark" textAnchor="middle">
593
+ ×
594
+ </text>
595
+ ) : null}
596
+ </g>
597
+ );
598
+ })}
599
+
600
+ {puzzle.verticalEdges.map((edge) => {
601
+ const x = margin + edge.col * cell;
602
+ const y1 = margin + edge.row * cell;
603
+ const y2 = y1 + cell;
604
+ const state = states[edge.id] ?? "unknown";
605
+ return (
606
+ <g key={edge.id} className="svg-hit-target" onClick={() => cycle(edge)}>
607
+ <line x1={x} y1={y1} x2={x} y2={y2} className="svg-hit-line" />
608
+ {state === "line" ? <line x1={x} y1={y1} x2={x} y2={y2} className="svg-line active" /> : null}
609
+ {state === "unknown" ? <line x1={x} y1={y1} x2={x} y2={y2} className="svg-line ghost" /> : null}
610
+ {state === "blocked" ? (
611
+ <text x={x} y={(y1 + y2) / 2 + 6} className="svg-xmark" textAnchor="middle">
612
+ ×
613
+ </text>
614
+ ) : null}
615
+ </g>
616
+ );
617
+ })}
618
+ </svg>
619
+ </div>
620
+ </div>
621
+ );
622
+ }
623
+
624
+ function GalaxiesEditor({ problemAscii, boardAscii, onChange }: Omit<Props, "puzzleType">) {
625
+ const puzzle = parseGalaxies(problemAscii);
626
+ const states = galaxiesBoundaryStates(problemAscii, boardAscii);
627
+ const cell = 56;
628
+ const half = cell / 2;
629
+ const margin = 28;
630
+ const width = margin * 2 + puzzle.cols * cell;
631
+ const height = margin * 2 + puzzle.rows * cell;
632
+
633
+ function toggle(boundary: GalaxyBoundary) {
634
+ onChange(
635
+ galaxiesBoardFromStates(problemAscii, {
636
+ ...states,
637
+ [boundary.id]: !states[boundary.id],
638
+ }),
639
+ );
640
+ }
641
+
642
+ return (
643
+ <div className="editor-stack">
644
+ <div className="puzzle-note">Click an interior wall segment to add or remove it. Dots mark each galaxy center.</div>
645
+ <div className="svg-board-shell">
646
+ <svg className="svg-board" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="Galaxies board">
647
+ {Array.from({ length: puzzle.rows }).map((_, row) =>
648
+ Array.from({ length: puzzle.cols }).map((__, col) => (
649
+ <rect
650
+ key={`cell-${row}-${col}`}
651
+ x={margin + col * cell}
652
+ y={margin + row * cell}
653
+ width={cell}
654
+ height={cell}
655
+ rx={14}
656
+ className="galaxy-cell-bg"
657
+ />
658
+ )),
659
+ )}
660
+
661
+ {Array.from({ length: puzzle.rows + 1 }).map((_, row) =>
662
+ Array.from({ length: puzzle.cols + 1 }).map((__, col) => (
663
+ <circle
664
+ key={`vertex-${row}-${col}`}
665
+ cx={margin + col * cell}
666
+ cy={margin + row * cell}
667
+ r={3.6}
668
+ className="svg-vertex"
669
+ />
670
+ )),
671
+ )}
672
+
673
+ {Array.from({ length: puzzle.rows + 1 }).map((_, row) =>
674
+ Array.from({ length: puzzle.cols }).map((__, col) => (
675
+ <line
676
+ key={`outer-h-${row}-${col}`}
677
+ x1={margin + col * cell}
678
+ y1={margin + row * cell}
679
+ x2={margin + (col + 1) * cell}
680
+ y2={margin + row * cell}
681
+ className={`svg-wall ${row === 0 || row === puzzle.rows ? "fixed" : "ghost"}`}
682
+ />
683
+ )),
684
+ )}
685
+
686
+ {Array.from({ length: puzzle.rows }).map((_, row) =>
687
+ Array.from({ length: puzzle.cols + 1 }).map((__, col) => (
688
+ <line
689
+ key={`outer-v-${row}-${col}`}
690
+ x1={margin + col * cell}
691
+ y1={margin + row * cell}
692
+ x2={margin + col * cell}
693
+ y2={margin + (row + 1) * cell}
694
+ className={`svg-wall ${col === 0 || col === puzzle.cols ? "fixed" : "ghost"}`}
695
+ />
696
+ )),
697
+ )}
698
+
699
+ {puzzle.horizontalBoundaries.map((boundary) => {
700
+ const x1 = margin + boundary.col * cell;
701
+ const y = margin + boundary.row * cell;
702
+ const x2 = x1 + cell;
703
+ return (
704
+ <g key={boundary.id} className="svg-hit-target" onClick={() => toggle(boundary)}>
705
+ <line x1={x1} y1={y} x2={x2} y2={y} className="svg-hit-line" />
706
+ <line x1={x1} y1={y} x2={x2} y2={y} className={`svg-wall ${states[boundary.id] ? "active" : "ghost"}`} />
707
+ </g>
708
+ );
709
+ })}
710
+
711
+ {puzzle.verticalBoundaries.map((boundary) => {
712
+ const x = margin + boundary.col * cell;
713
+ const y1 = margin + boundary.row * cell;
714
+ const y2 = y1 + cell;
715
+ return (
716
+ <g key={boundary.id} className="svg-hit-target" onClick={() => toggle(boundary)}>
717
+ <line x1={x} y1={y1} x2={x} y2={y2} className="svg-hit-line" />
718
+ <line x1={x} y1={y1} x2={x} y2={y2} className={`svg-wall ${states[boundary.id] ? "active" : "ghost"}`} />
719
+ </g>
720
+ );
721
+ })}
722
+
723
+ {puzzle.dots.map((dot) => (
724
+ <circle
725
+ key={`dot-${dot.rowCoord}-${dot.colCoord}`}
726
+ cx={margin + dot.colCoord * half}
727
+ cy={margin + dot.rowCoord * half}
728
+ r={7}
729
+ className="galaxy-dot"
730
+ />
731
+ ))}
732
+ </svg>
733
+ </div>
734
+ </div>
735
+ );
736
+ }
737
+
738
+ function PatternEditor({ boardAscii, onChange }: Omit<Props, "puzzleType">) {
739
+ const template = parsePattern(boardAscii);
740
+ const columnClues = template.topLines.map((line) => line.trimEnd()).join("\n");
741
+
742
+ function cycle(rowIndex: number, cellIndex: number) {
743
+ const next = parsePattern(boardAscii);
744
+ const current = next.rows[rowIndex].cells[cellIndex];
745
+ next.rows[rowIndex].cells[cellIndex] =
746
+ current === " " ? "##" : current === "##" ? ".." : " ";
747
+ onChange(patternToAscii(next));
748
+ }
749
+
750
+ return (
751
+ <div className="editor-stack">
752
+ <div className="puzzle-note">Click a square to cycle unknown, filled, and empty.</div>
753
+ <div className="pattern-layout">
754
+ <pre className="ascii-preview compact-preview">{columnClues}</pre>
755
+ <div className="pattern-grid">
756
+ {template.rows.map((row, rowIndex) => (
757
+ <div key={rowIndex} className="pattern-row">
758
+ <div className="pattern-clue">{row.prefix.trim()}</div>
759
+ <div className="board-grid pattern-board" style={{ gridTemplateColumns: `repeat(${row.cells.length}, 46px)` }}>
760
+ {row.cells.map((cell, cellIndex) => (
761
+ <button
762
+ key={`${rowIndex}-${cellIndex}`}
763
+ type="button"
764
+ className={`board-square ${cell === "##" ? "fill" : cell === ".." ? "empty" : "unknown"}`}
765
+ onClick={() => cycle(rowIndex, cellIndex)}
766
+ >
767
+ {cell === "##" ? "■" : cell === ".." ? "·" : ""}
768
+ </button>
769
+ ))}
770
+ </div>
771
+ </div>
772
+ ))}
773
+ </div>
774
+ </div>
775
+ </div>
776
+ );
777
+ }
778
+
779
+ function UndeadEditor({ boardAscii, onChange }: Omit<Props, "puzzleType">) {
780
+ const template = parseUndead(boardAscii);
781
+ const topClues = template.topLine.trim().split(/\s+/);
782
+ const bottomClues = template.bottomLine.trim().split(/\s+/);
783
+ const boardWidth = template.rows[0]?.cells.length ?? 0;
784
+ const rowTemplate = `40px repeat(${boardWidth}, 38px) 40px`;
785
+
786
+ function cycle(rowIndex: number, cellIndex: number) {
787
+ const current = template.rows[rowIndex].cells[cellIndex];
788
+ if (current === "/" || current === "\\") {
789
+ return;
790
+ }
791
+ const next = parseUndead(boardAscii);
792
+ next.rows[rowIndex].cells[cellIndex] =
793
+ current === "." ? "G" : current === "G" ? "V" : current === "V" ? "Z" : ".";
794
+ onChange(undeadToAscii(next));
795
+ }
796
+
797
+ return (
798
+ <div className="editor-stack">
799
+ <div className="puzzle-note">Click a cell to cycle ghost, vampire, zombie, and blank. Mirror cells are fixed.</div>
800
+ <div className="undead-legend">{template.header}</div>
801
+ <div className="undead-grid-wrap">
802
+ <div className="undead-clue-row" style={{ gridTemplateColumns: rowTemplate }}>
803
+ <div className="undead-clue-spacer" />
804
+ {topClues.map((clue, index) => (
805
+ <div key={`top-${index}`} className="undead-clue-box">
806
+ {clue}
807
+ </div>
808
+ ))}
809
+ <div className="undead-clue-spacer" />
810
+ </div>
811
+
812
+ {template.rows.map((row, rowIndex) => (
813
+ <div key={rowIndex} className="undead-clue-row" style={{ gridTemplateColumns: rowTemplate }}>
814
+ <div className="undead-clue-box side">{row.left}</div>
815
+ {row.cells.map((cell, cellIndex) => {
816
+ const fixedMirror = cell === "/" || cell === "\\";
817
+ return (
818
+ <button
819
+ key={`${rowIndex}-${cellIndex}`}
820
+ type="button"
821
+ className={`board-cell undead-cell ${fixedMirror ? "mirror" : "monster-cell"}`}
822
+ onClick={() => cycle(rowIndex, cellIndex)}
823
+ >
824
+ {cell === "." ? "" : cell}
825
+ </button>
826
+ );
827
+ })}
828
+ <div className="undead-clue-box side">{row.right}</div>
829
+ </div>
830
+ ))}
831
+
832
+ <div className="undead-clue-row" style={{ gridTemplateColumns: rowTemplate }}>
833
+ <div className="undead-clue-spacer" />
834
+ {bottomClues.map((clue, index) => (
835
+ <div key={`bottom-${index}`} className="undead-clue-box">
836
+ {clue}
837
+ </div>
838
+ ))}
839
+ <div className="undead-clue-spacer" />
840
+ </div>
841
+ </div>
842
+ </div>
843
+ );
844
+ }
845
+
846
+ export function PuzzleEditor(props: Props) {
847
+ if (props.puzzleType === "bridges") {
848
+ return <BridgesEditor {...props} />;
849
+ }
850
+ if (props.puzzleType === "flow_free") {
851
+ return <FlowFreeEditor {...props} />;
852
+ }
853
+ if (props.puzzleType === "galaxies") {
854
+ return <GalaxiesEditor {...props} />;
855
+ }
856
+ if (props.puzzleType === "loopy") {
857
+ return <LoopyEditor {...props} />;
858
+ }
859
+ if (props.puzzleType === "pattern") {
860
+ return <PatternEditor {...props} />;
861
+ }
862
+ if (props.puzzleType === "undead") {
863
+ return <UndeadEditor {...props} />;
864
+ }
865
+ return null;
866
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,652 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color: #13231d;
3
+ background:
4
+ radial-gradient(circle at top left, rgba(235, 242, 168, 0.45), transparent 34%),
5
+ radial-gradient(circle at bottom right, rgba(117, 205, 161, 0.28), transparent 28%),
6
+ linear-gradient(180deg, #f7f7f0 0%, #ecf2ea 100%);
7
+ font-family:
8
+ "IBM Plex Sans",
9
+ "Avenir Next",
10
+ "Segoe UI",
11
+ sans-serif;
12
+ line-height: 1.4;
13
+ font-weight: 400;
14
+ font-synthesis: none;
15
+ text-rendering: optimizeLegibility;
16
+ -webkit-font-smoothing: antialiased;
17
+ -moz-osx-font-smoothing: grayscale;
18
+ }
19
+
20
+ * {
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ margin: 0;
26
+ min-width: 320px;
27
+ min-height: 100vh;
28
+ }
29
+
30
+ button,
31
+ input,
32
+ select,
33
+ textarea {
34
+ font: inherit;
35
+ }
36
+
37
+ button {
38
+ cursor: pointer;
39
+ }
40
+
41
+ a {
42
+ color: inherit;
43
+ }
44
+
45
+ .shell {
46
+ min-height: 100vh;
47
+ padding: 24px;
48
+ }
49
+
50
+ .panel {
51
+ border: 1px solid rgba(18, 49, 41, 0.14);
52
+ border-radius: 24px;
53
+ background: rgba(255, 255, 255, 0.82);
54
+ box-shadow:
55
+ 0 20px 50px rgba(31, 51, 42, 0.08),
56
+ inset 0 1px 0 rgba(255, 255, 255, 0.7);
57
+ backdrop-filter: blur(18px);
58
+ }
59
+
60
+ .home-layout {
61
+ display: grid;
62
+ gap: 24px;
63
+ max-width: 1100px;
64
+ margin: 0 auto;
65
+ }
66
+
67
+ .hero {
68
+ padding: 40px;
69
+ display: grid;
70
+ gap: 16px;
71
+ }
72
+
73
+ .eyebrow {
74
+ font-size: 0.78rem;
75
+ letter-spacing: 0.14em;
76
+ text-transform: uppercase;
77
+ color: #42685a;
78
+ }
79
+
80
+ .hero h1 {
81
+ margin: 0;
82
+ font-family:
83
+ "Iowan Old Style",
84
+ "Palatino Linotype",
85
+ serif;
86
+ font-size: clamp(2.4rem, 6vw, 4.8rem);
87
+ line-height: 0.95;
88
+ max-width: 9ch;
89
+ }
90
+
91
+ .hero p {
92
+ margin: 0;
93
+ max-width: 65ch;
94
+ color: #365145;
95
+ font-size: 1.02rem;
96
+ }
97
+
98
+ .hero-grid {
99
+ display: grid;
100
+ gap: 24px;
101
+ }
102
+
103
+ .card {
104
+ padding: 28px;
105
+ }
106
+
107
+ .form-grid {
108
+ display: grid;
109
+ gap: 18px;
110
+ }
111
+
112
+ .field {
113
+ display: grid;
114
+ gap: 8px;
115
+ }
116
+
117
+ .field label {
118
+ font-size: 0.92rem;
119
+ font-weight: 600;
120
+ color: #29463b;
121
+ }
122
+
123
+ .field input,
124
+ .field select,
125
+ .ascii-preview {
126
+ width: 100%;
127
+ border: 1px solid rgba(28, 55, 45, 0.16);
128
+ border-radius: 16px;
129
+ background: rgba(255, 255, 255, 0.94);
130
+ padding: 14px 16px;
131
+ color: #13231d;
132
+ }
133
+
134
+ .primary-button,
135
+ .secondary-button,
136
+ .ghost-button {
137
+ border: none;
138
+ border-radius: 999px;
139
+ padding: 14px 22px;
140
+ font-weight: 700;
141
+ transition:
142
+ transform 140ms ease,
143
+ box-shadow 140ms ease,
144
+ opacity 140ms ease;
145
+ }
146
+
147
+ .primary-button {
148
+ color: #fcfff8;
149
+ background: linear-gradient(135deg, #154f3a, #2f7f63);
150
+ box-shadow: 0 12px 28px rgba(28, 77, 59, 0.25);
151
+ }
152
+
153
+ .secondary-button {
154
+ color: #12362c;
155
+ background: #dcefe5;
156
+ }
157
+
158
+ .ghost-button {
159
+ color: #285144;
160
+ background: transparent;
161
+ border: 1px solid rgba(40, 81, 68, 0.18);
162
+ }
163
+
164
+ .primary-button:hover,
165
+ .secondary-button:hover,
166
+ .ghost-button:hover {
167
+ transform: translateY(-1px);
168
+ }
169
+
170
+ .button-row {
171
+ display: flex;
172
+ gap: 12px;
173
+ flex-wrap: wrap;
174
+ }
175
+
176
+ .hint-grid {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
179
+ gap: 14px;
180
+ }
181
+
182
+ .hint-box {
183
+ padding: 18px;
184
+ border-radius: 18px;
185
+ background: rgba(18, 54, 44, 0.05);
186
+ }
187
+
188
+ .hint-box strong {
189
+ display: block;
190
+ margin-bottom: 6px;
191
+ }
192
+
193
+ .play-layout {
194
+ max-width: 1400px;
195
+ margin: 0 auto;
196
+ display: grid;
197
+ gap: 18px;
198
+ }
199
+
200
+ .topbar {
201
+ padding: 20px 24px;
202
+ display: flex;
203
+ gap: 16px;
204
+ justify-content: space-between;
205
+ align-items: center;
206
+ flex-wrap: wrap;
207
+ }
208
+
209
+ .topbar h1 {
210
+ margin: 0;
211
+ font-size: 1.4rem;
212
+ }
213
+
214
+ .meta-line {
215
+ display: flex;
216
+ gap: 10px;
217
+ flex-wrap: wrap;
218
+ color: #46695d;
219
+ font-size: 0.92rem;
220
+ }
221
+
222
+ .meta-pill {
223
+ padding: 8px 12px;
224
+ border-radius: 999px;
225
+ background: rgba(19, 35, 29, 0.06);
226
+ }
227
+
228
+ .play-grid {
229
+ display: grid;
230
+ grid-template-columns: minmax(0, 1fr) 320px;
231
+ gap: 18px;
232
+ }
233
+
234
+ .puzzle-panel {
235
+ padding: 24px;
236
+ overflow: auto;
237
+ }
238
+
239
+ .sidebar {
240
+ padding: 20px;
241
+ display: grid;
242
+ gap: 18px;
243
+ align-content: start;
244
+ }
245
+
246
+ .image-frame {
247
+ border-radius: 18px;
248
+ overflow: hidden;
249
+ border: 1px solid rgba(18, 35, 29, 0.08);
250
+ background: #f4f7f1;
251
+ }
252
+
253
+ .image-frame img {
254
+ display: block;
255
+ width: 100%;
256
+ height: auto;
257
+ }
258
+
259
+ .timer {
260
+ font-size: clamp(2rem, 5vw, 3.2rem);
261
+ font-weight: 800;
262
+ letter-spacing: -0.04em;
263
+ }
264
+
265
+ .status-box {
266
+ padding: 14px 16px;
267
+ border-radius: 18px;
268
+ background: rgba(18, 54, 44, 0.06);
269
+ color: #23463a;
270
+ }
271
+
272
+ .status-box.error {
273
+ background: rgba(170, 44, 44, 0.1);
274
+ color: #7f1e1e;
275
+ }
276
+
277
+ .ascii-preview {
278
+ min-height: 180px;
279
+ font-family:
280
+ "SFMono-Regular",
281
+ "Menlo",
282
+ "Consolas",
283
+ monospace;
284
+ font-size: 0.8rem;
285
+ white-space: pre;
286
+ overflow: auto;
287
+ }
288
+
289
+ .compact-preview {
290
+ min-height: 0;
291
+ }
292
+
293
+ .editor-stack {
294
+ display: grid;
295
+ gap: 16px;
296
+ }
297
+
298
+ .puzzle-note {
299
+ padding: 12px 14px;
300
+ border-radius: 16px;
301
+ background: rgba(18, 54, 44, 0.06);
302
+ color: #2b5446;
303
+ font-size: 0.95rem;
304
+ }
305
+
306
+ .board-grid {
307
+ display: inline-grid;
308
+ gap: 2px;
309
+ padding: 14px;
310
+ border-radius: 18px;
311
+ background: rgba(17, 35, 29, 0.06);
312
+ }
313
+
314
+ .board-cell,
315
+ .board-square,
316
+ .board-char {
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ user-select: none;
321
+ }
322
+
323
+ .board-cell {
324
+ width: 38px;
325
+ height: 38px;
326
+ border-radius: 12px;
327
+ border: none;
328
+ background: rgba(255, 255, 255, 0.92);
329
+ color: #143228;
330
+ font-weight: 700;
331
+ }
332
+
333
+ .board-cell.water {
334
+ background: rgba(255, 255, 255, 0.58);
335
+ color: #4c7567;
336
+ }
337
+
338
+ .board-cell.fixed {
339
+ background: linear-gradient(180deg, #fff3ce, #f2df9b);
340
+ }
341
+
342
+ .board-cell.island-cell {
343
+ border-radius: 50%;
344
+ box-shadow: inset 0 -2px 0 rgba(20, 40, 31, 0.12);
345
+ }
346
+
347
+ .board-cell.bridge-cell {
348
+ font-size: 1.1rem;
349
+ }
350
+
351
+ .board-cell.mirror {
352
+ background: linear-gradient(180deg, #dbe9ff, #b8d1ff);
353
+ }
354
+
355
+ .board-cell.endpoint-cell {
356
+ box-shadow:
357
+ inset 0 -2px 0 rgba(20, 40, 31, 0.12),
358
+ 0 0 0 3px rgba(255, 255, 255, 0.4);
359
+ }
360
+
361
+ .board-square {
362
+ width: 44px;
363
+ height: 44px;
364
+ border-radius: 14px;
365
+ border: 1px solid rgba(20, 50, 40, 0.08);
366
+ background: rgba(255, 255, 255, 0.92);
367
+ font-weight: 700;
368
+ }
369
+
370
+ .board-square.fill {
371
+ background: #17352b;
372
+ color: #f7fff6;
373
+ }
374
+
375
+ .board-square.empty {
376
+ background: #eef2ea;
377
+ color: #406257;
378
+ }
379
+
380
+ .board-square.unknown {
381
+ background: rgba(255, 255, 255, 0.92);
382
+ color: #a9b7b0;
383
+ }
384
+
385
+ .char-board {
386
+ display: inline-grid;
387
+ gap: 1px;
388
+ padding: 14px;
389
+ border-radius: 18px;
390
+ background: rgba(17, 35, 29, 0.08);
391
+ font-family:
392
+ "SFMono-Regular",
393
+ "Menlo",
394
+ "Consolas",
395
+ monospace;
396
+ }
397
+
398
+ .board-char {
399
+ width: 24px;
400
+ height: 24px;
401
+ border: none;
402
+ border-radius: 6px;
403
+ background: transparent;
404
+ color: #17362c;
405
+ }
406
+
407
+ .board-char.clickable {
408
+ background: rgba(255, 255, 255, 0.85);
409
+ }
410
+
411
+ .pattern-layout {
412
+ display: grid;
413
+ gap: 16px;
414
+ }
415
+
416
+ .pattern-board {
417
+ padding: 10px;
418
+ }
419
+
420
+ .pattern-grid {
421
+ display: grid;
422
+ gap: 8px;
423
+ }
424
+
425
+ .pattern-row {
426
+ display: grid;
427
+ grid-template-columns: 92px auto;
428
+ gap: 12px;
429
+ align-items: center;
430
+ }
431
+
432
+ .pattern-clue {
433
+ font-family:
434
+ "SFMono-Regular",
435
+ "Menlo",
436
+ "Consolas",
437
+ monospace;
438
+ color: #406257;
439
+ text-align: right;
440
+ }
441
+
442
+ .palette {
443
+ display: flex;
444
+ gap: 8px;
445
+ flex-wrap: wrap;
446
+ }
447
+
448
+ .palette button {
449
+ width: 38px;
450
+ height: 38px;
451
+ border-radius: 999px;
452
+ border: 2px solid transparent;
453
+ color: #112219;
454
+ font-weight: 800;
455
+ }
456
+
457
+ .palette button.active {
458
+ border-color: #13231d;
459
+ transform: scale(1.05);
460
+ }
461
+
462
+ .erase-swatch {
463
+ width: auto;
464
+ padding: 0 16px;
465
+ background: #eef2ea;
466
+ }
467
+
468
+ .svg-board-shell {
469
+ overflow: auto;
470
+ padding: 10px;
471
+ border-radius: 20px;
472
+ background: rgba(17, 35, 29, 0.05);
473
+ }
474
+
475
+ .svg-board {
476
+ display: block;
477
+ width: min(100%, 760px);
478
+ height: auto;
479
+ }
480
+
481
+ .svg-cell-bg,
482
+ .galaxy-cell-bg {
483
+ fill: rgba(255, 255, 255, 0.84);
484
+ }
485
+
486
+ .galaxy-cell-bg {
487
+ stroke: rgba(35, 70, 58, 0.08);
488
+ stroke-width: 1.5;
489
+ }
490
+
491
+ .svg-clue-text {
492
+ fill: #18392e;
493
+ font-size: 22px;
494
+ font-weight: 700;
495
+ }
496
+
497
+ .svg-vertex {
498
+ fill: #1f4d3c;
499
+ }
500
+
501
+ .svg-hit-target {
502
+ cursor: pointer;
503
+ }
504
+
505
+ .svg-hit-line {
506
+ stroke: transparent;
507
+ stroke-width: 22;
508
+ stroke-linecap: round;
509
+ }
510
+
511
+ .svg-line,
512
+ .svg-wall {
513
+ stroke-linecap: round;
514
+ transition:
515
+ stroke 120ms ease,
516
+ opacity 120ms ease,
517
+ stroke-width 120ms ease;
518
+ }
519
+
520
+ .svg-line.active {
521
+ stroke: #1f6a4f;
522
+ stroke-width: 7;
523
+ }
524
+
525
+ .svg-line.ghost {
526
+ stroke: rgba(39, 83, 68, 0.16);
527
+ stroke-width: 4;
528
+ }
529
+
530
+ .svg-wall.fixed {
531
+ stroke: #28493d;
532
+ stroke-width: 5;
533
+ }
534
+
535
+ .svg-wall.active {
536
+ stroke: #215b46;
537
+ stroke-width: 6;
538
+ }
539
+
540
+ .svg-wall.ghost {
541
+ stroke: rgba(39, 83, 68, 0.16);
542
+ stroke-width: 4;
543
+ }
544
+
545
+ .svg-xmark {
546
+ fill: #7c8e87;
547
+ font-size: 21px;
548
+ font-weight: 700;
549
+ }
550
+
551
+ .galaxy-dot {
552
+ fill: #17352b;
553
+ stroke: #f7f7f0;
554
+ stroke-width: 3;
555
+ }
556
+
557
+ .undead-legend {
558
+ display: inline-flex;
559
+ width: fit-content;
560
+ padding: 10px 14px;
561
+ border-radius: 999px;
562
+ background: rgba(18, 54, 44, 0.08);
563
+ color: #224639;
564
+ font-weight: 600;
565
+ }
566
+
567
+ .undead-grid-wrap {
568
+ display: grid;
569
+ gap: 10px;
570
+ }
571
+
572
+ .undead-clue-row {
573
+ display: grid;
574
+ grid-template-columns: 40px repeat(4, 38px) 40px;
575
+ gap: 8px;
576
+ align-items: center;
577
+ }
578
+
579
+ .undead-clue-box,
580
+ .undead-clue-spacer {
581
+ display: flex;
582
+ align-items: center;
583
+ justify-content: center;
584
+ min-height: 32px;
585
+ color: #46695d;
586
+ font-family:
587
+ "SFMono-Regular",
588
+ "Menlo",
589
+ "Consolas",
590
+ monospace;
591
+ }
592
+
593
+ .undead-clue-box.side {
594
+ font-weight: 700;
595
+ }
596
+
597
+ .undead-cell.monster-cell {
598
+ background: rgba(255, 255, 255, 0.96);
599
+ }
600
+
601
+ .done-layout {
602
+ max-width: 820px;
603
+ margin: 0 auto;
604
+ padding-top: 64px;
605
+ }
606
+
607
+ .done-card {
608
+ padding: 36px;
609
+ display: grid;
610
+ gap: 16px;
611
+ }
612
+
613
+ @media (max-width: 980px) {
614
+ .play-grid {
615
+ grid-template-columns: 1fr;
616
+ }
617
+ }
618
+
619
+ @media (max-width: 640px) {
620
+ .shell {
621
+ padding: 14px;
622
+ }
623
+
624
+ .hero,
625
+ .card,
626
+ .puzzle-panel,
627
+ .sidebar,
628
+ .done-card {
629
+ padding: 18px;
630
+ }
631
+
632
+ .board-cell {
633
+ width: 32px;
634
+ height: 32px;
635
+ }
636
+
637
+ .board-square {
638
+ width: 38px;
639
+ height: 38px;
640
+ }
641
+
642
+ .undead-clue-row {
643
+ grid-template-columns: 32px repeat(4, 32px) 32px;
644
+ gap: 6px;
645
+ }
646
+
647
+ .board-char {
648
+ width: 20px;
649
+ height: 20px;
650
+ font-size: 0.75rem;
651
+ }
652
+ }
frontend/src/lib/api.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Difficulty = "easy" | "medium" | "hard";
2
+ export type PuzzleType =
3
+ | "bridges"
4
+ | "flow_free"
5
+ | "galaxies"
6
+ | "loopy"
7
+ | "pattern"
8
+ | "undead";
9
+
10
+ export type SessionResponse = {
11
+ session_id: string;
12
+ engine: string;
13
+ puzzle_type: PuzzleType;
14
+ difficulty: Difficulty;
15
+ puzzle_id: string;
16
+ args: string;
17
+ status: string;
18
+ started_at: string | null;
19
+ payload: {
20
+ problem_ascii: string;
21
+ current_board_ascii: string;
22
+ image_base64: string | null;
23
+ };
24
+ };
25
+
26
+ export type SubmitResponse = {
27
+ solved: boolean;
28
+ elapsed_ms: number | null;
29
+ status: string;
30
+ verification: Record<string, unknown>;
31
+ };
32
+
33
+ async function handle<T>(response: Response): Promise<T> {
34
+ if (!response.ok) {
35
+ let message = response.statusText;
36
+ try {
37
+ const payload = (await response.json()) as { detail?: string };
38
+ message = payload.detail ?? message;
39
+ } catch {
40
+ // Fall back to the HTTP status text.
41
+ }
42
+ throw new Error(message);
43
+ }
44
+ return (await response.json()) as T;
45
+ }
46
+
47
+ export async function createSession(input: {
48
+ player_name: string;
49
+ puzzle_type: PuzzleType;
50
+ difficulty: Difficulty;
51
+ }): Promise<SessionResponse> {
52
+ return handle<SessionResponse>(
53
+ await fetch("/api/sessions", {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(input),
57
+ }),
58
+ );
59
+ }
60
+
61
+ export async function fetchSession(sessionId: string): Promise<SessionResponse> {
62
+ return handle<SessionResponse>(await fetch(`/api/sessions/${sessionId}`));
63
+ }
64
+
65
+ export async function readySession(
66
+ sessionId: string,
67
+ ): Promise<{ status: string; started_at: string | null }> {
68
+ return handle<{ status: string; started_at: string | null }>(
69
+ await fetch(`/api/sessions/${sessionId}/ready`, { method: "POST" }),
70
+ );
71
+ }
72
+
73
+ export async function submitSession(
74
+ sessionId: string,
75
+ boardAscii: string,
76
+ ): Promise<SubmitResponse> {
77
+ return handle<SubmitResponse>(
78
+ await fetch(`/api/sessions/${sessionId}/submit`, {
79
+ method: "POST",
80
+ headers: { "Content-Type": "application/json" },
81
+ body: JSON.stringify({ board_ascii: boardAscii }),
82
+ }),
83
+ );
84
+ }
frontend/src/main.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ import App from "./App";
5
+ import "./index.css";
6
+
7
+ createRoot(document.getElementById("root")!).render(
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>,
11
+ );
frontend/src/routes/DonePage.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useNavigate, useParams, useLocation } from "react-router-dom";
2
+
3
+ import type { SessionResponse, SubmitResponse } from "../lib/api";
4
+
5
+ type DoneState = {
6
+ result?: SubmitResponse;
7
+ session?: SessionResponse;
8
+ playerName?: string;
9
+ };
10
+
11
+ function formatElapsed(ms: number | null) {
12
+ if (ms === null) {
13
+ return "--:--.--";
14
+ }
15
+ const totalSeconds = Math.floor(ms / 1000);
16
+ const minutes = Math.floor(totalSeconds / 60)
17
+ .toString()
18
+ .padStart(2, "0");
19
+ const seconds = (totalSeconds % 60).toString().padStart(2, "0");
20
+ const centiseconds = Math.floor((ms % 1000) / 10)
21
+ .toString()
22
+ .padStart(2, "0");
23
+ return `${minutes}:${seconds}.${centiseconds}`;
24
+ }
25
+
26
+ export function DonePage() {
27
+ const { sessionId = "" } = useParams();
28
+ const location = useLocation();
29
+ const navigate = useNavigate();
30
+ const state = (location.state as DoneState | null) ?? null;
31
+
32
+ function playAgain() {
33
+ navigate("/", {
34
+ state: {
35
+ playerName: state?.playerName ?? "",
36
+ puzzleType: state?.session?.puzzle_type,
37
+ difficulty: state?.session?.difficulty,
38
+ },
39
+ });
40
+ }
41
+
42
+ return (
43
+ <div className="shell">
44
+ <div className="done-layout">
45
+ <section className="panel done-card">
46
+ <div className="eyebrow">Solved</div>
47
+ <h1 style={{ margin: 0 }}>Puzzle complete.</h1>
48
+ <p style={{ margin: 0, color: "#46695d" }}>
49
+ Session <code>{sessionId}</code> finished in{" "}
50
+ <strong>{formatElapsed(state?.result?.elapsed_ms ?? null)}</strong>.
51
+ </p>
52
+ <div className="button-row">
53
+ <button className="primary-button" type="button" onClick={playAgain}>
54
+ Start Another Random Puzzle
55
+ </button>
56
+ <button className="ghost-button" type="button" onClick={() => navigate("/")}>
57
+ Back Home
58
+ </button>
59
+ </div>
60
+ </section>
61
+ </div>
62
+ </div>
63
+ );
64
+ }
frontend/src/routes/HomePage.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import type { FormEvent } from "react";
3
+ import { useLocation, useNavigate } from "react-router-dom";
4
+
5
+ import { createSession, type Difficulty, type PuzzleType } from "../lib/api";
6
+
7
+ const PUZZLE_OPTIONS: Array<{ value: PuzzleType; label: string; blurb: string }> = [
8
+ { value: "bridges", label: "Bridges", blurb: "Connect islands with single or double links." },
9
+ { value: "flow_free", label: "Flow Free", blurb: "Fill the board with non-crossing color paths." },
10
+ { value: "galaxies", label: "Galaxies", blurb: "Draw region boundaries around rotationally symmetric clusters." },
11
+ { value: "loopy", label: "Loopy", blurb: "Toggle loop edges around numeric clues." },
12
+ { value: "pattern", label: "Pattern", blurb: "Fill cells to satisfy run clues while keeping one connected shape." },
13
+ { value: "undead", label: "Undead", blurb: "Place monsters around mirrors and sightline clues." },
14
+ ];
15
+
16
+ export function HomePage() {
17
+ const navigate = useNavigate();
18
+ const location = useLocation();
19
+ const defaults = (location.state as
20
+ | { playerName?: string; puzzleType?: PuzzleType; difficulty?: Difficulty }
21
+ | null) ?? { playerName: "", puzzleType: "bridges", difficulty: "easy" };
22
+ const [playerName, setPlayerName] = useState(defaults.playerName ?? "");
23
+ const [puzzleType, setPuzzleType] = useState(defaults.puzzleType ?? "bridges");
24
+ const [difficulty, setDifficulty] = useState(defaults.difficulty ?? "easy");
25
+ const [error, setError] = useState<string | null>(null);
26
+ const [submitting, setSubmitting] = useState(false);
27
+
28
+ async function onSubmit(event: FormEvent<HTMLFormElement>) {
29
+ event.preventDefault();
30
+ setSubmitting(true);
31
+ setError(null);
32
+ try {
33
+ const session = await createSession({
34
+ player_name: playerName,
35
+ puzzle_type: puzzleType,
36
+ difficulty,
37
+ });
38
+ navigate(`/play/${session.session_id}`, { state: { session, playerName } });
39
+ } catch (caught) {
40
+ setError(caught instanceof Error ? caught.message : "Could not create a session.");
41
+ } finally {
42
+ setSubmitting(false);
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div className="shell">
48
+ <div className="home-layout">
49
+ <section className="panel hero">
50
+ <div className="eyebrow">TopoBench Space</div>
51
+ <h1>Play the benchmark, not just the report.</h1>
52
+ <p>
53
+ This Space pulls real TopoBench puzzles from Hugging Face, times each solve
54
+ on the server, and verifies your submission against the same native logic
55
+ used by the benchmark repo.
56
+ </p>
57
+ </section>
58
+
59
+ <div className="hero-grid">
60
+ <section className="panel card">
61
+ <form className="form-grid" onSubmit={onSubmit}>
62
+ <div className="field">
63
+ <label htmlFor="playerName">Player name</label>
64
+ <input
65
+ id="playerName"
66
+ value={playerName}
67
+ onChange={(event) => setPlayerName(event.target.value)}
68
+ placeholder="Ada Lovelace"
69
+ maxLength={80}
70
+ required
71
+ />
72
+ </div>
73
+
74
+ <div className="field">
75
+ <label htmlFor="puzzleType">Puzzle family</label>
76
+ <select
77
+ id="puzzleType"
78
+ value={puzzleType}
79
+ onChange={(event) => setPuzzleType(event.target.value as PuzzleType)}
80
+ >
81
+ {PUZZLE_OPTIONS.map((option) => (
82
+ <option key={option.value} value={option.value}>
83
+ {option.label}
84
+ </option>
85
+ ))}
86
+ </select>
87
+ </div>
88
+
89
+ <div className="field">
90
+ <label htmlFor="difficulty">Difficulty</label>
91
+ <select
92
+ id="difficulty"
93
+ value={difficulty}
94
+ onChange={(event) => setDifficulty(event.target.value as Difficulty)}
95
+ >
96
+ <option value="easy">Easy</option>
97
+ <option value="medium">Medium</option>
98
+ <option value="hard">Hard</option>
99
+ </select>
100
+ </div>
101
+
102
+ {error ? <div className="status-box error">{error}</div> : null}
103
+
104
+ <div className="button-row">
105
+ <button className="primary-button" type="submit" disabled={submitting}>
106
+ {submitting ? "Starting..." : "Start Puzzle"}
107
+ </button>
108
+ </div>
109
+ </form>
110
+ </section>
111
+
112
+ <section className="panel card hint-grid">
113
+ {PUZZLE_OPTIONS.map((option) => (
114
+ <div key={option.value} className="hint-box">
115
+ <strong>{option.label}</strong>
116
+ <div>{option.blurb}</div>
117
+ </div>
118
+ ))}
119
+ </section>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
frontend/src/routes/PlayPage.tsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from "react";
2
+ import { useLocation, useNavigate, useParams } from "react-router-dom";
3
+
4
+ import { PuzzleEditor } from "../components/PuzzleEditor";
5
+ import { fetchSession, readySession, submitSession, type SessionResponse } from "../lib/api";
6
+
7
+ const PUZZLE_HELP: Record<string, string> = {
8
+ bridges: "Connect every numbered island into a single network. Each route cycles empty, single bridge, then double bridge.",
9
+ flow_free: "Choose a color from the palette, then paint a continuous path between matching endpoints without changing the endpoints themselves.",
10
+ galaxies: "Partition the board with interior walls so each region has exactly one dot-symmetry center.",
11
+ loopy: "Build one single loop. Every segment cycles blank, line, then blocked, including the outer perimeter.",
12
+ pattern: "Each square cycles unknown, filled, then empty so the row and column clues match the finished pattern.",
13
+ undead: "Place ghosts, vampires, and zombies so the side clues and the global monster counts all line up.",
14
+ };
15
+
16
+ function formatElapsed(ms: number) {
17
+ const totalSeconds = Math.floor(ms / 1000);
18
+ const minutes = Math.floor(totalSeconds / 60)
19
+ .toString()
20
+ .padStart(2, "0");
21
+ const seconds = (totalSeconds % 60).toString().padStart(2, "0");
22
+ const centiseconds = Math.floor((ms % 1000) / 10)
23
+ .toString()
24
+ .padStart(2, "0");
25
+ return `${minutes}:${seconds}.${centiseconds}`;
26
+ }
27
+
28
+ type LocationState = {
29
+ session?: SessionResponse;
30
+ playerName?: string;
31
+ };
32
+
33
+ export function PlayPage() {
34
+ const { sessionId = "" } = useParams();
35
+ const location = useLocation();
36
+ const navigate = useNavigate();
37
+ const state = (location.state as LocationState | null) ?? null;
38
+ const [session, setSession] = useState<SessionResponse | null>(state?.session ?? null);
39
+ const [boardAscii, setBoardAscii] = useState(state?.session?.payload.current_board_ascii ?? "");
40
+ const [startedAt, setStartedAt] = useState<string | null>(state?.session?.started_at ?? null);
41
+ const [elapsedMs, setElapsedMs] = useState(0);
42
+ const [error, setError] = useState<string | null>(null);
43
+ const [statusMessage, setStatusMessage] = useState<string | null>(null);
44
+ const [submitting, setSubmitting] = useState(false);
45
+
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+ async function load() {
49
+ try {
50
+ const payload = await fetchSession(sessionId);
51
+ if (cancelled) {
52
+ return;
53
+ }
54
+ setSession(payload);
55
+ setBoardAscii(payload.payload.current_board_ascii);
56
+ setStatusMessage(null);
57
+ if (!payload.started_at) {
58
+ const ready = await readySession(sessionId);
59
+ if (!cancelled) {
60
+ setStartedAt(ready.started_at);
61
+ }
62
+ } else {
63
+ setStartedAt(payload.started_at);
64
+ }
65
+ } catch (caught) {
66
+ if (!cancelled) {
67
+ setError(caught instanceof Error ? caught.message : "Could not load the session.");
68
+ }
69
+ }
70
+ }
71
+ void load();
72
+ return () => {
73
+ cancelled = true;
74
+ };
75
+ }, [sessionId]);
76
+
77
+ useEffect(() => {
78
+ if (!startedAt) {
79
+ return;
80
+ }
81
+ const tick = window.setInterval(() => {
82
+ const delta = Date.now() - Date.parse(startedAt);
83
+ setElapsedMs(Math.max(0, delta));
84
+ }, 80);
85
+ return () => window.clearInterval(tick);
86
+ }, [startedAt]);
87
+
88
+ async function onSubmit() {
89
+ if (!session) {
90
+ return;
91
+ }
92
+ setSubmitting(true);
93
+ setError(null);
94
+ setStatusMessage(null);
95
+ try {
96
+ const result = await submitSession(session.session_id, boardAscii);
97
+ if (result.solved) {
98
+ navigate(`/done/${session.session_id}`, {
99
+ state: {
100
+ result,
101
+ session,
102
+ playerName: state?.playerName ?? "",
103
+ boardAscii,
104
+ },
105
+ });
106
+ } else {
107
+ if (!result.verification.board_valid) {
108
+ setError("That board is not structurally valid yet. Double-check fixed clues, endpoints, and wall or bridge placement.");
109
+ } else {
110
+ setStatusMessage("Close, but not solved yet. Keep going and submit again when you are ready.");
111
+ }
112
+ }
113
+ } catch (caught) {
114
+ setError(caught instanceof Error ? caught.message : "Could not submit the puzzle.");
115
+ } finally {
116
+ setSubmitting(false);
117
+ }
118
+ }
119
+
120
+ function newRandom() {
121
+ navigate("/", {
122
+ state: {
123
+ playerName: state?.playerName ?? "",
124
+ puzzleType: session?.puzzle_type,
125
+ difficulty: session?.difficulty,
126
+ },
127
+ });
128
+ }
129
+
130
+ function resetBoard() {
131
+ if (!session) {
132
+ return;
133
+ }
134
+ setBoardAscii(session.payload.problem_ascii);
135
+ setError(null);
136
+ setStatusMessage("Board reset to the original puzzle.");
137
+ }
138
+
139
+ if (!session) {
140
+ return (
141
+ <div className="shell">
142
+ <div className="panel card">{error ?? "Loading puzzle..."}</div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <div className="shell">
149
+ <div className="play-layout">
150
+ <section className="panel topbar">
151
+ <div>
152
+ <h1>{session.puzzle_type.replace("_", " ")}</h1>
153
+ <div className="meta-line">
154
+ <span className="meta-pill">{session.difficulty}</span>
155
+ <span className="meta-pill">{session.puzzle_id}</span>
156
+ </div>
157
+ </div>
158
+ <div className="timer">{formatElapsed(elapsedMs)}</div>
159
+ </section>
160
+
161
+ <div className="play-grid">
162
+ <section className="panel puzzle-panel">
163
+ <PuzzleEditor
164
+ puzzleType={session.puzzle_type}
165
+ problemAscii={session.payload.problem_ascii}
166
+ boardAscii={boardAscii}
167
+ onChange={setBoardAscii}
168
+ />
169
+ </section>
170
+
171
+ <aside className="panel sidebar">
172
+ <div className="status-box">
173
+ Timer starts once the puzzle appears. The server keeps the official solve time.
174
+ </div>
175
+
176
+ {error ? <div className="status-box error">{error}</div> : null}
177
+ {statusMessage ? <div className="status-box">{statusMessage}</div> : null}
178
+ <div className="status-box">{PUZZLE_HELP[session.puzzle_type] ?? "Solve the puzzle, then submit to verify it."}</div>
179
+
180
+ <div className="button-row">
181
+ <button className="primary-button" type="button" onClick={onSubmit} disabled={submitting}>
182
+ {submitting ? "Checking..." : "Submit"}
183
+ </button>
184
+ <button className="ghost-button" type="button" onClick={resetBoard}>
185
+ Reset Board
186
+ </button>
187
+ <button className="secondary-button" type="button" onClick={newRandom}>
188
+ New Random Puzzle
189
+ </button>
190
+ </div>
191
+
192
+ {session.payload.image_base64 ? (
193
+ <div className="image-frame">
194
+ <img
195
+ src={`data:image/png;base64,${session.payload.image_base64}`}
196
+ alt="Reference rendering of the puzzle"
197
+ />
198
+ </div>
199
+ ) : null}
200
+
201
+ <div>
202
+ <strong>ASCII board</strong>
203
+ <pre className="ascii-preview">{boardAscii}</pre>
204
+ </div>
205
+ </aside>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ );
210
+ }
frontend/tsconfig.app.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "es2023",
5
+ "lib": ["ES2023", "DOM"],
6
+ "module": "esnext",
7
+ "types": ["vite/client"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true
23
+ },
24
+ "include": ["src"]
25
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }