diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a70a029 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.{py,rs,toml}] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f47922c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.png binary +*.jpg binary +*.zip binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ad3879 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# In-progress planning and engineering documents stay local; never publish to Gitea. +/docs + +# Local configuration and credentials +.env +.env.* +!.env.example +!*.env.example +*.pem +*.key + +# Tool caches and generated build output +.cache/ +.tools/ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.coverage +htmlcov/ +node_modules/ +dist/ +target/ +*.egg-info/ + +# Local data and editor state +.Trash-*/ +.idea/ +.vscode/ +.DS_Store +*.log +deploy/data/ +experiments/results/ +!experiments/results/.gitkeep diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..43ad8a9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# KF-OnlineJudge 工程约定 + +按现有模块边界扩展。本地存在 `docs/ENGINEERING.md` 和 `docs/DEVELOPMENT.md` 时先阅读;开发文档不在远端仓库中。 + +- 本机 `docs` 使用共享目录链接,Windows 和 CachyOS 使用同一份共享文档;实际目标由 `readlink docs` 确认。不创建另一份同步副本,不把源码、依赖或构建产物写回共享目录。 +- `docs/` 中的开发文档仅保留本地,不加入 Git 提交,不推送到 Gitea;不得用 `git add -f` 绕过忽略规则。 +- 分支采用 `feat/`、`fix/`、`chore/`、`perf/`、`test/` 等工程前缀,提交采用 Conventional Commits;不添加工具名称前缀或工具署名。 +- 本仓库提交身份使用 `kronecker <2026692687@qq.com>`。 +- 后端固定 Python 3.13.16,由 uv 管理;不要改用系统 Python 3.14 或用 uvx 运行项目服务。 +- 在本机原生文件系统中开发,使用仓库根目录 Makefile;Python 环境为 `backend/.venv`,Rust 构建产物为 `judge/target`。 +- 平台为模块化单体;业务域之间通过 `public.py` 调用,不直接操作其他模块的 ORM/repository。 +- Runtime 通过注入端口调用业务工具;不导入业务模块或具体基础设施实现。 +- Rust crate 依赖方向与 Python 分层由 `scripts/check_architecture.py` 检查。新增边界须同步更新架构说明与检查规则。 +- 跨服务协议变更先更新模型和共享样例,运行 `make contracts`,保留 Python/Rust 的兼容验证。 +- 数据库迁移只由后端 Alembic 管理;协调器控制执行状态,判题 Worker 不直接写评测数据库。 +- 沙箱未验证隔离前拒绝执行;不得用直接运行宿主命令作为临时判题实现。 +- 只建立当期需要的接口和实现;未实现能力明确报告,不提供模拟成绩或虚假任务成功状态。 +- 使用项目锁文件;新增依赖说明职责。凭据、私有代码和隐藏测试数据不进入仓库。 + +按变更运行 `make check-backend`、`make check-judge`、`make check-frontend` 和 `make architecture`。 +跨工程骨架或契约改动运行完整 `make check`,记录实际通过项和未验证的能力。 +沙箱的权限、隔离、超限和破坏性测试单独在规定执行环境验证。 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..83679b0 --- /dev/null +++ b/Makefile @@ -0,0 +1,83 @@ +SHELL := /bin/bash +CARGO ?= $(if $(wildcard $(HOME)/.cargo/bin/cargo),$(HOME)/.cargo/bin/cargo,cargo) +UV ?= uv +PNPM ?= pnpm + +.PHONY: help install check architecture check-backend check-frontend check-judge contracts format api frontend coordinator sandbox-probe worker-check infra-config infra-up infra-down + +help: + @echo 'install 安装锁定依赖并编译 Rust workspace' + @echo 'check 架构、契约、静态检查、测试与前端构建' + @echo 'contracts 更新 JSON Schema、OpenAPI 与前端类型' + @echo 'format 格式化 Python、Rust 与前端源码' + @echo 'api / frontend 启动平台 API / 前端开发服务' + @echo 'coordinator 启动协调器 HTTP 骨架' + @echo 'sandbox-probe 只读探测当前 Linux 环境' + @echo 'worker-check 验证三个后台进程入口,不消费任务' + @echo 'infra-config 检查本地 PostgreSQL / RabbitMQ 配置' + @echo 'infra-up/down 启动/停止本地开发依赖(保留数据卷)' + +install: + cd backend && $(UV) sync --locked + cd frontend && $(PNPM) install --frozen-lockfile + cd judge && $(CARGO) build --workspace --locked + +check: architecture check-backend check-judge check-frontend infra-config + +architecture: + python3 scripts/check_architecture.py + +check-backend: + cd backend && $(UV) run --locked ruff check . ../scripts + cd backend && $(UV) run --locked ruff format --check . ../scripts + cd backend && $(UV) run --locked pytest + cd backend && $(UV) run --locked python -m kf_oj.contracts.export --check + +check-judge: + cd judge && $(CARGO) fmt --all -- --check + cd judge && $(CARGO) clippy --workspace --all-targets --locked -- -D warnings + cd judge && $(CARGO) test --workspace --locked + +check-frontend: + cd frontend && $(PNPM) check:api + cd frontend && $(PNPM) lint + cd frontend && $(PNPM) format:check + cd frontend && $(PNPM) typecheck + cd frontend && $(PNPM) test + cd frontend && $(PNPM) build + +contracts: + cd backend && $(UV) run --locked python -m kf_oj.contracts.export + cd frontend && $(PNPM) generate:api + +format: + cd backend && $(UV) run --locked ruff check --fix . ../scripts + cd backend && $(UV) run --locked ruff format . ../scripts + cd judge && $(CARGO) fmt --all + cd frontend && $(PNPM) format + +api: + cd backend && $(UV) run --locked uvicorn kf_oj.api.app:create_app --factory --reload --host 127.0.0.1 --port 8000 + +frontend: + cd frontend && $(PNPM) dev + +coordinator: + cd judge && $(CARGO) run --locked -p kf-coordinator + +sandbox-probe: + cd judge && $(CARGO) run --locked -p kf-sandbox -- probe + +worker-check: + cd backend && $(UV) run --locked kf-learning-worker --check + cd backend && $(UV) run --locked kf-outbox-publisher --check + cd judge && $(CARGO) run --locked -p kf-worker -- --check + +infra-config: + docker compose --env-file deploy/.env.example -f deploy/compose.dev.yml config --quiet + +infra-up: + docker compose --env-file deploy/.env -f deploy/compose.dev.yml up -d --wait + +infra-down: + docker compose --env-file deploy/.env -f deploy/compose.dev.yml down diff --git a/README.md b/README.md index 661bdde..6d0019a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ # KF-OnlineJudge +以 Rust 自研判题与 Linux 沙箱、以 Python/FastAPI 提供平台服务的在线评测系统。 +前端使用 Vue 3、TypeScript、Element Plus 和 Vite。 + +当前版本为 **0.0.1 工程骨架**:平台 API 和协调器提供存活接口,前端可检查实际 API 连接。 +沙箱隔离执行、判题、账号、题库、提交与学习业务尚未实现。 + +## 工程划分 + +| 目录 | 职责 | +| --- | --- | +| `backend/` | Python 模块化单体、平台 API、学习 Worker、待投递事件发布器及唯一数据库迁移入口 | +| `judge/` | Rust workspace:协议、沙箱、判题编排、判题 Worker 和协调器 | +| `frontend/` | 页面、路由、请求生命周期与生成的 API 类型 | +| `contracts/` | 版本化 JSON Schema 与 OpenAPI 快照 | +| `deploy/` | 本地 PostgreSQL / RabbitMQ 配置 | +| `scripts/` | Python 导入边界与 Rust crate 依赖检查 | +| `tests/` | 跨语言共享协议样例 | +| `skills/`、`experiments/` | 后续内置技能与可复现实验的扩展位置 | + +Python 业务域分为 `identity`、`problems`、`submissions`、`community`、`learning`、`skills` +和 `operations`。业务域之间通过公开应用接口协作,不直接访问其他域的持久化实现。 +`runtime` 管理执行机制,通过注入端口调用业务能力。 + +Rust 分为 `kf-contracts`、`kf-sandbox`、`kf-engine`、`kf-worker`、`kf-coordinator` 五个 crate。 +协调器拥有执行状态写入权,判题 Worker 不直接写评测数据库。 +沙箱尚未验证隔离能力,当前执行入口明确拒绝运行程序。 + +## 安装与检查 + +工具链基线:Python 3.13.16 + uv、Rust 1.99.0、Node 26.10.0 + pnpm 11.28.4。 +Python 和 Rust 版本由工程配置固定,三套工程分别提交依赖锁文件。 +开发与构建使用 Linux 原生文件系统。 + +```bash +make install +make check +``` + +`make check` 包含架构边界、契约同步、静态检查、测试、前端生产构建和 Compose 配置检查。 +检查无需启动数据库或队列,也不执行破坏性沙箱测试。 +`make format` 格式化源码,`make contracts` 更新 Schema、OpenAPI 和前端类型。 + +## 启动骨架 + +分别在终端运行: + +```bash +make api # 平台 API:127.0.0.1:8000 +make frontend # Vue 开发服务:127.0.0.1:5173 +make coordinator # 协调器:127.0.0.1:8100 +``` + +平台存活接口为 `/api/v1/health/live`,开发 API 文档为 `/api/docs`。 +前端开发服务将 `/api` 代理到平台;生产部署需另行配置反向代理与 SPA 路由回退。 +协调器存活接口为 `/health/live`。 + +```bash +make worker-check # 三个后台入口仅检查配置,不消费任务 +make sandbox-probe # 只读观察 Linux 环境,不创建隔离资源 +``` + +本地数据库与队列的启动步骤见 [部署说明](deploy/README.md)。 +业务表和数据库 revision 尚未建立;迁移统一由后端 Alembic 管理。 +模型服务与第三方插件尚未接入。 + +开发中的规划文档仅在本地维护,不包含在远端仓库中。 +后续逐步实现沙箱、单机判题、分布式恢复、并行优化和学习功能。 + +## 许可证 + +[MIT](LICENSE)。引用或复用其他项目代码时记录来源并遵守对应许可证。 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..faad37a --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +KF_OJ_ENVIRONMENT=development +KF_OJ_LOG_LEVEL=INFO +# Optional until database-backed use cases are implemented. +KF_OJ_DATABASE_URL=postgresql+asyncpg://kf_oj:local-dev-only@127.0.0.1:5432/kf_oj +KF_OJ_AMQP_URL=amqp://kf_oj:local-dev-only@127.0.0.1:5672/kf_oj diff --git a/backend/.python-version b/backend/.python-version new file mode 100644 index 0000000..d540408 --- /dev/null +++ b/backend/.python-version @@ -0,0 +1 @@ +3.13.16 diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..27d4b56 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,25 @@ +# Python 平台工程 + +固定 Python 3.13.16,使用 uv 管理项目与锁文件。API、待投递事件发布器和学习 Worker +共用一个 Python 包、一个依赖锁文件,分别运行独立进程。 + +业务模块位于 `src/kf_oj/modules/`;HTTP 在 `api/`;通用适配器在 `infra/`; +Agent 执行机制在 `runtime/`;进程组合入口在 `workers/`。 +业务域之间通过公开应用接口调用,不直接访问其他域的 repository/models。 +Runtime 通过注入端口调用业务能力;导入边界由根目录的架构检查脚本验证。 + +```bash +# 从仓库根目录调用。 +make install +make api +make worker-check +``` + +API 当前只有 `/api/v1/health/live`,不连接数据库或消息队列。 +Worker 目前仅支持 `--check`,默认拒绝进入尚未实现的任务循环。 +日志、配置和迁移入口已建立;业务表、登录、判题投递和模型接入尚未实现。 + +Python 虚拟环境位于 `backend/.venv`,可在 `backend/` 直接使用 `uv sync --locked` 和 `uv run --locked`。 +数据库由 Alembic 唯一管理;业务模型落地后在 `migrations/env.py` 中显式登记元数据。 +不要用 API 启动时的 `create_all` 代替迁移,也不要让 Rust 维护另一套迁移历史。 +配置示例在 `.env.example`;执行迁移前复制为 `.env` 并设置实际数据库连接。 diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..e7c98f8 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,37 @@ +[alembic] +script_location = %(here)s/migrations +prepend_sys_path = . + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 0000000..c105c05 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,52 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import Connection + +from kf_oj.core.settings import Settings +from kf_oj.infra.db.base import Base +from kf_oj.infra.db.session import build_engine + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# Register each business module's ORM models here when its schema is implemented. +target_metadata = Base.metadata + + +def run_offline() -> None: + settings = Settings() + if settings.database_url is None: + raise ValueError("KF_OJ_DATABASE_URL is required for migrations") + context.configure( + url=settings.database_url.get_secret_value(), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def migrate(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + with context.begin_transaction(): + context.run_migrations() + + +async def run_online() -> None: + engine = build_engine(Settings()) + try: + async with engine.connect() as connection: + await connection.run_sync(migrate) + finally: + await engine.dispose() + + +if context.is_offline_mode(): + run_offline() +else: + asyncio.run(run_online()) diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 0000000..49a7df2 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision: str = ${repr(up_revision)} +down_revision: str | None = ${repr(down_revision)} +branch_labels: str | Sequence[str] | None = ${repr(branch_labels)} +depends_on: str | Sequence[str] | None = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/versions/.gitkeep b/backend/migrations/versions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..949761d --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,46 @@ +[build-system] +requires = ["hatchling>=1.27,<2"] +build-backend = "hatchling.build" + +[project] +name = "kf-oj-platform" +version = "0.0.1" +description = "KF-OnlineJudge platform and learning workers" +readme = "README.md" +requires-python = ">=3.13,<3.14" +dependencies = [ + "fastapi>=0.115,<1", + "pydantic>=2.10,<3", + "pydantic-settings>=2.7,<3", + "uvicorn>=0.34,<1", + "sqlalchemy[asyncio]>=2.0,<3", + "asyncpg>=0.30,<1", + "alembic>=1.15,<2", + "aio-pika>=9.5,<10", +] + +[project.scripts] +kf-learning-worker = "kf_oj.workers.learning:main" +kf-outbox-publisher = "kf_oj.workers.outbox:main" + +[dependency-groups] +dev = [ + "ruff>=0.11,<1", + "pytest>=8.3,<10", + "httpx>=0.28,<1", + "jsonschema>=4.23,<5", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/kf_oj"] + +[tool.ruff] +target-version = "py313" +line-length = 100 + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-ra" diff --git a/backend/src/kf_oj/__init__.py b/backend/src/kf_oj/__init__.py new file mode 100644 index 0000000..fb3a09f --- /dev/null +++ b/backend/src/kf_oj/__init__.py @@ -0,0 +1,3 @@ +"""KF-OnlineJudge platform, application modules and learning workers.""" + +__version__ = "0.0.1" diff --git a/backend/src/kf_oj/api/__init__.py b/backend/src/kf_oj/api/__init__.py new file mode 100644 index 0000000..b6f9df6 --- /dev/null +++ b/backend/src/kf_oj/api/__init__.py @@ -0,0 +1 @@ +"""HTTP transport and application composition.""" diff --git a/backend/src/kf_oj/api/app.py b/backend/src/kf_oj/api/app.py new file mode 100644 index 0000000..0d6d81c --- /dev/null +++ b/backend/src/kf_oj/api/app.py @@ -0,0 +1,36 @@ +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from kf_oj import __version__ +from kf_oj.api.v1.router import router +from kf_oj.core.logging import configure_logging +from kf_oj.core.settings import Settings + + +def create_app(settings: Settings | None = None) -> FastAPI: + settings = settings or Settings() + + @asynccontextmanager + async def lifespan(_: FastAPI): + configure_logging(settings.log_level) + logger = logging.getLogger("kf_oj.api") + logger.info("Platform API started") + try: + yield + finally: + logger.info("Platform API stopped") + + app = FastAPI( + title="KF-OnlineJudge Platform API", + version=__version__, + description="工程骨架。当前只提供进程存活检查,业务接口逐项实现。", + docs_url="/api/docs" if settings.environment != "production" else None, + redoc_url=None, + openapi_url="/api/openapi.json" if settings.environment != "production" else None, + lifespan=lifespan, + ) + app.state.settings = settings + app.include_router(router, prefix="/api/v1") + return app diff --git a/backend/src/kf_oj/api/v1/__init__.py b/backend/src/kf_oj/api/v1/__init__.py new file mode 100644 index 0000000..27a6722 --- /dev/null +++ b/backend/src/kf_oj/api/v1/__init__.py @@ -0,0 +1 @@ +"""Platform API version 1.""" diff --git a/backend/src/kf_oj/api/v1/router.py b/backend/src/kf_oj/api/v1/router.py new file mode 100644 index 0000000..8407ad9 --- /dev/null +++ b/backend/src/kf_oj/api/v1/router.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from kf_oj import __version__ +from kf_oj.contracts.health import HealthResponse + +router = APIRouter() + + +@router.get("/health/live", response_model=HealthResponse, tags=["health"], operation_id="liveness") +async def liveness() -> HealthResponse: + """Check process liveness only; does not attest DB, queue or judge readiness.""" + return HealthResponse(status="ok", service="platform", version=__version__) diff --git a/backend/src/kf_oj/contracts/__init__.py b/backend/src/kf_oj/contracts/__init__.py new file mode 100644 index 0000000..2a9d45c --- /dev/null +++ b/backend/src/kf_oj/contracts/__init__.py @@ -0,0 +1 @@ +"""Versioned transport models; never import business or infrastructure code.""" diff --git a/backend/src/kf_oj/contracts/export.py b/backend/src/kf_oj/contracts/export.py new file mode 100644 index 0000000..6102016 --- /dev/null +++ b/backend/src/kf_oj/contracts/export.py @@ -0,0 +1,41 @@ +"""Export deterministic public contracts without opening DB or queue connections.""" + +import argparse +import json +from pathlib import Path + +from kf_oj.api.app import create_app +from kf_oj.contracts.judge import JudgeTaskNotification +from kf_oj.core.settings import Settings + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="Fail if checked-in files are stale") + args = parser.parse_args() + root = Path(__file__).resolve().parents[4] + schema = JudgeTaskNotification.model_json_schema() + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + schema["$id"] = "urn:kf-oj:judge-task:v1" + outputs = { + root / "contracts/schemas/judge-task.v1.schema.json": schema, + root / "contracts/openapi/platform.v1.json": create_app( + Settings(_env_file=None, environment="test") + ).openapi(), + } + stale = [] + for path, document in outputs.items(): + content = json.dumps(document, indent=2, sort_keys=True, ensure_ascii=False) + "\n" + if args.check: + if not path.exists() or path.read_text(encoding="utf-8") != content: + stale.append(str(path.relative_to(root))) + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print(f"Exported {path.relative_to(root)}") + if stale: + parser.exit(1, "Stale contracts: " + ", ".join(stale) + "; run make contracts\n") + + +if __name__ == "__main__": + main() diff --git a/backend/src/kf_oj/contracts/health.py b/backend/src/kf_oj/contracts/health.py new file mode 100644 index 0000000..2077005 --- /dev/null +++ b/backend/src/kf_oj/contracts/health.py @@ -0,0 +1,11 @@ +from typing import Literal + +from pydantic import BaseModel, ConfigDict + + +class HealthResponse(BaseModel): + model_config = ConfigDict(extra="forbid") + + status: Literal["ok"] + service: Literal["platform"] + version: str diff --git a/backend/src/kf_oj/contracts/judge.py b/backend/src/kf_oj/contracts/judge.py new file mode 100644 index 0000000..07b1149 --- /dev/null +++ b/backend/src/kf_oj/contracts/judge.py @@ -0,0 +1,14 @@ +from typing import Literal +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + + +class JudgeTaskNotification(BaseModel): + """A wake-up notification, not execution authority or the source of task state.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + schema_version: Literal[1] + kind: Literal["judge.evaluation"] + evaluation_id: UUID diff --git a/backend/src/kf_oj/core/__init__.py b/backend/src/kf_oj/core/__init__.py new file mode 100644 index 0000000..1801d85 --- /dev/null +++ b/backend/src/kf_oj/core/__init__.py @@ -0,0 +1 @@ +"""Cross-cutting configuration and logging; no business or transport dependencies.""" diff --git a/backend/src/kf_oj/core/logging.py b/backend/src/kf_oj/core/logging.py new file mode 100644 index 0000000..ae5f0e1 --- /dev/null +++ b/backend/src/kf_oj/core/logging.py @@ -0,0 +1,22 @@ +import json +import logging +from datetime import UTC, datetime + + +class JsonFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + payload = { + "timestamp": datetime.fromtimestamp(record.created, UTC).isoformat(), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + if record.exc_info: + payload["exception"] = self.formatException(record.exc_info) + return json.dumps(payload, ensure_ascii=False) + + +def configure_logging(level: str) -> None: + handler = logging.StreamHandler() + handler.setFormatter(JsonFormatter()) + logging.basicConfig(level=level, handlers=[handler], force=True) diff --git a/backend/src/kf_oj/core/settings.py b/backend/src/kf_oj/core/settings.py new file mode 100644 index 0000000..f5fb278 --- /dev/null +++ b/backend/src/kf_oj/core/settings.py @@ -0,0 +1,13 @@ +from typing import Literal + +from pydantic import SecretStr +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="KF_OJ_", env_file=".env", extra="ignore") + + environment: Literal["development", "test", "production"] = "development" + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" + database_url: SecretStr | None = None + amqp_url: SecretStr | None = None diff --git a/backend/src/kf_oj/infra/__init__.py b/backend/src/kf_oj/infra/__init__.py new file mode 100644 index 0000000..fb6c8be --- /dev/null +++ b/backend/src/kf_oj/infra/__init__.py @@ -0,0 +1 @@ +"""Shared database, messaging and artifact adapters; no HTTP or business orchestration.""" diff --git a/backend/src/kf_oj/infra/artifacts/__init__.py b/backend/src/kf_oj/infra/artifacts/__init__.py new file mode 100644 index 0000000..38d4a81 --- /dev/null +++ b/backend/src/kf_oj/infra/artifacts/__init__.py @@ -0,0 +1 @@ +"""Reserved for authenticated artifact access and digest verification.""" diff --git a/backend/src/kf_oj/infra/db/__init__.py b/backend/src/kf_oj/infra/db/__init__.py new file mode 100644 index 0000000..f36de67 --- /dev/null +++ b/backend/src/kf_oj/infra/db/__init__.py @@ -0,0 +1 @@ +"""Database primitives; all schema migrations live in backend/migrations.""" diff --git a/backend/src/kf_oj/infra/db/base.py b/backend/src/kf_oj/infra/db/base.py new file mode 100644 index 0000000..932a4b8 --- /dev/null +++ b/backend/src/kf_oj/infra/db/base.py @@ -0,0 +1,14 @@ +from sqlalchemy import MetaData +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + metadata = MetaData( + naming_convention={ + "ix": "ix_%(table_name)s_%(column_0_name)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", + } + ) diff --git a/backend/src/kf_oj/infra/db/session.py b/backend/src/kf_oj/infra/db/session.py new file mode 100644 index 0000000..02806dd --- /dev/null +++ b/backend/src/kf_oj/infra/db/session.py @@ -0,0 +1,13 @@ +from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine + +from kf_oj.core.settings import Settings + + +def build_engine(settings: Settings) -> AsyncEngine: + if settings.database_url is None: + raise ValueError("KF_OJ_DATABASE_URL is required for database-backed operations") + return create_async_engine(settings.database_url.get_secret_value(), pool_pre_ping=True) + + +def build_session_factory(engine: AsyncEngine) -> async_sessionmaker: + return async_sessionmaker(engine, expire_on_commit=False) diff --git a/backend/src/kf_oj/infra/messaging/__init__.py b/backend/src/kf_oj/infra/messaging/__init__.py new file mode 100644 index 0000000..a7f11f0 --- /dev/null +++ b/backend/src/kf_oj/infra/messaging/__init__.py @@ -0,0 +1 @@ +"""Reserved for AMQP publishing/consuming adapters, confirms and retry transport.""" diff --git a/backend/src/kf_oj/modules/__init__.py b/backend/src/kf_oj/modules/__init__.py new file mode 100644 index 0000000..204b989 --- /dev/null +++ b/backend/src/kf_oj/modules/__init__.py @@ -0,0 +1 @@ +"""Business modules; cross-module calls go through explicit public application interfaces.""" diff --git a/backend/src/kf_oj/modules/community/README.md b/backend/src/kf_oj/modules/community/README.md new file mode 100644 index 0000000..374c288 --- /dev/null +++ b/backend/src/kf_oj/modules/community/README.md @@ -0,0 +1,6 @@ +# community + +负责题目讨论、题解、评论、作者归属、可见性及内容修改记录。 +题目和账号信息通过对应模块的公开接口查询,不直接操作其 ORM 表。 +技能社区的技能版本与发布语义归 `skills` 管理,本模块只提供可复用的内容/反馈能力。 +当前未实现社区接口。 diff --git a/backend/src/kf_oj/modules/community/__init__.py b/backend/src/kf_oj/modules/community/__init__.py new file mode 100644 index 0000000..f2b7616 --- /dev/null +++ b/backend/src/kf_oj/modules/community/__init__.py @@ -0,0 +1 @@ +"""Problem discussions, solutions, comments and content visibility (PF-04).""" diff --git a/backend/src/kf_oj/modules/identity/README.md b/backend/src/kf_oj/modules/identity/README.md new file mode 100644 index 0000000..299e021 --- /dev/null +++ b/backend/src/kf_oj/modules/identity/README.md @@ -0,0 +1,5 @@ +# identity + +负责账号、登录凭证、会话与角色;向其他模块提供经过验证的用户身份和授权依据。 +后续 `public.py` 暴露身份查询接口,`service.py` 编排用例,`repository.py` 封装持久化。 +用户身份不能来自前端自报字段或模型参数。目前仅建立模块边界,未实现登录。 diff --git a/backend/src/kf_oj/modules/identity/__init__.py b/backend/src/kf_oj/modules/identity/__init__.py new file mode 100644 index 0000000..03de82b --- /dev/null +++ b/backend/src/kf_oj/modules/identity/__init__.py @@ -0,0 +1 @@ +"""Accounts, credentials, roles and trusted user identity (PF-02).""" diff --git a/backend/src/kf_oj/modules/learning/README.md b/backend/src/kf_oj/modules/learning/README.md new file mode 100644 index 0000000..43d3f90 --- /dev/null +++ b/backend/src/kf_oj/modules/learning/README.md @@ -0,0 +1,7 @@ +# learning + +负责学习证据、统计与代码差异、题库筛选、分层答疑、计划校验和反馈。 +读取其他业务域的授权公开接口,通过组合层向 `runtime` 注入受控业务工具。 +规则推荐属于业务模块;步骤执行、预算与检查点属于 `runtime`。 +基础设施重试不能算成学生失败;模型输出必须校验后才能保存业务结果。 +当前未接入模型,也未创建训练计划表。 diff --git a/backend/src/kf_oj/modules/learning/__init__.py b/backend/src/kf_oj/modules/learning/__init__.py new file mode 100644 index 0000000..a270d0f --- /dev/null +++ b/backend/src/kf_oj/modules/learning/__init__.py @@ -0,0 +1 @@ +"""Evidence, recommendations, tutoring, training plans and feedback (AI-01..05).""" diff --git a/backend/src/kf_oj/modules/operations/README.md b/backend/src/kf_oj/modules/operations/README.md new file mode 100644 index 0000000..9681a78 --- /dev/null +++ b/backend/src/kf_oj/modules/operations/README.md @@ -0,0 +1,5 @@ +# operations + +负责平台侧节点、任务积压、重试原因和审计查询的管理视图。 +对判题状态的操作必须调用协调器协议;观测模块不成为第二个状态写入入口。 +当前只有目录边界,节点登记、指标聚合和管理员接口待实现。 diff --git a/backend/src/kf_oj/modules/operations/__init__.py b/backend/src/kf_oj/modules/operations/__init__.py new file mode 100644 index 0000000..e096dbd --- /dev/null +++ b/backend/src/kf_oj/modules/operations/__init__.py @@ -0,0 +1 @@ +"""Read-only node/task observability and administrator views (PF-05).""" diff --git a/backend/src/kf_oj/modules/problems/README.md b/backend/src/kf_oj/modules/problems/README.md new file mode 100644 index 0000000..9b62478 --- /dev/null +++ b/backend/src/kf_oj/modules/problems/README.md @@ -0,0 +1,5 @@ +# problems + +负责题目逻辑身份、不可变版本、导入、标签、限制、发布状态与测试工件元数据。 +跨模块访问通过后续 `public.py`;大文件存取通过 `infra/artifacts` 的适配器。 +隐藏数据与公开样例分别授权。当前未创建题库表或导入接口。 diff --git a/backend/src/kf_oj/modules/problems/__init__.py b/backend/src/kf_oj/modules/problems/__init__.py new file mode 100644 index 0000000..0fc7c32 --- /dev/null +++ b/backend/src/kf_oj/modules/problems/__init__.py @@ -0,0 +1 @@ +"""Problem catalog, immutable versions, import and publication (PF-02).""" diff --git a/backend/src/kf_oj/modules/skills/README.md b/backend/src/kf_oj/modules/skills/README.md new file mode 100644 index 0000000..a398830 --- /dev/null +++ b/backend/src/kf_oj/modules/skills/README.md @@ -0,0 +1,7 @@ +# skills + +负责声明式技能包、不可变版本、个人绑定和参数、启停及后续分享生命周期。 +包内容与用户私有学习档案分开存储;工具声明不能增加后端授权。 +`runtime` 只接收已经校验的固定版本和参数,不导入本模块实现。 +仓库根目录 `skills/` 保存未来平台维护的静态内置内容;用户绑定属于数据库业务记录。 +当前未建立包解析器,不执行任何插件脚本。 diff --git a/backend/src/kf_oj/modules/skills/__init__.py b/backend/src/kf_oj/modules/skills/__init__.py new file mode 100644 index 0000000..044794b --- /dev/null +++ b/backend/src/kf_oj/modules/skills/__init__.py @@ -0,0 +1 @@ +"""Skill definitions, immutable versions, user bindings and publication (SP-01..04).""" diff --git a/backend/src/kf_oj/modules/submissions/README.md b/backend/src/kf_oj/modules/submissions/README.md new file mode 100644 index 0000000..28d657c --- /dev/null +++ b/backend/src/kf_oj/modules/submissions/README.md @@ -0,0 +1,6 @@ +# submissions + +负责提交幂等请求、不可变源码引用、创建初始评测、历史查询与学生可见结果。 +创建提交、评测和待投递事件必须处于同一事务。 +协调器拥有领取后的执行状态与最终结果写入权;本模块不能绕过协议覆盖终态。 +后续 `public.py` 提供授权后的学习证据入口。当前未实现提交接口。 diff --git a/backend/src/kf_oj/modules/submissions/__init__.py b/backend/src/kf_oj/modules/submissions/__init__.py new file mode 100644 index 0000000..9672f09 --- /dev/null +++ b/backend/src/kf_oj/modules/submissions/__init__.py @@ -0,0 +1 @@ +"""Source snapshots, submission creation, evaluation references and queries (PF-03).""" diff --git a/backend/src/kf_oj/runtime/__init__.py b/backend/src/kf_oj/runtime/__init__.py new file mode 100644 index 0000000..293509b --- /dev/null +++ b/backend/src/kf_oj/runtime/__init__.py @@ -0,0 +1,5 @@ +"""Reserved for bounded Agent execution, tool ports, budgets and checkpoints. + +Application tools are injected by the learning composition layer. This package +does not import business modules, database adapters or any model SDK. +""" diff --git a/backend/src/kf_oj/workers/__init__.py b/backend/src/kf_oj/workers/__init__.py new file mode 100644 index 0000000..26f6223 --- /dev/null +++ b/backend/src/kf_oj/workers/__init__.py @@ -0,0 +1 @@ +"""Independent process composition roots; task loops are implemented per milestone.""" diff --git a/backend/src/kf_oj/workers/bootstrap.py b/backend/src/kf_oj/workers/bootstrap.py new file mode 100644 index 0000000..c9e9480 --- /dev/null +++ b/backend/src/kf_oj/workers/bootstrap.py @@ -0,0 +1,25 @@ +import argparse +import json + +from kf_oj import __version__ +from kf_oj.core.settings import Settings + + +def check_entrypoint(service: str) -> None: + parser = argparse.ArgumentParser(description=f"{service} process entrypoint") + parser.add_argument("--check", action="store_true", help="Validate configuration and exit") + args = parser.parse_args() + settings = Settings() + if not args.check: + parser.exit(2, f"{service}: task loop is not implemented; use --check\n") + print( + json.dumps( + { + "service": service, + "version": __version__, + "environment": settings.environment, + "status": "scaffold", + "consuming_tasks": False, + } + ) + ) diff --git a/backend/src/kf_oj/workers/learning.py b/backend/src/kf_oj/workers/learning.py new file mode 100644 index 0000000..a9fd012 --- /dev/null +++ b/backend/src/kf_oj/workers/learning.py @@ -0,0 +1,9 @@ +from kf_oj.workers.bootstrap import check_entrypoint + + +def main() -> None: + check_entrypoint("learning-worker") + + +if __name__ == "__main__": + main() diff --git a/backend/src/kf_oj/workers/outbox.py b/backend/src/kf_oj/workers/outbox.py new file mode 100644 index 0000000..1e38949 --- /dev/null +++ b/backend/src/kf_oj/workers/outbox.py @@ -0,0 +1,9 @@ +from kf_oj.workers.bootstrap import check_entrypoint + + +def main() -> None: + check_entrypoint("outbox-publisher") + + +if __name__ == "__main__": + main() diff --git a/backend/tests/test_contracts.py b/backend/tests/test_contracts.py new file mode 100644 index 0000000..250ab53 --- /dev/null +++ b/backend/tests/test_contracts.py @@ -0,0 +1,26 @@ +import json +from pathlib import Path + +import pytest +from jsonschema import Draft202012Validator, FormatChecker +from pydantic import ValidationError + +from kf_oj.contracts.judge import JudgeTaskNotification + +ROOT = Path(__file__).resolve().parents[2] +FIXTURES = ROOT / "tests/fixtures/contracts" +SCHEMA = json.loads((ROOT / "contracts/schemas/judge-task.v1.schema.json").read_text()) + + +@pytest.mark.parametrize("path", sorted(FIXTURES.glob("judge-task.*.json")), ids=lambda p: p.stem) +def test_shared_wire_examples(path: Path) -> None: + payload = json.loads(path.read_text()) + validator = Draft202012Validator(SCHEMA, format_checker=FormatChecker()) + if ".invalid-" in path.name: + with pytest.raises(ValidationError): + JudgeTaskNotification.model_validate(payload) + assert not validator.is_valid(payload) + else: + notification = JudgeTaskNotification.model_validate(payload) + validator.validate(payload) + assert notification.model_dump(mode="json") == payload diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..76a1d16 --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,33 @@ +import asyncio + +import httpx + +from kf_oj.api.app import create_app +from kf_oj.core.settings import Settings + + +def test_liveness_without_database_or_queue() -> None: + settings = Settings(_env_file=None, environment="test", database_url=None, amqp_url=None) + + async def request(): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=create_app(settings)), base_url="http://test" + ) as client: + return await client.get("/api/v1/health/live") + + response = asyncio.run(request()) + assert response.status_code == 200 + assert response.json() == {"status": "ok", "service": "platform", "version": "0.0.1"} + + +def test_production_disables_api_documentation() -> None: + app = create_app(Settings(_env_file=None, environment="production")) + + async def request(): + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), base_url="http://test" + ) as client: + for path in ("/api/docs", "/api/openapi.json"): + assert (await client.get(path)).status_code == 404 + + asyncio.run(request()) diff --git a/backend/uv.lock b/backend/uv.lock new file mode 100644 index 0000000..34143de --- /dev/null +++ b/backend/uv.lock @@ -0,0 +1,677 @@ +version = 1 +revision = 5 +requires-python = "==3.13.*" + +[[package]] +name = "aio-pika" +version = "9.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiormq" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" }, +] + +[[package]] +name = "aiormq" +version = "6.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pamqp" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" }, +] + +[[package]] +name = "alembic" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/aa/02910bdb8e2f1444f6654d5b296cd827d126f82209050ee7b1000f92ac4b/alembic-1.20.0.tar.gz", hash = "sha256:db505480647bc60386c5369402f4a57a506b7539c9e9ef5e270d45cbbe4939bf", size = 2093272, upload-time = "2026-09-11T19:09:11.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/78a89b55b0904d222183164e079b4ca56208e94eff1d35ad1f1ad5be9b06/alembic-1.20.0-py3-none-any.whl", hash = "sha256:77eb101048d95f982c0353e9233404889dcd7a6fc244c107836c0e2fc9cf7d9d", size = 268719, upload-time = "2026-09-11T19:09:12.88Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.142.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/4f/f7c30a73127e0a8bbffe788369b8359e530b01ae06e2757936fa35bc5e6d/fastapi-0.142.2.tar.gz", hash = "sha256:06366626f2e70576367714d9ab2fe8472e6c8456dba69b399f9f797ab5e92570", size = 466721, upload-time = "2026-09-30T09:51:11.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/b6/78aaf9141fb46742928c113f3cf6ef2259d538cb02b604b7656c1dc9883c/fastapi-0.142.2-py3-none-any.whl", hash = "sha256:bd5f4d81f1e93a88bcd77caf4dfe3c2dbffc3805407a0007e9a114c18b3a670b", size = 144409, upload-time = "2026-09-30T09:51:09.8Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7c/40400455f5b5a65bb83e94fde66d1be9e5ec518638113f8083ace746c309/greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e", size = 632632, upload-time = "2026-09-14T15:25:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/1f35cff30a6c51c3f23b63d4afcc7313ab4f97490ba3676fa78178984b27/greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e", size = 434933, upload-time = "2026-09-14T15:28:38.858Z" }, + { url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" }, + { url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kf-oj-platform" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "aio-pika" }, + { name = "alembic" }, + { name = "asyncpg" }, + { name = "fastapi" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "jsonschema" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aio-pika", specifier = ">=9.5,<10" }, + { name = "alembic", specifier = ">=1.15,<2" }, + { name = "asyncpg", specifier = ">=0.30,<1" }, + { name = "fastapi", specifier = ">=0.115,<1" }, + { name = "pydantic", specifier = ">=2.10,<3" }, + { name = "pydantic-settings", specifier = ">=2.7,<3" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0,<3" }, + { name = "uvicorn", specifier = ">=0.34,<1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.28,<1" }, + { name = "jsonschema", specifier = ">=4.23,<5" }, + { name = "pytest", specifier = ">=8.3,<10" }, + { name = "ruff", specifier = ">=0.11,<1" }, +] + +[[package]] +name = "mako" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/09/e07c4b5579a79f4b16f8d4f29f6c54514ac787c4ad506b8c4f28a0e6b0bf/mako-1.4.3.tar.gz", hash = "sha256:cd6537fe88d5fec315c55c2f8529bc4ce7a9a352ad7db3eeaa6a66e2dd4ec37a", size = 412799, upload-time = "2026-09-22T20:54:31.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/a0/053d6af3e8f871e0073b4a36732d9e65be77a72e5434c31b94f6af78a6bb/mako-1.4.3-py3-none-any.whl", hash = "sha256:723296007c870bfd6b3f0c3230dba7198096e5269297ebf5e4eff9e7ffa39d4f", size = 80164, upload-time = "2026-09-22T20:54:33.128Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, +] + +[[package]] +name = "multidict" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/64/642465a4827331a98ba4ae29f97658be25d1bdcb868872a2f78f7482b507/multidict-7.0.0.tar.gz", hash = "sha256:a7fcd089a0af2e0ef053c0d39c22c9ebf2434dddcb91034fd2f59ec99623788e", size = 280561, upload-time = "2026-09-26T19:36:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/6e/c388fccb88dd6cb41ce2454e420c869fb0629ce10bfe12e4cad578ba940d/multidict-7.0.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:15d9ad70615ce1b37c124445145f071ba4eaa88cb21b49a6b81727cc85aefc37", size = 92310, upload-time = "2026-09-26T19:32:24.366Z" }, + { url = "https://files.pythonhosted.org/packages/58/2f/33505a5c498613d53ab1d87b3c9425f6757afc7e48c2409f7aa001d0cdc1/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:313e336c75a53447cd943d93235c4702428b9740cb681e7174e398ba1f3f2918", size = 85114, upload-time = "2026-09-26T19:32:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/76/f5/27b19597821cb80d082b649e5e6e01033aed5e0eebdbbc6a644eb49515a7/multidict-7.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a4838959217e2652ef7dce1d01bb370aed3e5b694eb7a8258fa1cc50beef08ca", size = 86952, upload-time = "2026-09-26T19:32:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/a5e44d5453c8bb295979d242cfd3beee06f6ab9f6c1c06a13c2191b792b1/multidict-7.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd6520538e19fe104c3b74cc64767d6f8959dd2c9b8d59da62d83641ea646deb", size = 147356, upload-time = "2026-09-26T19:32:28.816Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/4da412a7267ac85759300a078f3bdad986e18a30c1ead8b74d2e3b335a2b/multidict-7.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e9bf055becb08ff797d5cf80b760b726b8babb39046564d2b8481bbe6504bce", size = 88022, upload-time = "2026-09-26T19:32:30.44Z" }, + { url = "https://files.pythonhosted.org/packages/b6/46/bebeb7df10298d390942e61bc7fc48472183ee4f5a0b7c285d93369b6874/multidict-7.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8e443cab381f8b6d24950982ae508600d464412cbdcf78e263cf8cd9b8372d68", size = 87046, upload-time = "2026-09-26T19:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d3/38b254c21cb8e9a65f8751ad8ba0a6f9baad75e3b6d3e559d1d05db26482/multidict-7.0.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e45bb7b9c850f216c0081dab8c6862fdec6b8be1c800b2a1aa4d1e56dcfacb54", size = 488729, upload-time = "2026-09-26T19:32:33.338Z" }, + { url = "https://files.pythonhosted.org/packages/2a/28/19d66c476cc44fffdeacd5227a59046f94612075008069286bdbb9d3fa35/multidict-7.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2f742000b47d806e5798f5ef0cdb2ed43d822fc913331a33ed1a40515495a7", size = 496708, upload-time = "2026-09-26T19:32:35.286Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/3bb2d2cdf67742b002985906785b0db1584ca2e67ddb3677f6e9614321d6/multidict-7.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70e28954ae5ae3c1806aa6d85bbb26ef7ab73017228eb236a3f245e24720bb34", size = 458261, upload-time = "2026-09-26T19:32:37.027Z" }, + { url = "https://files.pythonhosted.org/packages/af/5c/c8ca15c191f957e8d86ec65148ed1e8ca189f456e66153e23c4ae610649a/multidict-7.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c41ccfa34b152146ccf05e8da753a85f8e67437477f8a00783aa427a132abd31", size = 511932, upload-time = "2026-09-26T19:32:39.025Z" }, + { url = "https://files.pythonhosted.org/packages/14/ff/0665b2c3d5e42165a96fffc4c34c3d5fa66d0819d82b9bc2977b9a6aa1ed/multidict-7.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a1de5f1da3ae056b45b6599a53dba6c86879bdfa5c3f15d7dde99f6cfbc03c6", size = 529152, upload-time = "2026-09-26T19:32:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f7/967875e5d1f908613cfcd5d9d078011168acb88a128028b81a0de43e8aa6/multidict-7.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:094852ebc620a10ee0e11f36a68e016d73596c0f5ed8351a9a52fcff7d449ad6", size = 506337, upload-time = "2026-09-26T19:32:42.839Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f9/56f04cf560bf736c23ba9cacc1ae3fcd2bbb591ba4f166c4bc08f5fead61/multidict-7.0.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03cc4027103bb61feac20336a15fced1a6a369fed6a61c516353921207e4578c", size = 448144, upload-time = "2026-09-26T19:32:44.556Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/ff7978bb38f2e2908cf7fc80061175cbfacdcada248dc48fb79a10c0e92b/multidict-7.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0fef8622896443fd0bdf39586391c2162ac05004ba4c9918827dfd2f79da750c", size = 485535, upload-time = "2026-09-26T19:32:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/1e9a6457587da0a31b3ef31ca0a40624877ca29408c8a7385260a3d45cd9/multidict-7.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1847d33ca198aba3bf5fad036a7e0376cca325e7ec89f26362f9aa48994d8146", size = 471078, upload-time = "2026-09-26T19:32:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/ee11505f0ae23a344544010485db982290c0e6105caefd74ce3830c315ca/multidict-7.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b5c93f582f8a973ea803c46097729e34969683bc99f99fc00321b1f76317e81", size = 490764, upload-time = "2026-09-26T19:32:49.861Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8d/1e954f7cbb1f2080c2712b6a4cb6ce196037c06c686ae030003465b65ad7/multidict-7.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7c717376f801dd1ee102b292e0a4ca6037e948a8710316e64e698fe39eeb1307", size = 500752, upload-time = "2026-09-26T19:32:51.756Z" }, + { url = "https://files.pythonhosted.org/packages/da/54/0f3899f51364f502219192b8c03da123c8618e9709499194ecd8e4143cc9/multidict-7.0.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c6d6e73b9f7b70558de5624b036815464a7ead4671fe2c3a4b3ce444c240cba6", size = 444706, upload-time = "2026-09-26T19:32:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/54/03/8edc3cb05a35185a4910582a50aa4dd342cceda3b26b2053dbd004adc9e8/multidict-7.0.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:18f627c69d041ae17d8fb45dbc63e192a2afc3ba062195441b5a50e93c50ae68", size = 511149, upload-time = "2026-09-26T19:32:55.281Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b4/4234b76f97b2966d1de2f3507f5dc9af4072e2ed16cfb476855490e34ae9/multidict-7.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8477e14353097077e1011464794d11e1d7559e821ab7c612cf7c06e7826ee1ba", size = 501451, upload-time = "2026-09-26T19:32:57.073Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/fe7965b74cb3cf822d3e88063f13e3eff2600c36022f69c603b5b52f3e21/multidict-7.0.0-cp313-cp313-win32.whl", hash = "sha256:2efca6de4fbefc12f2aaac2d5e1c176a3864d22250246b44fe3823e3634f06f8", size = 76414, upload-time = "2026-09-26T19:32:58.84Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c7/a4960e81ba8a3a9698011d1ac92d4d11ce8c9bb0ed6d235de76c49cae3d1/multidict-7.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:188a7f89a1935a138237088bff22400bacd25d80fb0d3a8a26fab25961df2239", size = 87005, upload-time = "2026-09-26T19:33:00.576Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/7f6b0898c895f2b39ebaa3fc34d32537d263aa5c74809ee747b5e705da80/multidict-7.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:debb701a0436c96f2717c6156428a2c8bce2559236e120a674d503aeed678aa3", size = 85466, upload-time = "2026-09-26T19:33:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/46/a4/a3e9e72ed0f99587e9e96c3de72b743e1385e5e2b15fb583d8dfc05ffffb/multidict-7.0.0-py3-none-any.whl", hash = "sha256:1ba6a45450243bd8953223adff7183fce5e3f9f3556a03bd17a674caaa046215", size = 26592, upload-time = "2026-09-26T19:36:09.764Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/dc/e12c1fe1ed8a7b7149777127b1a0e12ce5bd5a81d97408bedc2128c260f5/opentelemetry_api-1.45.0.tar.gz", hash = "sha256:711ede81773c8025c2c03dac0450bc89f3d30aea6eabcc815c570d4e35a963f7", size = 72115, upload-time = "2026-09-25T12:32:28.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/b9/040d1a1c7836922828e6480cd2366bb8fe0ebf75b413d2bb51a9b0e7f78f/opentelemetry_api-1.45.0-py3-none-any.whl", hash = "sha256:80e068aba7cd56c8b58512d6a36f8d25cb1dfaa0c0a4cc1c938ccf9f362d9cb3", size = 60020, upload-time = "2026-09-25T12:32:03.192Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/4b/81d972a46c9f1d978af1795e2abc988855c711453552cb45d75f6e4abbf5/sqlalchemy-2.1.3.tar.gz", hash = "sha256:ade5281df06038c6394f532590d592d3381ee5d11b9e0006a2570aef41145118", size = 10525778, upload-time = "2026-10-02T22:57:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/71/68c29931fb2c5178241f7d0c63dbdd800ad65685eac70d7f2117145b5e3c/sqlalchemy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:260bdd8baa1dc631d76d14b26ad009ebbb665b4469666afef8dcaa1f964016cb", size = 2457479, upload-time = "2026-10-03T02:39:19.741Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/5dbd91816b2cdb367bca3195f300ffeca916762e088d6924fc33a18f50ad/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fc01a8047d6a4f4e997441994f07af79fdd470fc80669751e32f91153d6b491", size = 4582199, upload-time = "2026-10-02T23:37:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/79962ce06d6cd34516def113f5170184fa9401ebddc59fd6cb18f6e919f3/sqlalchemy-2.1.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b1b3ff10eba0f9290142375e667351c99d24ee7fcff6e0f85bd8be9d39b50a17", size = 4632923, upload-time = "2026-10-02T23:35:55.318Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/03e98031dc9a1738c580c3bf01beeafe695f247eb647f4e8dda3fab2454f/sqlalchemy-2.1.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:efb1ebf695e80d9a69f5bbff9d812d2491fd168b27d6b42ad4c8a4d3d6faaef1", size = 4300224, upload-time = "2026-10-02T23:46:45.495Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1f/388c87607eb6c7abee4866355eeebb6221b3bcb413a64ce4a5cfc3edd22f/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a95754928d47b37ea5c4b5180101db2afabef1da0dd5cd0c573b547d5d036ff0", size = 4507170, upload-time = "2026-10-02T23:38:29.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/73/5515a3f555d31748564fbeb0b9e0b6ab7a647bf2ea0cdb6d233f20f710e3/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7606b413730ac6002930faa96f52195ed6969040b78071a3336370661f5e9e09", size = 4301439, upload-time = "2026-10-02T23:46:47.845Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a0/0eae9b96f6f8a09f3e5d388f6fc5d6fe8d5ff2aced2a5c75bea7333d445e/sqlalchemy-2.1.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76c3d5c7ada0925ff12e8c2395c547f88fcfccc13368ff8e8dd55ab5e28dcdab", size = 4594441, upload-time = "2026-10-03T02:46:16.928Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/b6d681015414ce9048c07ef3ac49b693c1b8373de4716ee4bdeff4ec19a0/sqlalchemy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:cb6fc5c856e8f1c0228bf74b636c0a03a6c12dd502ced7aa82ced21dcdc7f547", size = 2371547, upload-time = "2026-10-03T02:49:23.549Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/35aa4d737016640c6da7573a4bcd9d91b59a87359bc6df075627963a4781/sqlalchemy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:82d4c63d338737d593f1d29c4dae0a124577a9a76186d90b5c0f9ec9cbeeab35", size = 2421558, upload-time = "2026-10-03T02:49:25.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/42/0dcc7cd92a3f4773376a5488b6ce0d6d75d2d029ad7e7f9e0ecc5fb48fcd/sqlalchemy-2.1.3-cp313-cp313-win_arm64.whl", hash = "sha256:0ab6f42761cc5c48d2f3e8a70810d10630d8222a51a7111cf9b3a48f22c3f148", size = 2382507, upload-time = "2026-10-03T02:42:42.812Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/23d8fb7ed90e5b674006ace415ede1c60cdfc38ace50968b59ef42c22754/sqlalchemy-2.1.3-py3-none-any.whl", hash = "sha256:6f0cf4debd86cb5623a50310784bd005c215761ca7f618d0d4f46193d033675a", size = 2052603, upload-time = "2026-10-03T02:34:19.115Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + +[[package]] +name = "starlette" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, +] + +[[package]] +name = "yarl" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, +] diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..054fe8d --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,14 @@ +# 跨服务契约 + +`schemas/` 保存版本化 JSON Schema,`openapi/` 保存平台 API 快照。 +共享合法/非法样例位于 `tests/fixtures/contracts/`,Python 与 Rust 必须共同验证。 + +目前只固定最小判题通知:`schema_version=1`、`kind=judge.evaluation`、UUID 格式的 +`evaluation_id`。它不包含源码、隐藏数据、租约或执行授权;消费者仍须向协调器领取。 +未知版本、未知类型及未知字段均拒绝。 + +`make contracts` 从 Python 模型生成 Schema/OpenAPI,并生成前端 API 类型。 +`make check` 检查快照未过期及两种语言对样例的接受/拒绝一致。 +执行请求/报告、评测状态机、租约与结果提交契约在 JS-01/DR-01 中逐项补充。 + +生成器 `export.py` 是开发工具,允许读取 API 组合层;运行期的契约模型禁止依赖业务模块。 diff --git a/contracts/openapi/platform.v1.json b/contracts/openapi/platform.v1.json new file mode 100644 index 0000000..4401d71 --- /dev/null +++ b/contracts/openapi/platform.v1.json @@ -0,0 +1,62 @@ +{ + "components": { + "schemas": { + "HealthResponse": { + "additionalProperties": false, + "properties": { + "service": { + "const": "platform", + "title": "Service", + "type": "string" + }, + "status": { + "const": "ok", + "title": "Status", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "status", + "service", + "version" + ], + "title": "HealthResponse", + "type": "object" + } + } + }, + "info": { + "description": "工程骨架。当前只提供进程存活检查,业务接口逐项实现。", + "title": "KF-OnlineJudge Platform API", + "version": "0.0.1" + }, + "openapi": "3.1.0", + "paths": { + "/api/v1/health/live": { + "get": { + "description": "Check process liveness only; does not attest DB, queue or judge readiness.", + "operationId": "liveness", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + }, + "description": "Successful Response" + } + }, + "summary": "Liveness", + "tags": [ + "health" + ] + } + } + } +} diff --git a/contracts/schemas/judge-task.v1.schema.json b/contracts/schemas/judge-task.v1.schema.json new file mode 100644 index 0000000..64475fb --- /dev/null +++ b/contracts/schemas/judge-task.v1.schema.json @@ -0,0 +1,30 @@ +{ + "$id": "urn:kf-oj:judge-task:v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "description": "A wake-up notification, not execution authority or the source of task state.", + "properties": { + "evaluation_id": { + "format": "uuid", + "title": "Evaluation Id", + "type": "string" + }, + "kind": { + "const": "judge.evaluation", + "title": "Kind", + "type": "string" + }, + "schema_version": { + "const": 1, + "title": "Schema Version", + "type": "integer" + } + }, + "required": [ + "schema_version", + "kind", + "evaluation_id" + ], + "title": "JudgeTaskNotification", + "type": "object" +} diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..844ded3 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,10 @@ +# Development-only values; copy to deploy/.env before make infra-up. +POSTGRES_USER=kf_oj +POSTGRES_PASSWORD=local-dev-only +POSTGRES_DB=kf_oj +POSTGRES_PORT=5432 +RABBITMQ_USER=kf_oj +RABBITMQ_PASSWORD=local-dev-only +RABBITMQ_VHOST=kf_oj +RABBITMQ_PORT=5672 +RABBITMQ_MANAGEMENT_PORT=15672 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..c91f14e --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,21 @@ +# 本地开发依赖 + +`compose.dev.yml` 只提供 PostgreSQL 和 RabbitMQ,端口绑定 localhost。 +先复制 `.env.example` 为 `.env`,然后在仓库根目录运行: + +```bash +make infra-config +make infra-up +make infra-down +``` + +`infra-down` 保留数据卷;不要把示例口令用于正式部署。 +API 存活检查和前端预览不依赖这些容器,可以先独立启动。 +该 Compose 不运行判题 Worker 或不可信代码。 + +镜像固定具体维护版本(PostgreSQL 18.6、RabbitMQ 4.3.6),正式实验前另锁镜像摘要。 +配置检查只验证 Compose 结构;成功启动、实际健康与备份恢复需要单独验证。 +后续受控主机服务和最小权限沙箱启动器配置放在本目录。 + +官方镜像说明:[PostgreSQL](https://hub.docker.com/_/postgres)、 +[RabbitMQ](https://hub.docker.com/_/rabbitmq)。 diff --git a/deploy/compose.dev.yml b/deploy/compose.dev.yml new file mode 100644 index 0000000..5eec5fc --- /dev/null +++ b/deploy/compose.dev.yml @@ -0,0 +1,40 @@ +name: kf-oj-dev +services: + postgres: + image: postgres:18.6-bookworm + environment: + POSTGRES_USER: ${POSTGRES_USER:?required} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?required} + POSTGRES_DB: ${POSTGRES_DB:?required} + ports: + - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 3s + retries: 20 + mem_limit: 1g + cpus: 1 + rabbitmq: + image: rabbitmq:4.3.6-management + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:?required} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?required} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:?required} + ports: + - "127.0.0.1:${RABBITMQ_PORT:-5672}:5672" + - "127.0.0.1:${RABBITMQ_MANAGEMENT_PORT:-15672}:15672" + volumes: + - rabbitmq-data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] + interval: 5s + timeout: 5s + retries: 20 + mem_limit: 768m + cpus: 1 +volumes: + postgres-data: + rabbitmq-data: diff --git a/experiments/README.md b/experiments/README.md new file mode 100644 index 0000000..166f5b6 --- /dev/null +++ b/experiments/README.md @@ -0,0 +1,7 @@ +# 可复现实验 + +后续负载生成、调度对照、故障注入和统计脚本放在本目录;工程骨架阶段未开始性能实验。 +每次实验保存 Git 提交、环境、配置、负载摘要、原始事件、结果和复现命令。 + +`results/` 默认忽略,报告只提交去标识摘要和原始材料的复现索引。 +每项实验先明确条件、正确性检查和验收指标,再运行并保留复现材料。 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..8974b4d --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL= +# Development proxy only; never bundled into the browser as a credential. +KF_API_PROXY_TARGET=http://127.0.0.1:8000 diff --git a/frontend/.node-version b/frontend/.node-version new file mode 100644 index 0000000..8a94b99 --- /dev/null +++ b/frontend/.node-version @@ -0,0 +1 @@ +26.10.0 diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..bcad92e --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,2 @@ +# pnpm 11 reads project settings from pnpm-workspace.yaml. +# Keep this file for future registry/auth configuration only. diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..cabbe58 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +pnpm-lock.yaml +dist +node_modules +src/api/generated diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..0965e7e --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..3cc7779 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,27 @@ +# Vue 前端工程 + +Vue 3 + TypeScript + Vite + Element Plus,使用 Vue Router 与 Pinia。 +当前页面是开发预览和实际 API 连接检查;没有模拟登录或模拟评测结果。 + +```bash +pnpm install --frozen-lockfile +pnpm dev +pnpm typecheck +pnpm test +pnpm build +``` + +开发服务在 `http://127.0.0.1:5173`,`/api` 代理到 `http://127.0.0.1:8000`。 +复制 `.env.example` 为 `.env` 可调整代理和 API 地址。 +生产部署须配置 SPA 路由回退及 `/api` 反向代理;开发代理不进入生产产物。 + +- `views/`:路由页面;`layouts/`:页面框架;`components/`:可复用界面组件。 +- `api/`:HTTP 客户端和业务 API 封装;`api/generated/`:从 OpenAPI 生成的类型。 +- `composables/`:带生命周期的交互逻辑;`stores/`:跨页面共享状态。 +- `router/`:路由;`styles/`:全局样式。 + +已有代码按实际需要落位;`components/` 和 `stores/` 是后续扩展位置,不放空状态实现。 +API 类型更新执行根目录 `make contracts`;前端通过 `pnpm check:api` 拒绝过期类型。 +登录凭据、模型密钥和数据库连接不能进入 `VITE_` 环境变量。 + +`pnpm test` 验证代码草稿在刷新、切到后台与页面离开时的保存行为,已纳入 `make check-frontend`。 diff --git a/frontend/admin/index.html b/frontend/admin/index.html new file mode 100644 index 0000000..ad457f0 --- /dev/null +++ b/frontend/admin/index.html @@ -0,0 +1,12 @@ + + + + + + KF 管理控制台 + + +
+ + + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..d00c768 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,22 @@ +import js from '@eslint/js' +import tseslint from 'typescript-eslint' +import vue from 'eslint-plugin-vue' +import prettier from 'eslint-config-prettier/flat' + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**', 'src/api/generated/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + ...vue.configs['flat/recommended'], + { + files: ['**/*.vue'], + languageOptions: { parserOptions: { parser: tseslint.parser } }, + // TypeScript checks identifiers and DOM globals in Vue's script setup. + rules: { 'vue/multi-word-component-names': 'off', 'no-undef': 'off' }, + }, + { + files: ['vite.config.ts'], + languageOptions: { globals: { process: 'readonly' } }, + }, + prettier, +) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..3472db5 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + KF-OnlineJudge + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..58f7e6c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "kf-oj-frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "packageManager": "pnpm@11.28.4", + "engines": { + "node": ">=24.12.0 <27" + }, + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview --host 127.0.0.1", + "typecheck": "vue-tsc --noEmit", + "test": "node --test tests/*.test.ts", + "lint": "eslint . --max-warnings 0", + "format": "prettier --write .", + "format:check": "prettier --check .", + "generate:api": "openapi-typescript ../contracts/openapi/platform.v1.json -o src/api/generated/schema.d.ts", + "check:api": "pnpm generate:api --check" + }, + "dependencies": { + "@element-plus/icons-vue": "2.3.2", + "@misskey-dev/tagcanvas-es": "0.1.2", + "element-plus": "^2.9.0", + "monaco-editor": "0.57.0", + "openapi-fetch": "^0.14.0", + "pinia": "^3.0.0", + "vue": "^3.5.0", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.0", + "@types/node": "^24.0.0", + "@vitejs/plugin-vue": "^6.0.0", + "eslint": "^10.0.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-vue": "^10.0.0", + "openapi-typescript": "^7.0.0", + "prettier": "^3.0.0", + "typescript": "~5.9.0", + "typescript-eslint": "^8.0.0", + "vite": "^7.0.0", + "vue-tsc": "^3.0.0" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..8616279 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2398 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: 2.3.2 + version: 2.3.2(vue@3.5.43(typescript@5.9.3)) + '@misskey-dev/tagcanvas-es': + specifier: 0.1.2 + version: 0.1.2 + element-plus: + specifier: ^2.9.0 + version: 2.14.7(vue@3.5.43(typescript@5.9.3)) + monaco-editor: + specifier: 0.57.0 + version: 0.57.0 + openapi-fetch: + specifier: ^0.14.0 + version: 0.14.1 + pinia: + specifier: ^3.0.0 + version: 3.0.4(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3)) + vue: + specifier: ^3.5.0 + version: 3.5.43(typescript@5.9.3) + vue-router: + specifier: ^4.5.0 + version: 4.6.4(vue@3.5.43(typescript@5.9.3)) + devDependencies: + '@eslint/js': + specifier: ^10.0.0 + version: 10.0.1(eslint@10.12.0(supports-color@10.2.2)) + '@types/node': + specifier: ^24.0.0 + version: 24.19.1 + '@vitejs/plugin-vue': + specifier: ^6.0.0 + version: 6.0.9(vite@7.3.6(@types/node@24.19.1))(vue@3.5.43(typescript@5.9.3)) + eslint: + specifier: ^10.0.0 + version: 10.12.0(supports-color@10.2.2) + eslint-config-prettier: + specifier: 10.1.8 + version: 10.1.8(eslint@10.12.0(supports-color@10.2.2)) + eslint-plugin-vue: + specifier: ^10.0.0 + version: 10.11.1(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)) + openapi-typescript: + specifier: ^7.0.0 + version: 7.13.0(typescript@5.9.3) + prettier: + specifier: ^3.0.0 + version: 3.9.9 + typescript: + specifier: ~5.9.0 + version: 5.9.3 + typescript-eslint: + specifier: ^8.0.0 + version: 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + vite: + specifier: ^7.0.0 + version: 7.3.6(@types/node@24.19.1) + vue-tsc: + specifier: ^3.0.0 + version: 3.3.12(typescript@5.9.3) + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@ctrl/tinycolor@4.2.1': + resolution: {integrity: sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==} + engines: {node: '>=14'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@misskey-dev/tagcanvas-es@0.1.2': + resolution: {integrity: sha512-SGqyu30/CCQyIC95Zcmj3k1vYXwfX66/arsxpl1UUulUqvlRpTpuhB7kEAAps1lqXin0rQzXknb31EeveNKsaw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.20': + resolution: {integrity: sha512-ypeBZ/6BKXR9+7/TtbKhbl4UgD7raHhPS12oknlKno2A8+lnFkxIwiE/Aklu6L2cd/ioH+fCWuMxi9/p3EyAPw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.64.0': + resolution: {integrity: sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.64.0': + resolution: {integrity: sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.64.0': + resolution: {integrity: sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.64.0': + resolution: {integrity: sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.64.0': + resolution: {integrity: sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.64.0': + resolution: {integrity: sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.64.0': + resolution: {integrity: sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.64.0': + resolution: {integrity: sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.64.0': + resolution: {integrity: sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.64.0': + resolution: {integrity: sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.64.0': + resolution: {integrity: sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.64.0': + resolution: {integrity: sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.64.0': + resolution: {integrity: sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.64.0': + resolution: {integrity: sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.64.0': + resolution: {integrity: sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.64.0': + resolution: {integrity: sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.64.0': + resolution: {integrity: sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.64.0': + resolution: {integrity: sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.64.0': + resolution: {integrity: sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.64.0': + resolution: {integrity: sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.64.0': + resolution: {integrity: sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.64.0': + resolution: {integrity: sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.64.0': + resolution: {integrity: sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.64.0': + resolution: {integrity: sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.64.0': + resolution: {integrity: sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==} + cpu: [x64] + os: [win32] + + '@sxzz/popperjs-es@2.11.8': + resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/node@24.19.1': + resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.71.0': + resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.71.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.71.0': + resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.71.0': + resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.9': + resolution: {integrity: sha512-rD/MORlhaZMlXWW0rEn4FB4wMinWC0z/D7Ye160S5+Rs1mC8ZDAdcDp4SjKfUhG5t8q1ktcPVw4xuTkPRlzrSA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/compiler-core@3.5.43': + resolution: {integrity: sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==} + + '@vue/compiler-dom@3.5.43': + resolution: {integrity: sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==} + + '@vue/compiler-sfc@3.5.43': + resolution: {integrity: sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==} + + '@vue/compiler-ssr@3.5.43': + resolution: {integrity: sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.10': + resolution: {integrity: sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==} + + '@vue/devtools-kit@7.7.10': + resolution: {integrity: sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==} + + '@vue/devtools-shared@7.7.10': + resolution: {integrity: sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==} + + '@vue/language-core@3.3.12': + resolution: {integrity: sha512-k7uIKR0n4CBidk233y7cac9osnDQ5WRidTl6nsrNvpZ3nf8cx9iJxRVsRzVsoPRHbowT/iKgGMTS9RGov3jeIA==} + + '@vue/reactivity@3.5.43': + resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==} + + '@vue/runtime-core@3.5.43': + resolution: {integrity: sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==} + + '@vue/runtime-dom@3.5.43': + resolution: {integrity: sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==} + + '@vue/server-renderer@3.5.43': + resolution: {integrity: sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==} + + '@vue/shared@3.5.43': + resolution: {integrity: sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==} + + '@vueuse/core@15.0.0': + resolution: {integrity: sha512-Zv67h5mrk7YHDJsRObx+DXaCBz6RKBUpxqLesJ+3xZNPG6yuQs7ja7kg3veFrrouYLWAqydQ7CwGEbLVxB43Ng==} + engines: {node: '>=22'} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@15.0.0': + resolution: {integrity: sha512-sfWE5LADAKJ0KnjtGliMdSN8Ij/L7uP6rXIZlgPloAI4zvvefbw6H5Lt7KJA6RqQOYgVqWF5Guj50HHMIzygBg==} + engines: {node: '>=22'} + + '@vueuse/shared@15.0.0': + resolution: {integrity: sha512-vbNXhGrbV53xPOMSyOzMHQRvhMgZf7oNDw2RChUetVxwTIEkInQ1joZ9JbT0G3z2n2AJNjvmCWeM9ySYrnvogA==} + engines: {node: '>=22'} + peerDependencies: + vue: ^3.5.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + copy-anything@4.1.5: + resolution: {integrity: sha512-vePst2VLTSqPglW7Q98VwFpGiGpOHTyXIo81MO5VyLecA5zMbZw/P/V1vmwD1jksa2iJpn/YkDrJ3XhTfzUyhQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dompurify@3.4.15: + resolution: {integrity: sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==} + + element-plus@2.14.7: + resolution: {integrity: sha512-lrG7pZyCS+mpCNVpzxxyu5r35+T6V0hzkAeIwn7HA6to772XSTHKuYCZG51OfzlqaxGbNegLxs650zAF1j43gA==} + peerDependencies: + vue: ^3.3.7 + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-vue@10.11.1: + resolution: {integrity: sha512-pasVXhl7wUmIvhujoqs/m0mJ9r59QW1IR9XXYKGR0FETzSM+Hhlf4qKIYgWiDDP+9YKv4YfbwskD7LqIdVcgQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + vue-eslint-parser: ^10.3.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.12.0: + resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.12: + resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + monaco-editor@0.57.0: + resolution: {integrity: sha512-5BkI9KGoqrNvBGUe15/QlZq3OooZ8WLg1AxTpaqHRCP3HNpzPPZKE2EDz8M7c+VRmCeUw1Brp4cx/PWm3kI/5A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + openapi-fetch@0.14.1: + resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==} + + openapi-typescript-helpers@0.0.15: + resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} + engines: {node: '>=4'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.64.0: + resolution: {integrity: sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + typescript-eslint@8.71.0: + resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + vue-component-type-helpers@3.3.12: + resolution: {integrity: sha512-HwCqmgTfLFskQc4P5TRnbduCMyPat2Ezu0elLWOGn0dD/6BKHP15B5bxce6aDi+E4kblPtru8amIBQzu0Di3qQ==} + + vue-eslint-parser@10.4.1: + resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.3.12: + resolution: {integrity: sha512-kbpaz18O9jPVkXVHD1BEjpUXM6WhXSu+62L1sGGr6Wbzd8GbvkBQWjoj3KZ/C0aq2MZllce7UdtWzwCIYsBMGA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.43: + resolution: {integrity: sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@ctrl/tinycolor@4.2.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.43(typescript@5.9.3))': + dependencies: + vue: 3.5.43(typescript@5.9.3) + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@10.2.2))': + dependencies: + eslint: 10.12.0(supports-color@10.2.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5(supports-color@10.2.2)': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.12.0(supports-color@10.2.2))': + optionalDependencies: + eslint: 10.12.0(supports-color@10.2.2) + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + + '@misskey-dev/tagcanvas-es@0.1.2': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.20(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.3.2 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.64.0': + optional: true + + '@rollup/rollup-android-arm64@4.64.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.64.0': + optional: true + + '@rollup/rollup-darwin-x64@4.64.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.64.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.64.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.64.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.64.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.64.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.64.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.64.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.64.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.64.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.64.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.64.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.64.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.64.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.64.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.64.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.64.0': + optional: true + + '@sxzz/popperjs-es@2.11.8': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/node@24.19.1': + dependencies: + undici-types: 7.24.6 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + eslint: 10.12.0(supports-color@10.2.2) + ignore: 7.0.12 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.12.0(supports-color@10.2.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.71.0(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + debug: 4.4.3(supports-color@10.2.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.12.0(supports-color@10.2.2) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.71.0': {} + + '@typescript-eslint/typescript-estree@8.71.0(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.71.0(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2)) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3) + eslint: 10.12.0(supports-color@10.2.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@6.0.9(vite@7.3.6(@types/node@24.19.1))(vue@3.5.43(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 7.3.6(@types/node@24.19.1) + vue: 3.5.43(typescript@5.9.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 + optionalDependencies: + typescript: 5.9.3 + + '@vue/compiler-core@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/shared': 3.5.43 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.2 + + '@vue/compiler-dom@3.5.43': + dependencies: + '@vue/compiler-core': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/compiler-sfc@3.5.43': + dependencies: + '@babel/parser': 7.29.9 + '@vue/compiler-core': 3.5.43 + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-ssr': 3.5.43 + '@vue/shared': 3.5.43 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.28 + source-map-js: 1.2.2 + + '@vue/compiler-ssr@3.5.43': + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.10': + dependencies: + '@vue/devtools-kit': 7.7.10 + + '@vue/devtools-kit@7.7.10': + dependencies: + '@vue/devtools-shared': 7.7.10 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.10': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.3.12': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.43 + '@vue/shared': 3.5.43 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.7 + + '@vue/reactivity@3.5.43': + dependencies: + '@vue/shared': 3.5.43 + + '@vue/runtime-core@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/runtime-dom@3.5.43': + dependencies: + '@vue/reactivity': 3.5.43 + '@vue/runtime-core': 3.5.43 + '@vue/shared': 3.5.43 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.43': + dependencies: + '@vue/compiler-ssr': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/shared': 3.5.43 + + '@vue/shared@3.5.43': {} + + '@vueuse/core@15.0.0(vue@3.5.43(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 15.0.0 + '@vueuse/shared': 15.0.0(vue@3.5.43(typescript@5.9.3)) + vue: 3.5.43(typescript@5.9.3) + + '@vueuse/metadata@15.0.0': {} + + '@vueuse/shared@15.0.0(vue@3.5.43(typescript@5.9.3))': + dependencies: + vue: 3.5.43(typescript@5.9.3) + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.2.1: {} + + ansi-colors@4.1.3: {} + + argparse@2.0.1: {} + + async-validator@4.2.5: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + birpc@2.9.0: {} + + boolbase@1.0.0: {} + + brace-expansion@2.1.7: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + + change-case@5.4.4: {} + + colorette@1.4.0: {} + + copy-anything@4.1.5: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + dayjs@1.11.23: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + deep-is@0.1.4: {} + + dompurify@3.4.15: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + element-plus@2.14.7(vue@3.5.43(typescript@5.9.3)): + dependencies: + '@ctrl/tinycolor': 4.2.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.43(typescript@5.9.3)) + '@floating-ui/dom': 1.8.0 + '@popperjs/core': '@sxzz/popperjs-es@2.11.8' + '@types/lodash': 4.17.25 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 15.0.0(vue@3.5.43(typescript@5.9.3)) + async-validator: 4.2.5 + dayjs: 1.11.23 + lodash: 4.18.1 + lodash-es: 4.18.1 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.43(typescript@5.9.3) + vue-component-type-helpers: 3.3.12 + + entities@7.0.1: {} + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@10.12.0(supports-color@10.2.2)): + dependencies: + eslint: 10.12.0(supports-color@10.2.2) + + eslint-plugin-vue@10.11.1(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2)) + eslint: 10.12.0(supports-color@10.2.2) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.6 + semver: 7.8.5 + vue-eslint-parser: 10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2) + xml-name-validator: 5.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.12.0(supports-color@10.2.2): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@10.2.2)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5(supports-color@10.2.2) + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 11.1.5 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.6 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@11.1.5: + dependencies: + flat-cache: 6.1.23 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@6.1.23: + dependencies: + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 + + flatted@3.4.4: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hookable@5.5.3: {} + + hookified@1.15.1: {} + + hookified@2.2.0: {} + + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + ignore@5.3.2: {} + + ignore@7.0.12: {} + + imurmurhash@0.1.4: {} + + index-to-position@1.2.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.18.1 + lodash-es: 4.18.1 + + lodash@4.18.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + marked@14.0.0: {} + + memoize-one@6.0.0: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.7 + + mitt@3.0.1: {} + + monaco-editor@0.57.0: + dependencies: + dompurify: 3.4.15 + marked: 14.0.0 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.19: {} + + natural-compare@1.4.0: {} + + normalize-wheel-es@1.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + openapi-fetch@0.14.1: + dependencies: + openapi-typescript-helpers: 0.0.15 + + openapi-typescript-helpers@0.0.15: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.20(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.10 + vue: 3.5.43(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + pluralize@8.0.0: {} + + postcss-selector-parser@7.1.6: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.2 + + prelude-ls@1.2.1: {} + + prettier@3.9.9: {} + + punycode@2.3.1: {} + + qified@0.10.1: + dependencies: + hookified: 2.2.0 + + require-from-string@2.0.2: {} + + rfdc@1.4.1: {} + + rollup@4.64.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.64.0 + '@rollup/rollup-android-arm64': 4.64.0 + '@rollup/rollup-darwin-arm64': 4.64.0 + '@rollup/rollup-darwin-x64': 4.64.0 + '@rollup/rollup-freebsd-arm64': 4.64.0 + '@rollup/rollup-freebsd-x64': 4.64.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.64.0 + '@rollup/rollup-linux-arm-musleabihf': 4.64.0 + '@rollup/rollup-linux-arm64-gnu': 4.64.0 + '@rollup/rollup-linux-arm64-musl': 4.64.0 + '@rollup/rollup-linux-loong64-gnu': 4.64.0 + '@rollup/rollup-linux-loong64-musl': 4.64.0 + '@rollup/rollup-linux-ppc64-gnu': 4.64.0 + '@rollup/rollup-linux-ppc64-musl': 4.64.0 + '@rollup/rollup-linux-riscv64-gnu': 4.64.0 + '@rollup/rollup-linux-riscv64-musl': 4.64.0 + '@rollup/rollup-linux-s390x-gnu': 4.64.0 + '@rollup/rollup-linux-x64-gnu': 4.64.0 + '@rollup/rollup-linux-x64-musl': 4.64.0 + '@rollup/rollup-openbsd-x64': 4.64.0 + '@rollup/rollup-openharmony-arm64': 4.64.0 + '@rollup/rollup-win32-arm64-msvc': 4.64.0 + '@rollup/rollup-win32-ia32-msvc': 4.64.0 + '@rollup/rollup-win32-x64-gnu': 4.64.0 + '@rollup/rollup-win32-x64-msvc': 4.64.0 + fsevents: 2.3.3 + + semver@7.8.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.2: {} + + speakingurl@14.0.1: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.1.5 + + supports-color@10.2.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + typescript-eslint@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.71.0(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + eslint: 10.12.0(supports-color@10.2.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.24.6: {} + + uri-js-replace@1.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@7.3.6(@types/node@24.19.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.64.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.19.1 + fsevents: 2.3.3 + + vscode-uri@3.2.0: {} + + vue-component-type-helpers@3.3.12: {} + + vue-eslint-parser@10.4.1(eslint@10.12.0(supports-color@10.2.2))(supports-color@10.2.2): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.12.0(supports-color@10.2.2) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.43(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.43(typescript@5.9.3) + + vue-tsc@3.3.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28(typescript@5.9.3) + '@vue/language-core': 3.3.12 + typescript: 5.9.3 + + vue@3.5.43(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.43 + '@vue/compiler-sfc': 3.5.43 + '@vue/runtime-dom': 3.5.43 + '@vue/server-renderer': 3.5.43 + '@vue/shared': 3.5.43 + optionalDependencies: + typescript: 5.9.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + xml-name-validator@5.0.0: {} + + yaml-ast-parser@0.0.43: {} + + yargs-parser@21.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..51f6505 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +saveExact: true +verifyDepsBeforeRun: false +allowBuilds: + esbuild: true diff --git a/frontend/public/images/home-banner.svg b/frontend/public/images/home-banner.svg new file mode 100644 index 0000000..483f2f0 --- /dev/null +++ b/frontend/public/images/home-banner.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/public/images/practice-space.svg b/frontend/public/images/practice-space.svg new file mode 100644 index 0000000..626ae81 --- /dev/null +++ b/frontend/public/images/practice-space.svg @@ -0,0 +1,10 @@ + + + + + + solution.cppint main() {cin >> a >> b;cout << a + b;} + 01 / SIMPLE START1 + 2 = 3 + A + THINK / BUILD / REPEAT + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..792e08c --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/admin/AdminApp.vue b/frontend/src/admin/AdminApp.vue new file mode 100644 index 0000000..3c81b9f --- /dev/null +++ b/frontend/src/admin/AdminApp.vue @@ -0,0 +1,39 @@ + + diff --git a/frontend/src/admin/AdminView.vue b/frontend/src/admin/AdminView.vue new file mode 100644 index 0000000..c6535b7 --- /dev/null +++ b/frontend/src/admin/AdminView.vue @@ -0,0 +1,272 @@ + + diff --git a/frontend/src/admin/main.ts b/frontend/src/admin/main.ts new file mode 100644 index 0000000..49dfc10 --- /dev/null +++ b/frontend/src/admin/main.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { registerUi } from '../plugins/ui' +import { adminRouter } from './router' +import AdminApp from './AdminApp.vue' +import '../styles/base.css' +import '../styles/blueprint.css' + +const app = createApp(AdminApp) +registerUi(app) +app.use(adminRouter).mount('#admin-app') diff --git a/frontend/src/admin/navigation.ts b/frontend/src/admin/navigation.ts new file mode 100644 index 0000000..9d70ebf --- /dev/null +++ b/frontend/src/admin/navigation.ts @@ -0,0 +1,8 @@ +export const adminSections = [ + { id: 'overview', title: '概览', icon: 'monitor' }, + { id: 'problems', title: '题目管理', icon: 'problems' }, + { id: 'contests', title: '比赛管理', icon: 'contests' }, + { id: 'users', title: '用户管理', icon: 'teams' }, + { id: 'judges', title: '判题节点', icon: 'connection' }, + { id: 'settings', title: '站点设置', icon: 'settings' }, +] diff --git a/frontend/src/admin/router.ts b/frontend/src/admin/router.ts new file mode 100644 index 0000000..eac06de --- /dev/null +++ b/frontend/src/admin/router.ts @@ -0,0 +1,12 @@ +import { createRouter, createWebHashHistory } from 'vue-router' +import { adminSections } from './navigation' + +export const adminRouter = createRouter({ + history: createWebHashHistory('/admin/index.html'), + scrollBehavior: () => ({ top: 0 }), + routes: [{ path: '/:section?', component: () => import('./AdminView.vue') }], +}) +adminRouter.afterEach((to) => { + const section = adminSections.find((item) => item.id === (to.params.section || 'overview')) + document.title = `${section?.title ?? '页面不存在'} · KF 管理控制台` +}) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..ac8260f --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,6 @@ +import createClient from 'openapi-fetch' +import type { paths } from './generated/schema' + +export const api = createClient({ + baseUrl: import.meta.env.VITE_API_BASE_URL || '', +}) diff --git a/frontend/src/api/generated/schema.d.ts b/frontend/src/api/generated/schema.d.ts new file mode 100644 index 0000000..1872ce9 --- /dev/null +++ b/frontend/src/api/generated/schema.d.ts @@ -0,0 +1,75 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/health/live": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Liveness + * @description Check process liveness only; does not attest DB, queue or judge readiness. + */ + get: operations["liveness"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** HealthResponse */ + HealthResponse: { + /** + * Service + * @constant + */ + service: "platform"; + /** + * Status + * @constant + */ + status: "ok"; + /** Version */ + version: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + liveness: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; +} diff --git a/frontend/src/api/health.ts b/frontend/src/api/health.ts new file mode 100644 index 0000000..855d682 --- /dev/null +++ b/frontend/src/api/health.ts @@ -0,0 +1,9 @@ +import { api } from './client' + +export async function fetchLiveness(signal: AbortSignal) { + const { data, error, response } = await api.GET('/api/v1/health/live', { signal }) + if (!response.ok || error || !data) { + throw new Error(`平台暂时无法连接(HTTP ${response.status})`) + } + return data +} diff --git a/frontend/src/components/CodeEditor.vue b/frontend/src/components/CodeEditor.vue new file mode 100644 index 0000000..ab88a09 --- /dev/null +++ b/frontend/src/components/CodeEditor.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/DifficultyTag.vue b/frontend/src/components/DifficultyTag.vue new file mode 100644 index 0000000..ccac152 --- /dev/null +++ b/frontend/src/components/DifficultyTag.vue @@ -0,0 +1,15 @@ + + diff --git a/frontend/src/components/EmptyState.vue b/frontend/src/components/EmptyState.vue new file mode 100644 index 0000000..922cbe2 --- /dev/null +++ b/frontend/src/components/EmptyState.vue @@ -0,0 +1,10 @@ + + diff --git a/frontend/src/components/PageHeading.vue b/frontend/src/components/PageHeading.vue new file mode 100644 index 0000000..b11b840 --- /dev/null +++ b/frontend/src/components/PageHeading.vue @@ -0,0 +1,13 @@ + + diff --git a/frontend/src/components/PanelCard.vue b/frontend/src/components/PanelCard.vue new file mode 100644 index 0000000..1f42cfa --- /dev/null +++ b/frontend/src/components/PanelCard.vue @@ -0,0 +1,15 @@ + + diff --git a/frontend/src/components/ProblemTable.vue b/frontend/src/components/ProblemTable.vue new file mode 100644 index 0000000..26db178 --- /dev/null +++ b/frontend/src/components/ProblemTable.vue @@ -0,0 +1,39 @@ + + diff --git a/frontend/src/components/SubmissionChart.vue b/frontend/src/components/SubmissionChart.vue new file mode 100644 index 0000000..79bc754 --- /dev/null +++ b/frontend/src/components/SubmissionChart.vue @@ -0,0 +1,150 @@ + + + diff --git a/frontend/src/components/SubmissionTable.vue b/frontend/src/components/SubmissionTable.vue new file mode 100644 index 0000000..495629f --- /dev/null +++ b/frontend/src/components/SubmissionTable.vue @@ -0,0 +1,40 @@ + + diff --git a/frontend/src/components/TagSphere.vue b/frontend/src/components/TagSphere.vue new file mode 100644 index 0000000..03cea6f --- /dev/null +++ b/frontend/src/components/TagSphere.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/frontend/src/components/UiIcon.vue b/frontend/src/components/UiIcon.vue new file mode 100644 index 0000000..f11e6cb --- /dev/null +++ b/frontend/src/components/UiIcon.vue @@ -0,0 +1,54 @@ + + diff --git a/frontend/src/components/VerdictTag.vue b/frontend/src/components/VerdictTag.vue new file mode 100644 index 0000000..007b149 --- /dev/null +++ b/frontend/src/components/VerdictTag.vue @@ -0,0 +1,9 @@ + + diff --git a/frontend/src/composables/useCodeDraft.ts b/frontend/src/composables/useCodeDraft.ts new file mode 100644 index 0000000..f1c17c7 --- /dev/null +++ b/frontend/src/composables/useCodeDraft.ts @@ -0,0 +1,88 @@ +import { computed, onScopeDispose, ref, watch } from 'vue' +export type Language = 'cpp' | 'python' | 'rust' +export const languageLabels: Record = { + cpp: 'C++', + python: 'Python', + rust: 'Rust', +} +export const codeTemplates: Record = { + cpp: '#include \nusing namespace std;\n\nint main() {\n // 在这里编写你的解法\n return 0;\n}\n', + python: '# 在这里编写你的解法\n\n', + rust: 'use std::io::{self, Read};\n\nfn main() {\n let mut input = String::new();\n io::stdin().read_to_string(&mut input).unwrap();\n // 在这里编写你的解法\n}\n', +} +export function useCodeDraft(problemId: string) { + const key = 'kf-oj:code-draft:' + problemId + const language = ref('cpp') + const drafts = ref({ ...codeTemplates }) + const state = ref<'initial' | 'saved' | 'changed' | 'unavailable'>('initial') + let timer: ReturnType | undefined + try { + const raw = localStorage.getItem(key) + if (raw) { + const saved: unknown = JSON.parse(raw) + if ( + saved && + typeof saved === 'object' && + 'version' in saved && + saved.version === 1 && + 'drafts' in saved && + saved.drafts && + typeof saved.drafts === 'object' + ) { + const storedDrafts = saved.drafts as Record + for (const lang of ['cpp', 'python', 'rust'] as const) { + const value = storedDrafts[lang] + if (typeof value === 'string') drafts.value[lang] = value + } + if ('language' in saved && ['cpp', 'python', 'rust'].includes(String(saved.language))) + language.value = saved.language as Language + state.value = 'saved' + } + } + } catch { + state.value = 'unavailable' + } + const code = computed({ + get: () => drafts.value[language.value], + set: (value) => { + drafts.value[language.value] = value + }, + }) + function save() { + clearTimeout(timer) + try { + localStorage.setItem( + key, + JSON.stringify({ version: 1, language: language.value, drafts: drafts.value }), + ) + state.value = 'saved' + } catch { + state.value = 'unavailable' + } + } + watch( + [drafts, language], + () => { + state.value = 'changed' + clearTimeout(timer) + timer = setTimeout(save, 400) + }, + // Mark pending changes before a lifecycle event can run in the same tick. + { deep: true, flush: 'sync' }, + ) + function flushPendingDraft() { + if (state.value === 'changed') save() + } + function onVisibilityChange() { + if (document.visibilityState === 'hidden') flushPendingDraft() + } + window.addEventListener('pagehide', flushPendingDraft) + document.addEventListener('visibilitychange', onVisibilityChange) + onScopeDispose(() => { + flushPendingDraft() + clearTimeout(timer) + window.removeEventListener('pagehide', flushPendingDraft) + document.removeEventListener('visibilitychange', onVisibilityChange) + }) + return { language, code, state, save } +} diff --git a/frontend/src/composables/usePagination.ts b/frontend/src/composables/usePagination.ts new file mode 100644 index 0000000..6d15e13 --- /dev/null +++ b/frontend/src/composables/usePagination.ts @@ -0,0 +1,13 @@ +import { computed, ref, watch, type Ref } from 'vue' +export function usePagination(items: Readonly>, initialSize = 10) { + const page = ref(1) + const pageSize = ref(initialSize) + const total = computed(() => items.value.length) + const paged = computed(() => + items.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value), + ) + watch([items, pageSize], () => { + page.value = 1 + }) + return { page, pageSize, total, paged } +} diff --git a/frontend/src/composables/usePlatformHealth.ts b/frontend/src/composables/usePlatformHealth.ts new file mode 100644 index 0000000..ffaa31e --- /dev/null +++ b/frontend/src/composables/usePlatformHealth.ts @@ -0,0 +1,28 @@ +import { onMounted, onScopeDispose, ref } from 'vue' +import { fetchLiveness } from '../api/health' + +export function usePlatformHealth() { + const state = ref<'loading' | 'connected' | 'unavailable'>('loading') + const version = ref('') + let controller: AbortController | undefined + + async function refresh() { + controller?.abort() + const request = new AbortController() + controller = request + state.value = 'loading' + try { + const health = await fetchLiveness(request.signal) + if (!request.signal.aborted) { + version.value = health.version + state.value = 'connected' + } + } catch { + if (!request.signal.aborted) state.value = 'unavailable' + } + } + + onMounted(refresh) + onScopeDispose(() => controller?.abort()) + return { state, version, refresh } +} diff --git a/frontend/src/data/preview.ts b/frontend/src/data/preview.ts new file mode 100644 index 0000000..d0b0496 --- /dev/null +++ b/frontend/src/data/preview.ts @@ -0,0 +1,465 @@ +// Explicit UI fixtures; business endpoints will be connected through API composables. +export type Difficulty = '入门' | '简单' | '中等' | '困难' +export interface Problem { + id: string + title: string + difficulty: Difficulty + tags: string[] + accepted: number + submissions: number + updated: string + description: string + input: string + output: string + sampleInput: string + sampleOutput: string + constraint: string +} +const definitions: [string, string, Difficulty, string[], number, number][] = [ + ['1001', 'A + B Problem', '入门', ['模拟', '输入输出'], 428, 536], + ['1002', '两数之和', '简单', ['数组', '哈希表'], 315, 482], + ['1003', '最长无重复子串', '中等', ['字符串', '滑动窗口'], 206, 527], + ['1004', '旅行者的背包', '中等', ['动态规划', '背包'], 186, 461], + ['1005', '城市间的最短路径', '中等', ['图论', '最短路'], 152, 387], + ['1006', '寻找第一个位置', '简单', ['二分', '数组'], 241, 402], + ['1007', '括号匹配', '简单', ['栈', '字符串'], 287, 389], + ['1008', '岛屿的数量', '中等', ['搜索', '图论'], 143, 328], + ['1009', '区间求和', '中等', ['数据结构', '前缀和'], 124, 306], + ['1010', '最小生成树', '困难', ['图论', '并查集'], 96, 288], + ['1011', '活动安排', '简单', ['贪心', '排序'], 203, 312], + ['1012', '最长公共子序列', '中等', ['动态规划', '字符串'], 112, 345], +] +export const problems: Problem[] = definitions.map( + ([id, title, difficulty, tags, accepted, submissions], index) => ({ + id, + title, + difficulty, + tags, + accepted, + submissions, + updated: '2026-10-' + String(4 - Math.floor(index / 4)).padStart(2, '0'), + description: + id === '1001' + ? '给定两个整数 A 和 B,请计算它们的和。认真阅读输入和输出格式,完成你的第一次提交。' + : '这道题的完整题面将在题库接入后展示。当前提供列表与编辑器的界面预览。', + input: + id === '1001' + ? '输入一行,包含两个整数 A 和 B,以一个空格分隔。' + : '完整输入格式待题库接入。', + output: id === '1001' ? '输出一个整数,表示 A 与 B 的和。' : '完整输出格式待题库接入。', + sampleInput: id === '1001' ? '1 2' : '', + sampleOutput: id === '1001' ? '3' : '', + constraint: + id === '1001' + ? '−10⁹ ≤ A, B ≤ 10⁹。建议使用 64 位整数保存计算结果。' + : '数据范围待题库接入。', + }), +) +export const problemTags = [...new Set(problems.flatMap((problem) => problem.tags))] +export const rankings = [ + { + username: 'Ada', + motto: '每天进步一点点', + solved: 167, + submissions: 238, + rating: 1924, + color: '#6196ec', + }, + { + username: 'Kronecker', + motto: '写下思路,再写代码。', + solved: 142, + submissions: 216, + rating: 1846, + color: '#6b70d6', + }, + { + username: 'Sora', + motto: '今天也要 AC', + solved: 129, + submissions: 204, + rating: 1732, + color: '#3fb7a1', + }, + { + username: 'Natsume', + motto: '慢慢来,比较快', + solved: 118, + submissions: 182, + rating: 1685, + color: '#e9a35d', + }, + { + username: 'Lin', + motto: '保持好奇', + solved: 106, + submissions: 175, + rating: 1621, + color: '#dc82a4', + }, + { + username: 'Mika', + motto: '从一道题开始', + solved: 98, + submissions: 153, + rating: 1594, + color: '#63a9c9', + }, + { + username: 'Chen', + motto: 'Hello, algorithm', + solved: 92, + submissions: 151, + rating: 1540, + color: '#897cb7', + }, + { + username: 'Yuki', + motto: '专注这一题', + solved: 87, + submissions: 144, + rating: 1506, + color: '#6aaa8a', + }, + { + username: 'Joago', + motto: 'Thinking in code', + solved: 81, + submissions: 132, + rating: 1482, + color: '#bb9b67', + }, + { + username: 'Hyperbola', + motto: '努力成为更好的自己', + solved: 74, + submissions: 128, + rating: 1428, + color: '#8095b9', + }, +] +export const notices = [ + { + id: '1', + title: '欢迎来到 KF OnlineJudge', + date: '2026-10-04', + category: '平台公告', + text: '从基础练习开始,记录每一次提交,让思路在代码里落地。当前页面使用示例内容。', + }, + { + id: '2', + title: '新学期基础训练题单已更新', + date: '2026-10-03', + category: '训练', + text: '输入输出、数组、字符串和简单搜索,按顺序完成你的第一组算法练习。', + }, + { + id: '3', + title: '周末练习赛 · 热身题单', + date: '2026-10-01', + category: '比赛', + text: '练习赛页面已准备好,实际比赛安排以平台发布的信息为准。', + }, +] +export const trainings = [ + { + id: 'basics', + title: '从零开始 · 编程基础', + category: '基础入门', + level: '入门', + description: '从输入输出到数组与字符串,熟悉在线评测的基本流程。', + ids: ['1001', '1002', '1006', '1007'], + color: 'blue', + icon: '01', + }, + { + id: 'search', + title: '搜索与图论入门', + category: '算法专题', + level: '中等', + description: '理解遍历、连通性与路径,建立图论的基础解题思路。', + ids: ['1008', '1005', '1010'], + color: 'green', + icon: '02', + }, + { + id: 'dp', + title: '动态规划 · 一步一步推出来', + category: '算法专题', + level: '中等', + description: '从状态定义开始,练习转移、边界和空间优化。', + ids: ['1004', '1012'], + color: 'violet', + icon: '03', + }, + { + id: 'structures', + title: '常用数据结构练习', + category: '进阶练习', + level: '中等', + description: '通过具体问题理解哈希表、栈、前缀和与并查集。', + ids: ['1002', '1007', '1009', '1010'], + color: 'amber', + icon: '04', + }, +] +export const contests = [ + { + id: 'weekly-01', + title: 'KF 周末练习赛 #01', + type: 'ACM', + status: '即将开始', + start: '2026-10-10 14:00', + duration: '2 小时', + description: '五道由浅入深的算法题,给自己一个专注思考的下午。', + ids: ['1001', '1002', '1003', '1004', '1005'], + }, + { + id: 'starter', + title: '新同学编程热身赛', + type: 'OI', + status: '即将开始', + start: '2026-10-12 19:00', + duration: '2 小时', + description: '面向刚开始学习程序设计的同学,练习输入输出与基本逻辑。', + ids: ['1001', '1006', '1007'], + }, + { + id: 'graph', + title: '图论专题 · 路径与连通性', + type: 'ACM', + status: '进行中', + start: '2026-10-04 14:00', + duration: '4 小时', + description: '题单式比赛界面示例,状态与时间为展示用数据。', + ids: ['1005', '1008', '1010'], + }, + { + id: 'weekly-00', + title: 'KF 周末练习赛 #00', + type: 'ACM', + status: '已结束', + start: '2026-09-27 14:00', + duration: '2 小时', + description: '往期比赛界面示例,便于浏览题目和比赛说明。', + ids: ['1001', '1003', '1011'], + }, + { + id: 'dp-practice', + title: '动态规划专项练习', + type: 'OI', + status: '已结束', + start: '2026-09-20 09:00', + duration: '3 小时', + description: '集中练习常见动态规划模型。', + ids: ['1004', '1012'], + }, +] +export type Verdict = 'AC' | 'WA' | 'TLE' | 'RE' | 'CE' | 'Pending' +export const verdictLabels: Record = { + AC: 'Accepted', + WA: 'Wrong Answer', + TLE: 'Time Limit Exceeded', + RE: 'Runtime Error', + CE: 'Compile Error', + Pending: '等待评测', +} +export const submissions: { + id: string + problemId: string + username: string + verdict: Verdict + language: string + time: number | null + memory: number | null + created: string +}[] = [ + { + id: '10248', + problemId: '1001', + username: 'Kronecker', + verdict: 'AC', + language: 'Rust', + time: 12, + memory: 2048, + created: '2026-10-04 14:32:18', + }, + { + id: '10247', + problemId: '1003', + username: 'Ada', + verdict: 'AC', + language: 'C++', + time: 34, + memory: 4096, + created: '2026-10-04 14:30:05', + }, + { + id: '10246', + problemId: '1004', + username: 'Sora', + verdict: 'WA', + language: 'Python', + time: 62, + memory: 8192, + created: '2026-10-04 14:28:41', + }, + { + id: '10245', + problemId: '1005', + username: 'Lin', + verdict: 'TLE', + language: 'C++', + time: 1000, + memory: 6144, + created: '2026-10-04 14:26:22', + }, + { + id: '10244', + problemId: '1002', + username: 'Mika', + verdict: 'AC', + language: 'Python', + time: 48, + memory: 7168, + created: '2026-10-04 14:24:09', + }, + { + id: '10243', + problemId: '1007', + username: 'Chen', + verdict: 'CE', + language: 'C++', + time: null, + memory: null, + created: '2026-10-04 14:21:33', + }, + { + id: '10242', + problemId: '1008', + username: 'Yuki', + verdict: 'RE', + language: 'Rust', + time: 18, + memory: 3072, + created: '2026-10-04 14:19:06', + }, + { + id: '10241', + problemId: '1006', + username: 'Kronecker', + verdict: 'AC', + language: 'Rust', + time: 11, + memory: 2048, + created: '2026-10-04 14:17:26', + }, + { + id: '10240', + problemId: '1011', + username: 'Natsume', + verdict: 'Pending', + language: 'C++', + time: null, + memory: null, + created: '2026-10-04 14:15:12', + }, + { + id: '10239', + problemId: '1009', + username: 'Ada', + verdict: 'AC', + language: 'C++', + time: 22, + memory: 4096, + created: '2026-10-04 14:12:45', + }, + { + id: '10238', + problemId: '1002', + username: 'Sora', + verdict: 'WA', + language: 'Python', + time: 56, + memory: 7168, + created: '2026-10-04 14:10:18', + }, + { + id: '10237', + problemId: '1012', + username: 'Hyperbola', + verdict: 'AC', + language: 'C++', + time: 71, + memory: 10240, + created: '2026-10-04 14:08:51', + }, +] +export const discussions = [ + { + id: '1', + title: '第一次使用在线评测,可以从哪里开始?', + category: '交流', + author: 'Kronecker', + replies: 12, + views: 328, + date: '2026-10-04', + pinned: true, + content: + '欢迎来到 KF OnlineJudge!可以先打开「从零开始 · 编程基础」题单,了解标准输入输出,再尝试 A + B Problem。\n\n遇到问题时,记录题号、语言和具体报错,把自己的思路一起写出来,讨论会更容易展开。', + }, + { + id: '2', + title: 'A + B Problem:输入输出与整数范围', + category: '题解', + author: 'Ada', + replies: 8, + views: 216, + date: '2026-10-04', + pinned: false, + content: + '读取两个整数,计算它们的和,输出结果。\n\n记得检查整数范围。Rust 可以选择 i64;Python 的整数可以直接参与计算。输出只需要答案和换行。', + problemId: '1001', + }, + { + id: '3', + title: '动态规划中的状态应该怎么定义?', + category: '问答', + author: 'Sora', + replies: 6, + views: 184, + date: '2026-10-03', + pinned: false, + content: + '练习背包问题时,我经常卡在状态定义上。现在尝试先描述「已经处理哪些物品」和「剩余多少容量」,再考虑每一步有哪些选择。\n\n大家平时会怎样从题意里提取状态?', + }, + { + id: '4', + title: '分享我的 Rust 刷题输入模板', + category: '交流', + author: 'Lin', + replies: 4, + views: 152, + date: '2026-10-02', + pinned: false, + content: + '先用 read_to_string 获取输入,再按空白分隔。\n\n不同题目的输入结构可能不同,使用模板之前还是需要认真阅读题面。', + }, + { + id: '5', + title: '关于题库筛选的一点建议', + category: '反馈', + author: 'Mika', + replies: 2, + views: 96, + date: '2026-10-01', + pinned: false, + content: '希望题库可以同时按照标签和难度筛选,这样练习某个知识点会更方便。', + }, +] +export function findProblem(id: string) { + return problems.find((problem) => problem.id === id) +} +export function acceptance(problem: Pick) { + return Math.round((problem.accepted / problem.submissions) * 100) +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..f614c5b --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue new file mode 100644 index 0000000..851d43b --- /dev/null +++ b/frontend/src/layouts/AppLayout.vue @@ -0,0 +1,94 @@ + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..4c9b9af --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,11 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import { registerUi } from './plugins/ui' +import App from './App.vue' +import { router } from './router' +import './styles/base.css' +import './styles/blueprint.css' + +const app = createApp(App) +registerUi(app) +app.use(createPinia()).use(router).mount('#app') diff --git a/frontend/src/plugins/ui.ts b/frontend/src/plugins/ui.ts new file mode 100644 index 0000000..72fb75a --- /dev/null +++ b/frontend/src/plugins/ui.ts @@ -0,0 +1,65 @@ +import type { App } from 'vue' +import { + ElButton, + ElConfigProvider, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElInput, + ElOption, + ElPagination, + ElPopover, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElResult, + ElSelect, + ElSlider, + ElSwitch, + ElTabPane, + ElTable, + ElTableColumn, + ElTabs, + ElTag, + ElTooltip, +} from 'element-plus' +import 'element-plus/dist/index.css' + +export function registerUi(app: App) { + for (const component of [ + ElButton, + ElConfigProvider, + ElDescriptions, + ElDescriptionsItem, + ElDialog, + ElEmpty, + ElForm, + ElFormItem, + ElIcon, + ElInput, + ElOption, + ElPagination, + ElPopover, + ElProgress, + ElRadio, + ElRadioButton, + ElRadioGroup, + ElResult, + ElSelect, + ElSlider, + ElSwitch, + ElTabPane, + ElTable, + ElTableColumn, + ElTabs, + ElTag, + ElTooltip, + ]) { + app.use(component) + } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..b9da1e3 --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,100 @@ +import { createRouter, createWebHistory } from 'vue-router' +export const router = createRouter({ + history: createWebHistory(), + scrollBehavior: (to, _from, saved) => saved ?? (to.hash ? { el: to.hash } : { top: 0 }), + routes: [ + { + path: '/', + name: 'home', + meta: { title: '首页' }, + component: () => import('../views/HomeView.vue'), + }, + { + path: '/problems', + meta: { title: '题库' }, + component: () => import('../views/ProblemsView.vue'), + }, + { + path: '/problems/:id', + meta: { title: '题目详情' }, + component: () => import('../views/ProblemDetailView.vue'), + }, + { + path: '/trainings', + meta: { title: '训练' }, + component: () => import('../views/TrainingsView.vue'), + }, + { + path: '/trainings/:id', + meta: { title: '训练详情' }, + component: () => import('../views/TrainingDetailView.vue'), + }, + { + path: '/contests', + meta: { title: '比赛' }, + component: () => import('../views/ContestsView.vue'), + }, + { + path: '/contests/:id', + meta: { title: '比赛详情' }, + component: () => import('../views/ContestDetailView.vue'), + }, + { + path: '/submissions', + meta: { title: '评测' }, + component: () => import('../views/SubmissionsView.vue'), + }, + { + path: '/submissions/:id', + meta: { title: '评测详情' }, + component: () => import('../views/SubmissionDetailView.vue'), + }, + { + path: '/rankings', + meta: { title: '排名' }, + component: () => import('../views/RankingsView.vue'), + }, + { + path: '/discussions', + meta: { title: '讨论' }, + component: () => import('../views/DiscussionsView.vue'), + }, + { + path: '/discussions/:id', + meta: { title: '讨论详情' }, + component: () => import('../views/DiscussionDetailView.vue'), + }, + { path: '/teams', meta: { title: '团队' }, component: () => import('../views/TeamsView.vue') }, + { + path: '/teams/create', + meta: { title: '创建团队' }, + component: () => import('../views/CreateTeamView.vue'), + }, + { + path: '/users/:username', + meta: { title: '个人主页' }, + component: () => import('../views/ProfileView.vue'), + }, + { path: '/login', meta: { title: '登录' }, component: () => import('../views/AuthView.vue') }, + { + path: '/register', + meta: { title: '注册' }, + component: () => import('../views/AuthView.vue'), + }, + { + path: '/logout', + meta: { title: '退出登录' }, + component: () => import('../views/LogoutView.vue'), + }, + { path: '/about', meta: { title: '关于' }, component: () => import('../views/AboutView.vue') }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + meta: { title: '页面不存在' }, + component: () => import('../views/NotFoundView.vue'), + }, + ], +}) +router.afterEach((to) => { + document.title = `${String(to.meta.title ?? '首页')} · KF OnlineJudge` +}) diff --git a/frontend/src/styles/base.css b/frontend/src/styles/base.css new file mode 100644 index 0000000..1b02049 --- /dev/null +++ b/frontend/src/styles/base.css @@ -0,0 +1,1436 @@ +:root { + font-family: Inter, 'Noto Sans SC', 'Microsoft YaHei', sans-serif; + color: #354257; + background: #f0f3f7; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + --el-color-primary: #409eff; + --el-border-radius-base: 5px; + --el-border-color: #e0e6ee; + --el-text-color-primary: #354257; + --el-text-color-regular: #566379; + --el-fill-color-lighter: #f8fafc; + --blue: #409eff; + --muted: #8490a3; + --line: #edf0f5; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +a { + color: inherit; + text-decoration: none; +} +a:hover { + color: var(--blue); +} +button, +input, +textarea { + font: inherit; +} +button { + cursor: pointer; +} +button:disabled { + cursor: not-allowed; +} +h1, +h2, +h3, +p { + margin: 0; +} +p { + line-height: 1.75; +} +::selection { + background: #daecff; +} +.app-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} +.app-header { + background: #fff; + border-bottom: 1px solid #e5eaf1; + box-shadow: 0 2px 5px #263c5605; +} +.header-inner { + width: min(100% - 48px, 1480px); + margin: auto; + display: flex; + align-items: center; + min-height: 68px; + gap: 28px; +} +.brand { + display: flex; + align-items: center; + gap: 10px; + white-space: nowrap; +} +.brand-mark { + font-size: 29px; + letter-spacing: -2px; + font-weight: 850; + color: #409eff; + line-height: 1; +} +.brand-dot { + color: #91bff3; +} +.brand-name { + font-size: 15px; + font-weight: 750; + color: #2d4261; + letter-spacing: -0.3px; +} +.brand-name small { + display: block; + font-size: 8px; + letter-spacing: 1px; + color: #91a3bb; + margin-top: 4px; +} +.main-nav { + display: flex; + align-self: stretch; + gap: 3px; +} +.main-nav a { + display: flex; + align-items: center; + gap: 7px; + padding: 0 13px; + font-size: 13px; + border-bottom: 2px solid transparent; + white-space: nowrap; + color: #67758a; +} +.main-nav a.active { + color: var(--blue); + background: #f4f9ff; + border-color: var(--blue); +} +.main-nav .el-icon { + font-size: 14px; +} +.header-account { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + white-space: nowrap; +} +.preview-label { + font-size: 11px; + color: #91a0b2; + margin-right: 6px; +} +.mobile-toggle { + display: none; +} +.app-main { + width: min(100% - 48px, 1480px); + margin: 24px auto 40px; + flex: 1; +} +.app-footer { + background: #fff; + border-top: 1px solid #e6ecf3; + padding: 36px 0 20px; +} +.footer-inner, +.footer-bottom { + width: min(100% - 64px, 1400px); + margin: auto; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 32px; +} +.footer-brand strong { + display: block; + margin: 15px 0 7px; + font-size: 14px; + font-weight: 500; +} +.footer-brand p { + font-size: 12px; + color: var(--muted); +} +.footer-inner h3 { + font-size: 13px; + font-weight: 600; + margin: 2px 0 12px; +} +.footer-inner a { + display: block; + font-size: 12px; + color: #8490a3; + margin: 10px 0; +} +.footer-inner a:hover { + color: var(--blue); +} +.footer-bottom { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + border-top: 1px solid var(--line); + padding-top: 20px; + margin-top: 30px; + font-size: 11px; + color: #9aa5b5; +} +.panel { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + box-shadow: 0 2px 7px #263c5605; + min-width: 0; + overflow: hidden; +} +.panel-heading { + min-height: 54px; + padding: 15px 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + border-bottom: 1px solid var(--line); +} +.panel-heading h2 { + font-size: 15px; + color: #4995e5; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} +.panel-heading small { + font-size: 11px; + font-weight: 400; + color: #98a4b4; + margin-left: 4px; +} +.panel-actions { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: var(--muted); + flex-shrink: 0; +} +.panel-actions a { + color: var(--blue); +} +.panel-body { + padding: 20px; + min-width: 0; +} +.page-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + margin: 8px 0 24px; +} +.page-heading h1 { + font-size: 26px; + font-weight: 650; + letter-spacing: -0.6px; + margin-bottom: 8px; + color: #2e405b; +} +.page-heading p { + font-size: 13px; +} +.page-heading-actions { + display: flex; + align-items: center; + gap: 10px; +} +.eyebrow { + text-transform: uppercase; + font-size: 11px !important; + letter-spacing: 2px; + color: #86a0be; + margin-bottom: 8px; +} +.muted { + color: var(--muted); +} +.small { + font-size: 12px; +} +.mono { + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; +} +.tabular { + font-variant-numeric: tabular-nums; +} +.text-link { + color: var(--blue); + font-size: 13px; +} +.table-link { + color: #455e80; + font-weight: 500; +} +.table-link:hover { + color: var(--blue); +} +.tag-list { + display: flex; + gap: 5px; + flex-wrap: wrap; +} +.tag { + display: inline-flex; + align-items: center; + padding: 3px 7px; + color: #8492a6; + background: #f3f6fa; + border-radius: 3px; + font-size: 11px; + white-space: nowrap; +} +.difficulty { + padding: 3px 7px; + border-radius: 4px; + font-size: 11px; + white-space: nowrap; + background: #f0f3f7; + color: #8a99ac; +} +.difficulty-easy { + color: #49a389; + background: #eff9f5; +} +.difficulty-medium { + color: #c28e37; + background: #fff7e8; +} +.difficulty-hard { + color: #c86c85; + background: #fff0f4; +} +.verdict { + font: + 600 11px ui-monospace, + monospace; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; +} +.verdict-ac { + color: #22a278; + background: #edf9f4; +} +.verdict-wa, +.verdict-re { + color: #dd7184; + background: #fff0f3; +} +.verdict-tle { + color: #c39737; + background: #fff8e7; +} +.verdict-ce { + color: #9879cc; + background: #f6f0fc; +} +.verdict-pending { + color: #8897ac; + background: #f0f4f9; +} +.filters { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + margin-bottom: 22px; +} +.filters .el-input { + width: 260px; +} +.filters .el-select { + width: 155px; +} +.filter-label { + font-size: 12px; + color: #8a99ad; +} +.pagination-row { + padding: 20px 0 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--muted); +} +.el-table { + --el-table-header-bg-color: #f8fafc; + --el-table-header-text-color: #7e8a9c; + --el-table-border-color: #edf0f5; + font-size: 12px; +} +.el-table th.el-table__cell { + font-weight: 500; +} +.el-table .el-table__cell { + padding: 12px 0; +} +.stack { + display: flex; + flex-direction: column; + gap: 20px; + min-width: 0; +} +.content-with-aside { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 20px; +} +.summary-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 20px; +} +.summary-card { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + padding: 20px; +} +.summary-card p { + font-size: 12px; + color: var(--muted); +} +.summary-card strong { + display: block; + font-size: 27px; + font-weight: 600; + margin: 8px 0 2px; + letter-spacing: -0.5px; +} +.summary-card small { + font-size: 11px; + color: #9cabbc; +} +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 50%; + color: #fff; + font-size: 12px; + flex-shrink: 0; +} +.user-inline { + display: flex; + align-items: center; + gap: 10px; +} +.number-medal { + display: inline-grid; + place-items: center; + width: 21px; + height: 25px; + color: #9aa5b5; + font-weight: 600; + font-size: 12px; +} +.number-medal.top-1 { + color: #e3983e; + background: #fff3df; +} +.number-medal.top-2 { + color: #8498b7; + background: #edf2f9; +} +.number-medal.top-3 { + color: #c58d74; + background: #f9efe8; +} +.empty-state { + text-align: center; + padding: 12px 0 24px; +} +.empty-state .el-empty { + padding: 20px; +} +.empty-state > p { + font-size: 13px; + margin-bottom: 14px; +} +.breadcrumbs { + margin: 0 0 20px; + font-size: 12px; + color: #95a1b1; + display: flex; + gap: 10px; + flex-wrap: wrap; +} +.breadcrumbs a { + color: #7299c2; +} +.intro-banner { + background: linear-gradient(115deg, #eaf4ff, #f5f8fd); + border: 1px solid #e2edfa; + border-radius: 6px; + padding: 30px; + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; +} +.intro-banner h2 { + font-size: 25px; + margin: 8px 0 12px; + font-weight: 600; +} +.intro-banner p { + font-size: 13px; + color: #7a8eaa; + max-width: 540px; +} +.intro-number { + font-size: 65px; + color: #afceef; + font-family: Georgia, serif; +} +.card-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 20px; +} +.training-card { + padding: 26px; + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + display: flex; + gap: 22px; +} +.training-illustration { + width: 70px; + height: 78px; + border-radius: 12px; + display: grid; + place-items: center; + flex-shrink: 0; + font: + 600 27px ui-monospace, + monospace; + background: #ecf4ff; + color: #81b2ea; +} +.training-illustration.green { + background: #edf8f3; + color: #73b39f; +} +.training-illustration.violet { + background: #f3effb; + color: #a291cc; +} +.training-illustration.amber { + background: #fff7ea; + color: #d6b072; +} +.training-card h2 { + font-size: 17px; + margin: 10px 0; + color: #425874; +} +.training-card p { + font-size: 12px; + color: #92a0b2; + margin: 9px 0 18px; +} +.card-meta { + display: flex; + align-items: center; + gap: 15px; + font-size: 12px; + color: #92a0b2; +} +.card-meta .text-link { + margin-left: auto; +} +.detail-prose { + font-size: 14px; + line-height: 1.9; + color: #566379; +} +.detail-prose h2 { + font-size: 17px; + color: #40536d; + margin: 24px 0 10px; + font-weight: 600; +} +.detail-prose h2:first-child { + margin-top: 0; +} +.detail-prose p { + white-space: pre-line; +} +.sample-block { + background: #f7f9fc; + border: 1px solid #e9eef5; + border-radius: 5px; + padding: 16px; + font: + 13px/1.8 ui-monospace, + monospace; + white-space: pre-wrap; + margin: 12px 0; +} +.detail-meta { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + font-size: 12px; + color: #92a0b2; + margin: 14px 0 20px; +} +.details-list { + display: flex; + flex-direction: column; + gap: 15px; + font-size: 13px; +} +.details-list div { + display: flex; + justify-content: space-between; + gap: 20px; +} +.details-list dt { + color: #91a0b2; +} +.details-list dd { + margin: 0; + text-align: right; +} +.notice-strip { + padding: 12px 16px; + background: #f5f9ff; + border: 1px solid #e6f0fc; + border-radius: 5px; + font-size: 12px; + color: #7b94b2; + margin-bottom: 20px; + line-height: 1.7; +} +.form-panel { + max-width: 860px; + margin: auto; +} +.form-panel .panel-body { + padding: 32px; +} +.form-help { + font-size: 12px; + color: #98a5b6; + margin: 5px 0 0; +} +.form-actions { + display: flex; + gap: 12px; + align-items: center; + margin-top: 28px; +} +.form-actions .muted { + font-size: 12px; +} +.tab-summary { + padding: 16px 0; + font-size: 13px; + color: var(--muted); +} +.about-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} +.about-feature { + padding: 24px; + border: 1px solid #edf0f5; + border-radius: 6px; +} +.about-feature h3 { + font-size: 16px; + margin: 12px 0; +} +.about-feature p { + font-size: 13px; + color: #8c9db2; +} +.about-feature .el-icon { + font-size: 23px; + color: #7cacde; +} +.service-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} +.service-row h3 { + font-size: 15px; + margin-bottom: 8px; +} +.service-row p { + font-size: 12px; + color: #94a2b6; +} +.profile-banner { + background: #fff; + border: 1px solid #e8edf4; + border-radius: 6px; + padding: 36px; + display: flex; + align-items: center; + gap: 25px; + margin-bottom: 22px; +} +.profile-banner > .avatar { + width: 84px; + height: 84px; + font-size: 32px; +} +.profile-banner h1 { + font-size: 27px; + margin-bottom: 8px; +} +.profile-banner p { + color: #92a0b2; + font-size: 13px; +} +.profile-actions { + margin-left: auto; + display: flex; + gap: 10px; +} +.profile-tags { + margin-top: 12px; + display: flex; + gap: 7px; +} +.heatmap { + display: grid; + grid-template-columns: repeat(20, minmax(0, 1fr)); + gap: 5px; + padding: 15px 0; +} +.heat-cell { + aspect-ratio: 1; + border-radius: 3px; + background: #edf3fa; +} +.heat-cell[data-level='1'] { + background: #d6eaff; +} +.heat-cell[data-level='2'] { + background: #a3d1fc; +} +.heat-cell[data-level='3'] { + background: #6caff0; +} +.heat-cell[data-level='4'] { + background: #4094e4; +} +.skill-row { + margin: 17px 0; +} +.skill-row > div { + display: flex; + justify-content: space-between; + font-size: 12px; + margin-bottom: 8px; +} +.admin-shell { + display: grid; + grid-template-columns: 210px minmax(0, 1fr); + gap: 22px; +} +.admin-sidebar { + padding: 20px 12px; + align-self: start; +} +.admin-sidebar h2 { + font-size: 14px; + padding: 0 12px; + margin-bottom: 18px; +} +.admin-sidebar a { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 5px; + font-size: 13px; + color: #7b8da5; +} +.admin-sidebar a.active { + background: #eef6ff; + color: #409eff; +} +.admin-notice { + font-size: 12px; + color: #8fa0b6; + padding: 18px 12px 0; + border-top: 1px solid #edf0f5; + margin-top: 20px; + line-height: 1.7; +} +.status-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #80b69a; + margin-right: 6px; +} +.status-dot.offline { + background: #c5cedb; +} +.home-grid { + display: grid; + grid-template-columns: minmax(0, 1.75fr) minmax(320px, 1fr); + gap: 20px; +} +.home-banner .panel-body { + padding: 16px; +} +.hero-image { + position: relative; + min-height: 300px; + overflow: hidden; + border-radius: 4px; + background: #e6f2ff; + isolation: isolate; +} +.hero-image > img { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + inset: 0; + z-index: -1; +} +.hero-copy { + position: relative; + padding: 42px 36px; + max-width: 75%; +} +.hero-copy .eyebrow { + font-size: 10px !important; + color: #6b96c6; +} +.hero-copy h1 { + font-size: clamp(25px, 2.35vw, 38px); + font-weight: 600; + line-height: 1.4; + color: #335983; + letter-spacing: -1px; + margin: 12px 0; +} +.hero-copy p { + font-size: 13px; + color: #7093b7; + max-width: 320px; + margin-bottom: 23px; +} +.hero-controls { + display: flex; + justify-content: center; + gap: 7px; + padding: 12px 0 1px; +} +.hero-controls button { + width: 20px; + height: 3px; + padding: 0; + border: 0; + background: #d9e3ee; + border-radius: 2px; +} +.hero-controls button.active { + background: #409eff; +} +.home-rank .panel-body { + padding: 10px 20px; +} +.home-rank .el-table .el-table__cell { + padding: 8px 0; +} +.home-rank .avatar { + width: 23px; + height: 23px; + font-size: 10px; +} +.home-rank .number-medal { + height: 21px; + font-size: 11px; +} +.home-notice { + padding: 15px 4px; + display: flex; + gap: 15px; + border-bottom: 1px solid #edf0f5; +} +.home-notice:last-child { + border: 0; + padding-bottom: 2px; +} +.home-notice > div { + min-width: 0; +} +.home-notice-title { + display: block; + font-size: 13px; + margin: 0 0 5px; + font-weight: 500; + color: #4c6685; +} +.home-notice p { + font-size: 12px; + color: #98a5b7; +} +.home-notice time { + font-size: 11px; + color: #a0adbc; + white-space: nowrap; + margin-left: auto; +} +.notice-symbol { + background: #edf6ff; + width: 28px; + height: 28px; + border-radius: 5px; + display: grid; + place-items: center; + color: #66a8eb; + flex-shrink: 0; +} +.contest-mini { + display: flex; + gap: 11px; + padding: 13px 0; + border-bottom: 1px solid #edf0f5; + align-items: center; +} +.contest-mini:last-child { + border-bottom: 0; +} +.contest-mini strong { + display: block; + font-size: 12px; + color: #59708d; + font-weight: 500; +} +.contest-mini p { + font-size: 11px; + color: #9cacbf; + margin-top: 4px; +} +.contest-format { + font-size: 10px; + background: #eaf5ff; + color: #68a3de; + border-radius: 3px; + min-width: 31px; + text-align: center; + padding: 4px; +} +.contest-format.oi { + background: #eaf8f1; + color: #64b296; +} +.contest-status { + margin-left: auto; + font-size: 11px; + color: #cfad76; + white-space: nowrap; +} +.home-quick { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.home-quick a { + padding: 14px; + background: #f8fafc; + border: 1px solid #edf2f7; + border-radius: 5px; + font-size: 12px; + display: flex; + align-items: center; + gap: 9px; + color: #7b94b4; +} +.home-quick a:hover { + background: #eff6ff; + color: #409eff; +} +.auth-shell { + max-width: 1000px; + margin: 35px auto; + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 540px; + background: #fff; + border: 1px solid #e5ecf4; + border-radius: 8px; + overflow: hidden; +} +.auth-art { + padding: 48px 40px; + background: #e9f3ff url('/images/home-banner.svg') center bottom/cover; +} +.auth-art h1 { + font-size: 33px; + color: #4f78a4; + margin: 35px 0 15px; +} +.auth-art p { + font-size: 14px; + color: #86a4c5; +} +.auth-form { + padding: 40px; +} +.auth-form h2 { + font-size: 25px; + margin-bottom: 8px; +} +.auth-form > p { + font-size: 12px; + color: #9caabc; + margin-bottom: 25px; +} +.auth-form .el-form-item { + margin-bottom: 21px; +} +.auth-form .el-button { + width: 100%; +} +.auth-links { + font-size: 12px; + display: flex; + justify-content: space-between; + margin-top: 20px; + color: #91a2b8; +} +.discussion-item { + display: flex; + align-items: center; + gap: 16px; + padding: 23px 0; + border-bottom: 1px solid #edf0f5; +} +.discussion-item:last-child { + border-bottom: 0; +} +.discussion-item .avatar { + width: 40px; + height: 40px; + font-size: 16px; +} +.discussion-item h2 { + font-size: 15px; + font-weight: 500; + margin-bottom: 7px; + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; +} +.discussion-item p { + display: flex; + gap: 12px; + font-size: 11px; + color: #9cabbc; +} +.discussion-numbers { + margin-left: auto; + font-size: 12px; + color: #9dabbc; + display: flex; + gap: 20px; + white-space: nowrap; +} +.discussion-numbers strong { + font-weight: 500; + color: #6a819e; +} +.comment-placeholder { + padding: 25px; + background: #f7f9fc; + border-radius: 5px; + margin: 20px 0 0; + font-size: 13px; + color: #94a4b9; + text-align: center; +} +.team-card { + padding: 24px; + display: flex; + gap: 20px; + align-items: start; +} +.team-logo { + width: 55px; + height: 55px; + border-radius: 10px; + background: #edf4ff; + color: #7faae0; + font: + 600 20px ui-monospace, + monospace; + display: grid; + place-items: center; +} +.team-card h2 { + font-size: 17px; + margin: 3px 0 8px; +} +.team-card p { + font-size: 12px; + color: #96a5b7; +} +.team-card .tag-list { + margin-top: 15px; +} +@media (max-width: 1280px) { + .header-inner { + gap: 16px; + } + .main-nav a { + padding: 0 9px; + gap: 5px; + } + .main-nav .el-icon { + display: none; + } + .preview-label { + display: none; + } + .home-grid { + grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr); + } + .hero-copy { + padding: 36px 28px; + } + .hero-copy p { + max-width: 270px; + } + .card-grid { + gap: 16px; + } + .training-card { + padding: 20px; + gap: 16px; + } + .training-illustration { + width: 55px; + height: 65px; + } + .content-with-aside { + grid-template-columns: minmax(0, 1fr) 280px; + } +} +@media (max-width: 960px) { + .brand-name { + font-size: 13px; + } + .brand-mark { + font-size: 25px; + } + .header-inner { + gap: 12px; + } + .main-nav a { + font-size: 12px; + padding: 0 7px; + } + .header-account { + gap: 5px; + } + .hero-copy { + max-width: 100%; + padding: 30px 24px; + } + .home-grid { + grid-template-columns: minmax(0, 1.45fr) minmax(270px, 1fr); + gap: 16px; + } + .home-grid .panel-heading { + padding: 14px 16px; + } + .home-grid .panel-body { + padding: 16px; + } + .home-rank .panel-body { + padding: 8px 10px; + } + .contest-status { + display: none; + } + .content-with-aside, + .card-grid, + .about-grid { + grid-template-columns: 1fr; + } + .admin-shell { + grid-template-columns: 175px minmax(0, 1fr); + } + .summary-card { + padding: 16px; + } + .summary-card strong { + font-size: 24px; + } +} +@media (max-width: 760px) { + .app-main { + width: calc(100% - 28px); + margin-top: 18px; + } + .header-inner { + width: calc(100% - 28px); + flex-wrap: wrap; + gap: 12px; + min-height: 64px; + padding: 14px 0; + } + .brand-name small { + font-size: 7px; + } + .mobile-toggle { + display: flex; + align-items: center; + gap: 5px; + padding: 5px 7px; + background: none; + border: 1px solid #e6edf5; + border-radius: 4px; + font-size: 12px; + color: #7e93ad; + margin-left: auto; + } + .header-account { + margin-left: 0; + } + .main-nav { + display: none; + order: 4; + width: 100%; + flex-wrap: wrap; + gap: 4px; + padding-top: 5px; + } + .main-nav.mobile-open { + display: flex; + } + .main-nav a { + padding: 9px 13px; + flex: 1 0 auto; + justify-content: center; + border-radius: 4px; + border-bottom: 0; + } + .main-nav .el-icon { + display: inline-flex; + } + .home-grid { + grid-template-columns: minmax(0, 1fr); + } + .hero-copy h1 { + font-size: 32px; + } + .hero-copy { + padding: 32px; + } + .hero-copy p { + max-width: 270px; + } + .footer-inner { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 20px; + } + .footer-brand { + grid-column: 1/-1; + } + .footer-bottom { + gap: 12px; + } + .page-heading { + align-items: flex-start; + flex-wrap: wrap; + gap: 12px; + } + .page-heading h1 { + font-size: 23px; + } + .filters .el-input { + width: 100%; + } + .filters .el-select { + flex: 1; + min-width: 130px; + width: auto; + } + .pagination-row { + flex-wrap: wrap; + justify-content: center; + } + .pagination-row .el-pagination { + flex-wrap: wrap; + justify-content: center; + gap: 8px; + } + .summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + } + .intro-banner { + padding: 24px; + } + .intro-number { + display: none; + } + .profile-banner { + padding: 24px; + gap: 16px; + flex-wrap: wrap; + } + .profile-banner > .avatar { + width: 60px; + height: 60px; + font-size: 24px; + } + .profile-actions { + margin-left: 0; + width: 100%; + } + .heatmap { + grid-template-columns: repeat(14, minmax(0, 1fr)); + } + .auth-shell { + grid-template-columns: 1fr; + margin: 15px auto; + } + .auth-art { + display: none; + } + .auth-form { + padding: 28px; + } + .admin-shell { + grid-template-columns: 1fr; + } + .admin-sidebar { + display: flex; + flex-wrap: wrap; + gap: 5px; + padding: 12px; + } + .admin-sidebar h2, + .admin-notice { + display: none; + } + .admin-sidebar a { + padding: 8px; + font-size: 12px; + } + .discussion-numbers { + display: none; + } + .form-panel .panel-body { + padding: 22px; + } + .service-row { + flex-wrap: wrap; + } + .panel-heading { + padding: 14px 16px; + } + .panel-body { + padding: 16px; + } + .details-list { + font-size: 12px; + } + .footer-inner, + .footer-bottom { + width: calc(100% - 40px); + } +} +@media (max-width: 420px) { + .brand-name { + font-size: 11px; + } + .brand-name small { + display: none; + } + .brand { + gap: 7px; + } + .header-inner { + gap: 7px; + } + .header-account .el-button { + padding: 7px 10px; + } + .mobile-toggle { + padding: 5px; + font-size: 11px; + } + .hero-copy { + padding: 30px 24px; + } + .hero-copy h1 { + font-size: 27px; + } + .hero-copy p { + max-width: 230px; + font-size: 12px; + } + .panel-heading h2 { + font-size: 14px; + } + .panel-heading small { + display: none; + } + .training-illustration { + display: none; + } + .discussion-item { + gap: 10px; + } + .discussion-item h2 { + font-size: 14px; + } + .discussion-item p { + gap: 7px; + flex-wrap: wrap; + } + .summary-card strong { + font-size: 22px; + } +} diff --git a/frontend/src/styles/blueprint.css b/frontend/src/styles/blueprint.css new file mode 100644 index 0000000..4101af6 --- /dev/null +++ b/frontend/src/styles/blueprint.css @@ -0,0 +1,492 @@ +:root { + --el-border-radius-base: 8px; +} +.app-header { + position: sticky; + top: 0; + z-index: 100; + box-shadow: none; +} +.header-inner { + min-height: 76px; + max-width: 1360px; +} +.brand-mark { + font-size: 25px; + padding: 10px 12px; + border-radius: 11px; + background: #eaf4ff; + letter-spacing: -1.5px; +} +.brand-dot { + color: #409eff; +} +.brand-name { + font-size: 13px; +} +.brand-name small { + letter-spacing: 1.3px; +} +.main-nav { + gap: 5px; + align-items: center; +} +.main-nav a { + align-self: center; + padding: 10px 13px; + border: 0; + border-radius: 8px; + font-size: 13px; +} +.main-nav a.active { + border: 0; + background: #edf6ff; + font-weight: 650; +} +.main-nav .el-icon { + display: none; +} +.header-account .el-button { + border-radius: 8px; + padding: 8px 14px; +} +.app-main { + width: min(100% - 64px, 1360px); + margin-top: 30px; + margin-bottom: 50px; +} +.app-footer { + background: transparent; + box-shadow: none; + padding: 38px 0 22px; +} +.footer-inner, +.footer-bottom { + max-width: 1360px; +} +.footer-brand .brand-mark { + display: inline-block; + background: #e3eefb; +} +.footer-inner h3 { + font-size: 12px; + color: #657e9e; +} +.panel { + border-radius: 16px; + box-shadow: none; + border: 1px solid #e1e8f1; +} +.panel-heading { + border: 0; + padding: 23px 24px 10px; + min-height: 68px; + align-items: flex-start; +} +.panel-heading h2 { + color: #455e80; + font-size: 17px; + font-weight: 650; + gap: 6px; + display: grid; +} +.panel-heading h2 > .el-icon { + display: none; +} +.panel-heading small { + margin: 0; + font-size: 10px; + letter-spacing: 0.3px; +} +.panel-body { + padding: 18px 24px 24px; +} +.panel-actions { + padding-top: 4px; + font-size: 11px; +} +.panel-actions a { + display: inline-flex; + align-items: center; + gap: 8px; +} +.page-heading { + margin: 8px 0 30px; + align-items: flex-end; +} +.page-heading h1 { + font-size: 34px; + letter-spacing: -1.2px; + color: #2e405b; + font-weight: 700; + margin: 10px 0 10px; +} +.page-heading .eyebrow { + color: #91a3bb; + font: + 10px ui-monospace, + monospace; + letter-spacing: 2px; +} +.page-heading p { + font-size: 13px; +} +.el-button { + border-radius: 8px; +} +.el-table { + --el-table-header-bg-color: #f3f7fc; + --el-table-row-hover-bg-color: #f4f9ff; + --el-fill-color-lighter: #fff; + font-size: 13px; + border-radius: 8px; +} +.el-table .el-table__cell { + padding: 16px 0; +} +.el-table th.el-table__cell { + padding: 13px 0; + font-size: 11px; + color: #8a99ac; + letter-spacing: 0.3px; +} +.el-table .cell { + padding: 0 14px; +} +.table-link { + font-weight: 600; +} +.filters { + padding: 16px; + background: #f6f9fd; + border-radius: 10px; + gap: 10px; + margin-bottom: 22px; +} +.filters .el-input { + width: 260px; +} +.pagination-row { + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid var(--line); +} +.tag, +.difficulty, +.el-tag { + border-radius: 5px; +} +.summary-card { + border-radius: 14px; + position: relative; + padding: 24px; + border-bottom: 3px solid #dbeaff; +} +.summary-card strong { + font: + 650 32px ui-monospace, + monospace; + margin-top: 12px; +} +.intro-banner { + border-radius: 16px; + border-left: 4px solid #9cc8f5; + padding: 32px; +} +.training-card { + border-radius: 16px; + flex-direction: column; + gap: 10px; + position: relative; + padding: 26px; +} +.training-illustration { + width: 48px; + height: 48px; + border-radius: 12px; + font-size: 21px; +} +.training-card h2 { + font-size: 20px; + margin: 15px 0 10px; +} +.training-card p { + font-size: 13px; + line-height: 1.9; +} +.card-meta { + padding-top: 15px; + border-top: 1px solid var(--line); + flex-wrap: wrap; +} +.team-card { + border-radius: 16px; + flex-direction: column; +} +.team-logo { + width: 48px; + height: 48px; +} +.profile-banner { + border-radius: 18px; + border-left: 5px solid #a9cef5; +} +.profile-banner > .avatar { + border-radius: 22px; +} +.discussion-item { + padding: 24px 8px; +} +.discussion-item .avatar { + border-radius: 12px; +} +.discussion-item h2 { + font-size: 16px; + font-weight: 600; +} +.detail-prose h2 { + color: #455e80; + font-size: 18px; + padding-left: 12px; + border-left: 3px solid #b5d5f7; +} +.sample-block { + border-radius: 10px; + background: #f6f9fd; + border: 1px solid #e4edf7; +} +.auth-shell { + border-radius: 22px; + border: 1px solid #e1e8f1; + box-shadow: none; +} +.auth-art { + padding: 50px; + background-image: url('/images/practice-space.svg'); + background-size: 90%; + background-position: center bottom; + background-repeat: no-repeat; + min-height: 520px; +} +.auth-art h1 { + font-size: 33px; + max-width: 300px; + line-height: 1.5; +} +.about-feature { + border-radius: 14px; + padding: 26px; +} +#tags { + scroll-margin-top: 98px; +} +.admin-application { + min-height: 100dvh; +} +.admin-app-header { + min-height: 80px; + display: flex; + align-items: center; + gap: 26px; + padding: 0 30px; + background: #fff; + border-bottom: 1px solid #e1e8f1; + position: sticky; + top: 0; + z-index: 100; +} +.admin-app-label { + font-size: 11px; + color: #91a3bb; + padding-left: 26px; + border-left: 1px solid #e1e8f1; +} +.admin-return { + font-size: 12px; + color: #657e9e; + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} +.admin-app-nav { + position: fixed; + left: 0; + top: 80px; + bottom: 0; + width: 210px; + background: #fff; + border-right: 1px solid #e1e8f1; + padding: 30px 18px; + display: flex; + flex-direction: column; + gap: 7px; +} +.admin-app-nav a { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 18px; + font-size: 13px; + color: #7b8da5; + border-radius: 9px; +} +.admin-app-nav a.active { + background: #edf6ff; + color: #409eff; + font-weight: 600; +} +.admin-app-main { + margin: 32px 32px 48px 242px; + min-width: 0; +} +.admin-app-main .admin-shell { + display: block; +} +.admin-app-footer { + margin-left: 242px; + padding: 22px 32px 28px 0; + border-top: 1px solid #e1e8f1; + display: flex; + gap: 12px; + justify-content: space-between; + flex-wrap: wrap; + color: #9aa5b5; + font-size: 10px; +} +@media (min-width: 1200px) { + .card-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} +@media (max-width: 1100px) { + .header-inner { + gap: 18px; + } + .main-nav a { + padding: 10px; + } + .brand-name { + display: none; + } + .preview-label { + display: none; + } +} +@media (max-width: 960px) { + .panel-body { + padding: 16px 20px 22px; + } + .panel-heading { + padding: 22px 20px 8px; + } + .page-heading h1 { + font-size: 30px; + } + .admin-app-label { + display: none; + } +} +@media (max-width: 760px) { + .header-inner { + min-height: 66px; + padding: 10px 0; + } + .brand-name { + display: block; + } + .brand-mark { + font-size: 20px; + padding: 9px 10px; + } + .main-nav a { + padding: 11px 15px; + } + .main-nav .el-icon { + display: none; + } + .header-account .el-button { + padding: 8px 11px; + } + .mobile-toggle { + border-radius: 7px; + padding: 7px; + } + .app-main { + width: calc(100% - 28px); + margin-top: 22px; + } + .page-heading { + align-items: flex-start; + } + .page-heading h1 { + font-size: 28px; + } + .panel-heading { + padding: 20px 18px 8px; + min-height: 65px; + } + .panel-heading h2 { + font-size: 16px; + } + .panel-heading small { + display: block; + } + .panel-body { + padding: 16px 18px 20px; + } + .filters { + padding: 12px; + } + .filters .el-input { + width: 100%; + } + .training-card, + .team-card { + padding: 22px; + } + .training-illustration { + display: grid; + } + .summary-card { + padding: 20px; + } + .summary-card strong { + font-size: 26px; + } + .admin-app-header { + min-height: 70px; + padding: 0 18px; + } + .admin-app-header .brand-name { + font-size: 12px; + } + .admin-app-nav { + width: 62px; + top: 70px; + padding: 20px 7px; + } + .admin-app-nav a { + padding: 14px 15px; + } + .admin-app-nav a span { + display: none; + } + .admin-app-main { + margin: 24px 14px 35px 76px; + } + .admin-app-footer { + margin-left: 76px; + padding-right: 14px; + } + .admin-return { + font-size: 11px; + } + .auth-art { + min-height: 0; + } + .footer-inner, + .footer-bottom { + width: calc(100% - 36px); + } +} diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue new file mode 100644 index 0000000..a6df682 --- /dev/null +++ b/frontend/src/views/AboutView.vue @@ -0,0 +1,81 @@ + + diff --git a/frontend/src/views/AuthView.vue b/frontend/src/views/AuthView.vue new file mode 100644 index 0000000..6f550a7 --- /dev/null +++ b/frontend/src/views/AuthView.vue @@ -0,0 +1,100 @@ + + diff --git a/frontend/src/views/ContestDetailView.vue b/frontend/src/views/ContestDetailView.vue new file mode 100644 index 0000000..ef8712b --- /dev/null +++ b/frontend/src/views/ContestDetailView.vue @@ -0,0 +1,77 @@ + + diff --git a/frontend/src/views/ContestsView.vue b/frontend/src/views/ContestsView.vue new file mode 100644 index 0000000..a7315fe --- /dev/null +++ b/frontend/src/views/ContestsView.vue @@ -0,0 +1,73 @@ + + diff --git a/frontend/src/views/CreateTeamView.vue b/frontend/src/views/CreateTeamView.vue new file mode 100644 index 0000000..0b219c1 --- /dev/null +++ b/frontend/src/views/CreateTeamView.vue @@ -0,0 +1,69 @@ + + diff --git a/frontend/src/views/DiscussionDetailView.vue b/frontend/src/views/DiscussionDetailView.vue new file mode 100644 index 0000000..c5a9f42 --- /dev/null +++ b/frontend/src/views/DiscussionDetailView.vue @@ -0,0 +1,40 @@ + + diff --git a/frontend/src/views/DiscussionsView.vue b/frontend/src/views/DiscussionsView.vue new file mode 100644 index 0000000..3ed69ef --- /dev/null +++ b/frontend/src/views/DiscussionsView.vue @@ -0,0 +1,77 @@ + + diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue new file mode 100644 index 0000000..b200cf3 --- /dev/null +++ b/frontend/src/views/HomeView.vue @@ -0,0 +1,550 @@ + + + diff --git a/frontend/src/views/LogoutView.vue b/frontend/src/views/LogoutView.vue new file mode 100644 index 0000000..59cc64c --- /dev/null +++ b/frontend/src/views/LogoutView.vue @@ -0,0 +1,13 @@ + + diff --git a/frontend/src/views/NotFoundView.vue b/frontend/src/views/NotFoundView.vue new file mode 100644 index 0000000..0fbca32 --- /dev/null +++ b/frontend/src/views/NotFoundView.vue @@ -0,0 +1,13 @@ + + + diff --git a/frontend/src/views/ProblemDetailView.vue b/frontend/src/views/ProblemDetailView.vue new file mode 100644 index 0000000..6683560 --- /dev/null +++ b/frontend/src/views/ProblemDetailView.vue @@ -0,0 +1,520 @@ + + + diff --git a/frontend/src/views/ProblemsView.vue b/frontend/src/views/ProblemsView.vue new file mode 100644 index 0000000..51ae338 --- /dev/null +++ b/frontend/src/views/ProblemsView.vue @@ -0,0 +1,62 @@ + + diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue new file mode 100644 index 0000000..f310ba8 --- /dev/null +++ b/frontend/src/views/ProfileView.vue @@ -0,0 +1,105 @@ + + diff --git a/frontend/src/views/RankingsView.vue b/frontend/src/views/RankingsView.vue new file mode 100644 index 0000000..bdae973 --- /dev/null +++ b/frontend/src/views/RankingsView.vue @@ -0,0 +1,67 @@ + + diff --git a/frontend/src/views/SubmissionDetailView.vue b/frontend/src/views/SubmissionDetailView.vue new file mode 100644 index 0000000..a2e9f1e --- /dev/null +++ b/frontend/src/views/SubmissionDetailView.vue @@ -0,0 +1,63 @@ + + diff --git a/frontend/src/views/SubmissionsView.vue b/frontend/src/views/SubmissionsView.vue new file mode 100644 index 0000000..9b39f10 --- /dev/null +++ b/frontend/src/views/SubmissionsView.vue @@ -0,0 +1,75 @@ + + diff --git a/frontend/src/views/TeamsView.vue b/frontend/src/views/TeamsView.vue new file mode 100644 index 0000000..b1a2cb0 --- /dev/null +++ b/frontend/src/views/TeamsView.vue @@ -0,0 +1,61 @@ + + diff --git a/frontend/src/views/TrainingDetailView.vue b/frontend/src/views/TrainingDetailView.vue new file mode 100644 index 0000000..b77c9be --- /dev/null +++ b/frontend/src/views/TrainingDetailView.vue @@ -0,0 +1,59 @@ + + diff --git a/frontend/src/views/TrainingsView.vue b/frontend/src/views/TrainingsView.vue new file mode 100644 index 0000000..482765e --- /dev/null +++ b/frontend/src/views/TrainingsView.vue @@ -0,0 +1,55 @@ + + diff --git a/frontend/tests/useCodeDraft.test.ts b/frontend/tests/useCodeDraft.test.ts new file mode 100644 index 0000000..b2cc75a --- /dev/null +++ b/frontend/tests/useCodeDraft.test.ts @@ -0,0 +1,164 @@ +import assert from 'node:assert/strict' +import { test, type TestContext } from 'node:test' +import { effectScope, nextTick } from 'vue' +import { codeTemplates, useCodeDraft, type Language } from '../src/composables/useCodeDraft.ts' + +type SavedDraft = { version: number; language: Language; drafts: Record } + +function browser(t: TestContext) { + const page = new EventTarget() + const document = Object.assign(new EventTarget(), { + visibilityState: 'visible' as DocumentVisibilityState, + }) + const storage = { + items: new Map(), + writes: 0, + failWrites: false, + getItem(key: string) { + return this.items.get(key) ?? null + }, + setItem(key: string, value: string) { + this.writes++ + if (this.failWrites) throw new Error('Storage unavailable') + this.items.set(key, value) + }, + } + const globals = { window: page, document, localStorage: storage } + const descriptors = Object.keys(globals).map( + (name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)] as const, + ) + for (const [name, value] of Object.entries(globals)) { + Object.defineProperty(globalThis, name, { configurable: true, writable: true, value }) + } + t.mock.timers.enable({ apis: ['setTimeout'] }) + const scopes: ReturnType[] = [] + t.after(() => { + for (const scope of scopes) scope.stop() + for (const [name, descriptor] of descriptors) { + if (descriptor) Object.defineProperty(globalThis, name, descriptor) + else Reflect.deleteProperty(globalThis, name) + } + }) + return { + page, + document, + storage, + open(problemId = '1001') { + const scope = effectScope() + scopes.push(scope) + return { scope, ...scope.run(() => useCodeDraft(problemId))! } + }, + read(problemId = '1001'): SavedDraft | null { + return JSON.parse(storage.getItem('kf-oj:code-draft:' + problemId) ?? 'null') + }, + } +} + +test('pagehide saves the latest edit before the debounce and restores it after reload', (t) => { + const env = browser(t) + const draft = env.open() + draft.code.value = '// latest edit before refresh' + env.page.dispatchEvent(new Event('pagehide')) + assert.equal(env.read()?.drafts.cpp, draft.code.value) + assert.equal(draft.state.value, 'saved') + t.mock.timers.tick(400) + assert.equal(env.storage.writes, 1) + draft.scope.stop() + assert.equal(env.open().code.value, '// latest edit before refresh') +}) + +test('hiding the page flushes pending edits; remaining visible does not', async (t) => { + const env = browser(t) + const draft = env.open() + draft.code.value = '// edited while visible' + await nextTick() + env.document.dispatchEvent(new Event('visibilitychange')) + assert.equal(env.storage.writes, 0) + env.document.visibilityState = 'hidden' + env.document.dispatchEvent(new Event('visibilitychange')) + assert.equal(env.read()?.drafts.cpp, draft.code.value) + t.mock.timers.tick(400) + assert.equal(env.storage.writes, 1) +}) + +test('normal typing still debounces writes and saves the most recent edit', async (t) => { + const env = browser(t) + const draft = env.open() + draft.code.value = '// first edit' + await nextTick() + t.mock.timers.tick(399) + assert.equal(env.storage.writes, 0) + draft.code.value = '// second edit' + await nextTick() + t.mock.timers.tick(399) + assert.equal(env.storage.writes, 0) + t.mock.timers.tick(1) + assert.equal(env.read()?.drafts.cpp, '// second edit') + assert.equal(env.storage.writes, 1) +}) + +test('leaving a Vue page saves even an edit made in the same tick', (t) => { + const env = browser(t) + const draft = env.open() + draft.code.value = '// latest edit before navigation' + draft.scope.stop() + assert.equal(env.read()?.drafts.cpp, '// latest edit before navigation') + t.mock.timers.tick(400) + assert.equal(env.storage.writes, 1) +}) + +test('disposing the scope removes page lifecycle listeners', (t) => { + const env = browser(t) + const removePage = t.mock.method(env.page, 'removeEventListener') + const removeDocument = t.mock.method(env.document, 'removeEventListener') + env.open().scope.stop() + assert.deepEqual( + removePage.mock.calls.map((call) => call.arguments[0]), + ['pagehide'], + ) + assert.deepEqual( + removeDocument.mock.calls.map((call) => call.arguments[0]), + ['visibilitychange'], + ) + env.page.dispatchEvent(new Event('pagehide')) + env.document.visibilityState = 'hidden' + env.document.dispatchEvent(new Event('visibilitychange')) + assert.equal(env.storage.writes, 0) +}) + +test('pagehide preserves the selected language and keeps problem drafts separate', (t) => { + const env = browser(t) + const first = env.open('1001') + first.code.value = '// C++ draft' + first.language.value = 'python' + first.code.value = '# Python draft' + const second = env.open('1002') + second.code.value = '// another problem' + env.page.dispatchEvent(new Event('pagehide')) + assert.deepEqual(env.read('1001'), { + version: 1, + language: 'python', + drafts: { cpp: '// C++ draft', python: '# Python draft', rust: codeTemplates.rust }, + }) + assert.equal(env.read('1002')?.drafts.cpp, '// another problem') + first.scope.stop() + const restored = env.open('1001') + assert.equal(restored.language.value, 'python') + assert.equal(restored.code.value, '# Python draft') +}) + +test('a failed lifecycle save reports unavailable storage without throwing', (t) => { + const env = browser(t) + const draft = env.open() + env.storage.failWrites = true + draft.code.value = '// cannot persist yet' + assert.doesNotThrow(() => env.page.dispatchEvent(new Event('pagehide'))) + assert.equal(draft.state.value, 'unavailable') + t.mock.timers.tick(400) + assert.equal(env.storage.writes, 1) + env.storage.failWrites = false + draft.code.value = '// storage recovered' + env.page.dispatchEvent(new Event('pagehide')) + assert.equal(draft.state.value, 'saved') + assert.equal(env.read()?.drafts.cpp, '// storage recovered') +}) diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..2f52f31 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "types": ["vite/client", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "tests/**/*.ts", "vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..a5b61e3 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + return { + plugins: [vue()], + build: { + rollupOptions: { + input: { + main: fileURLToPath(new URL('./index.html', import.meta.url)), + admin: fileURLToPath(new URL('./admin/index.html', import.meta.url)), + }, + }, + }, + server: { + port: 5173, + strictPort: true, + proxy: { + '/api': { target: env.KF_API_PROXY_TARGET || 'http://127.0.0.1:8000' }, + }, + }, + preview: { port: 4173, strictPort: true }, + } +}) diff --git a/judge/Cargo.lock b/judge/Cargo.lock new file mode 100644 index 0000000..1699e97 --- /dev/null +++ b/judge/Cargo.lock @@ -0,0 +1,911 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kf-contracts" +version = "0.0.1" +dependencies = [ + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "kf-coordinator" +version = "0.0.1" +dependencies = [ + "axum", + "clap", + "kf-contracts", + "serde", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "kf-engine" +version = "0.0.1" +dependencies = [ + "kf-contracts", +] + +[[package]] +name = "kf-sandbox" +version = "0.0.1" +dependencies = [ + "clap", + "kf-contracts", + "serde", + "serde_json", +] + +[[package]] +name = "kf-worker" +version = "0.0.1" +dependencies = [ + "clap", + "kf-contracts", + "kf-engine", + "kf-sandbox", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/judge/Cargo.toml b/judge/Cargo.toml new file mode 100644 index 0000000..72995f1 --- /dev/null +++ b/judge/Cargo.toml @@ -0,0 +1,35 @@ +[workspace] +members = [ + "crates/contracts", + "crates/sandbox", + "crates/engine", + "crates/worker", + "crates/coordinator", +] +resolver = "3" + +[workspace.package] +version = "0.0.1" +edition = "2024" +rust-version = "1.99" +license = "MIT" + +[workspace.dependencies] +kf-contracts = { path = "crates/contracts" } +kf-sandbox = { path = "crates/sandbox" } +kf-engine = { path = "crates/engine" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1", features = ["serde"] } +clap = { version = "4", features = ["derive", "env"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] } +axum = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +[workspace.lints.rust] +unsafe_code = "deny" + +[workspace.lints.clippy] +dbg_macro = "deny" +todo = "deny" diff --git a/judge/README.md b/judge/README.md new file mode 100644 index 0000000..1aec378 --- /dev/null +++ b/judge/README.md @@ -0,0 +1,29 @@ +# Rust 判题工程 + +Cargo workspace 分为五个 crate: + +| Crate | 职责 | 当前能力 | +| --- | --- | --- | +| `kf-contracts` | 跨服务通知和执行契约 | 最小 v1 通知、共享 JSON 样例检查 | +| `kf-sandbox` | Linux 隔离、限制、计量与清理 | `probe` 只读观察;`run` 拒绝执行 | +| `kf-engine` | 语言适配、测试点编排、比较与确定性汇总 | 预留库边界,无模拟判定 | +| `kf-worker` | 拉取、租约、工件准备与本地编排 | `--check` 入口,无消费循环 | +| `kf-coordinator` | 节点登记、租约、状态与结果写入权 | HTTP 存活检查,无任务端点 | + +```bash +# 从仓库根目录调用。 +make install +make check-judge +make sandbox-probe +make worker-check +make coordinator +``` + +工具链由 `rust-toolchain.toml` 固定,依赖由 `Cargo.lock` 固定。 +协调器默认只监听 `127.0.0.1:8100`,唯一接口是 `/health/live`。 +`KF_COORDINATOR_LISTEN` 可覆盖地址;在业务协议落地前不对外暴露内部服务。 + +沙箱的 `probe` 输出只表明当前进程看到的 namespace/cgroup/seccomp 信息, +不代表已经验证创建权限、委派或安全隔离。`run` 返回结构化拒绝和退出码 2。 +真正的权限操作和 `unsafe` 代码只能落在沙箱模块,实施时逐项记录安全理由与 Linux 验证。 +当前统一禁止 `unsafe`,需要时在对应模块明确缩小例外范围。 diff --git a/judge/crates/contracts/Cargo.toml b/judge/crates/contracts/Cargo.toml new file mode 100644 index 0000000..07b0e0b --- /dev/null +++ b/judge/crates/contracts/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "kf-contracts" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +serde.workspace = true +uuid.workspace = true + +[dev-dependencies] +serde_json.workspace = true + +[lints] +workspace = true diff --git a/judge/crates/contracts/src/lib.rs b/judge/crates/contracts/src/lib.rs new file mode 100644 index 0000000..ff4bbcb --- /dev/null +++ b/judge/crates/contracts/src/lib.rs @@ -0,0 +1,43 @@ +//! Wire contracts only. No OS, networking, database or business orchestration. + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub const SCHEMA_VERSION: u16 = 1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "u16", into = "u16")] +pub struct SchemaVersion; + +impl TryFrom for SchemaVersion { + type Error = &'static str; + + fn try_from(value: u16) -> Result { + if value == SCHEMA_VERSION { + Ok(Self) + } else { + Err("unsupported contract version") + } + } +} + +impl From for u16 { + fn from(_: SchemaVersion) -> Self { + SCHEMA_VERSION + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskKind { + #[serde(rename = "judge.evaluation")] + JudgeEvaluation, +} + +/// Notification only; consumers must obtain execution authority from the coordinator. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JudgeTaskNotification { + pub schema_version: SchemaVersion, + pub kind: TaskKind, + pub evaluation_id: Uuid, +} diff --git a/judge/crates/contracts/tests/wire_examples.rs b/judge/crates/contracts/tests/wire_examples.rs new file mode 100644 index 0000000..d558b76 --- /dev/null +++ b/judge/crates/contracts/tests/wire_examples.rs @@ -0,0 +1,29 @@ +use kf_contracts::JudgeTaskNotification; +use std::{fs, path::Path}; + +#[test] +fn shared_examples_match_python_contract() { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/fixtures/contracts"); + let mut count = 0; + for entry in fs::read_dir(fixtures).expect("shared fixture directory must exist") { + let path = entry.expect("fixture entry").path(); + let name = path.file_name().expect("fixture name").to_string_lossy(); + if !name.starts_with("judge-task.") || !name.ends_with(".json") { + continue; + } + count += 1; + let text = fs::read_to_string(&path).expect("fixture is UTF-8"); + let decoded = serde_json::from_str::(&text); + if name.contains(".invalid-") { + assert!(decoded.is_err(), "must reject {name}"); + } else { + let notification = decoded.unwrap_or_else(|error| panic!("{name}: {error}")); + let original: serde_json::Value = serde_json::from_str(&text).expect("valid JSON"); + assert_eq!(serde_json::to_value(notification).unwrap(), original); + } + } + assert!( + count >= 5, + "shared acceptance/rejection cases must not disappear" + ); +} diff --git a/judge/crates/coordinator/Cargo.toml b/judge/crates/coordinator/Cargo.toml new file mode 100644 index 0000000..66853bc --- /dev/null +++ b/judge/crates/coordinator/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "kf-coordinator" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +kf-contracts.workspace = true +axum.workspace = true +tokio.workspace = true +clap.workspace = true +serde.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true + +[lints] +workspace = true diff --git a/judge/crates/coordinator/src/api.rs b/judge/crates/coordinator/src/api.rs new file mode 100644 index 0000000..4c0d7e7 --- /dev/null +++ b/judge/crates/coordinator/src/api.rs @@ -0,0 +1,21 @@ +use axum::{Json, Router, routing::get}; +use serde::Serialize; + +#[derive(Serialize)] +struct HealthResponse { + status: &'static str, + service: &'static str, + version: &'static str, +} + +async fn liveness() -> Json { + Json(HealthResponse { + status: "ok", + service: "coordinator", + version: env!("CARGO_PKG_VERSION"), + }) +} + +pub fn router() -> Router { + Router::new().route("/health/live", get(liveness)) +} diff --git a/judge/crates/coordinator/src/lib.rs b/judge/crates/coordinator/src/lib.rs new file mode 100644 index 0000000..9f05be0 --- /dev/null +++ b/judge/crates/coordinator/src/lib.rs @@ -0,0 +1,6 @@ +//! Internal HTTP process and future lease/state authority. +//! +//! Only process liveness is exposed now. SQLx repositories and task transitions +//! are added after DR-01/PF-01 define their shared schema and concurrency rules. + +pub mod api; diff --git a/judge/crates/coordinator/src/main.rs b/judge/crates/coordinator/src/main.rs new file mode 100644 index 0000000..95d91ba --- /dev/null +++ b/judge/crates/coordinator/src/main.rs @@ -0,0 +1,31 @@ +use clap::Parser; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tracing_subscriber::EnvFilter; + +#[derive(Parser)] +#[command(version, about = "KF judge coordinator HTTP shell")] +struct Cli { + #[arg(long, env = "KF_COORDINATOR_LISTEN", default_value = "127.0.0.1:8100")] + listen: SocketAddr, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .json() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + let listener = TcpListener::bind(Cli::parse().listen).await?; + tracing::info!(address = %listener.local_addr()?, "coordinator scaffold listening"); + axum::serve(listener, kf_coordinator::api::router()) + .with_graceful_shutdown(async { + if let Err(error) = tokio::signal::ctrl_c().await { + tracing::error!(%error, "failed to listen for shutdown signal"); + } + }) + .await?; + Ok(()) +} diff --git a/judge/crates/engine/Cargo.toml b/judge/crates/engine/Cargo.toml new file mode 100644 index 0000000..c725af3 --- /dev/null +++ b/judge/crates/engine/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "kf-engine" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +kf-contracts.workspace = true + +[lints] +workspace = true diff --git a/judge/crates/engine/src/lib.rs b/judge/crates/engine/src/lib.rs new file mode 100644 index 0000000..6956846 --- /dev/null +++ b/judge/crates/engine/src/lib.rs @@ -0,0 +1,6 @@ +//! Reserved for deterministic judging orchestration (JS-05/JS-06). +//! +//! Owns language adaptation, case order, output comparison and verdict aggregation. +//! Execution is supplied through a port; this crate must not access SQL, AMQP, +//! HTTP, privilege setup or hidden host paths directly. +//! No fake verdict or user-program execution is provided by this scaffold. diff --git a/judge/crates/sandbox/Cargo.toml b/judge/crates/sandbox/Cargo.toml new file mode 100644 index 0000000..c36bdf2 --- /dev/null +++ b/judge/crates/sandbox/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "kf-sandbox" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +kf-contracts.workspace = true +clap.workspace = true +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/judge/crates/sandbox/src/lib.rs b/judge/crates/sandbox/src/lib.rs new file mode 100644 index 0000000..2181421 --- /dev/null +++ b/judge/crates/sandbox/src/lib.rs @@ -0,0 +1,6 @@ +//! Linux execution boundary. Current implementation only provides read-only observations. +//! +//! JS-01 defines execution contracts; JS-02..04 add the launcher, isolation and limits. +//! Until that work is verified, the CLI cannot execute any user program. + +pub mod probe; diff --git a/judge/crates/sandbox/src/main.rs b/judge/crates/sandbox/src/main.rs new file mode 100644 index 0000000..05f26e8 --- /dev/null +++ b/judge/crates/sandbox/src/main.rs @@ -0,0 +1,38 @@ +use clap::{Parser, Subcommand}; +use std::process::ExitCode; + +#[derive(Parser)] +#[command(version, about = "KF Linux sandbox entrypoint")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Observe the current Linux environment without changing it. + Probe, + /// Reserved execution entrypoint; refuses execution until isolation is implemented. + Run, +} + +fn main() -> ExitCode { + match Cli::parse().command { + Command::Probe => match serde_json::to_string_pretty(&kf_sandbox::probe::observe()) { + Ok(report) => { + println!("{report}"); + ExitCode::SUCCESS + } + Err(error) => { + eprintln!("cannot encode environment observation: {error}"); + ExitCode::FAILURE + } + }, + Command::Run => { + eprintln!( + "{{\"error\":\"sandbox_not_implemented\",\"message\":\"isolated execution is unavailable\"}}" + ); + ExitCode::from(2) + } + } +} diff --git a/judge/crates/sandbox/src/probe.rs b/judge/crates/sandbox/src/probe.rs new file mode 100644 index 0000000..45dea35 --- /dev/null +++ b/judge/crates/sandbox/src/probe.rs @@ -0,0 +1,49 @@ +use serde::Serialize; +use std::fs; + +#[derive(Debug, Serialize)] +pub struct EnvironmentObservation { + pub os: &'static str, + pub namespace_entries: Vec, + pub cgroup_v2_visible: bool, + pub cgroup_v2_controllers: Vec, + pub current_process_seccomp_mode: Option, + pub isolation_verified: bool, +} + +/// Filesystem observations do not verify delegation, syscall access or safe execution. +pub fn observe() -> EnvironmentObservation { + let mut namespace_entries: Vec = fs::read_dir("/proc/self/ns") + .map(|entries| { + entries + .filter_map(Result::ok) + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + .collect() + }) + .unwrap_or_default(); + namespace_entries.sort(); + let controllers = fs::read_to_string("/sys/fs/cgroup/cgroup.controllers"); + let cgroup_v2_visible = controllers.is_ok(); + let cgroup_v2_controllers = controllers + .unwrap_or_default() + .split_whitespace() + .map(str::to_owned) + .collect(); + let current_process_seccomp_mode = + fs::read_to_string("/proc/self/status") + .ok() + .and_then(|status| { + status.lines().find_map(|line| { + line.strip_prefix("Seccomp:") + .and_then(|mode| mode.trim().parse().ok()) + }) + }); + EnvironmentObservation { + os: std::env::consts::OS, + namespace_entries, + cgroup_v2_visible, + cgroup_v2_controllers, + current_process_seccomp_mode, + isolation_verified: false, + } +} diff --git a/judge/crates/sandbox/tests/refuse_execution.rs b/judge/crates/sandbox/tests/refuse_execution.rs new file mode 100644 index 0000000..7a0e980 --- /dev/null +++ b/judge/crates/sandbox/tests/refuse_execution.rs @@ -0,0 +1,14 @@ +use std::process::Command; + +#[test] +fn execution_entrypoint_fails_closed() { + let output = Command::new(env!("CARGO_BIN_EXE_kf-sandbox")) + .arg("run") + .output() + .expect("sandbox binary must start"); + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let error: serde_json::Value = + serde_json::from_slice(&output.stderr).expect("structured refusal"); + assert_eq!(error["error"], "sandbox_not_implemented"); +} diff --git a/judge/crates/worker/Cargo.toml b/judge/crates/worker/Cargo.toml new file mode 100644 index 0000000..71152e8 --- /dev/null +++ b/judge/crates/worker/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "kf-worker" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +kf-contracts.workspace = true +kf-engine.workspace = true +kf-sandbox.workspace = true +clap.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/judge/crates/worker/src/main.rs b/judge/crates/worker/src/main.rs new file mode 100644 index 0000000..893ae94 --- /dev/null +++ b/judge/crates/worker/src/main.rs @@ -0,0 +1,28 @@ +use clap::Parser; +use std::process::ExitCode; + +#[derive(Parser)] +#[command(version, about = "KF judge worker entrypoint")] +struct Cli { + /// Check the process entrypoint without consuming or executing tasks. + #[arg(long)] + check: bool, +} + +fn main() -> ExitCode { + if !Cli::parse().check { + eprintln!("judge worker task loop is not implemented; use --check"); + return ExitCode::from(2); + } + println!( + "{}", + serde_json::json!({ + "service": "judge-worker", + "version": env!("CARGO_PKG_VERSION"), + "status": "scaffold", + "contract_version": kf_contracts::SCHEMA_VERSION, + "consuming_tasks": false + }) + ); + ExitCode::SUCCESS +} diff --git a/judge/rust-toolchain.toml b/judge/rust-toolchain.toml new file mode 100644 index 0000000..b432996 --- /dev/null +++ b/judge/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.99.0" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/scripts/check_architecture.py b/scripts/check_architecture.py new file mode 100644 index 0000000..b6dac3e --- /dev/null +++ b/scripts/check_architecture.py @@ -0,0 +1,109 @@ +"""Enforce Python import direction and the Rust internal crate dependency graph.""" + +import ast +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE = ROOT / "backend/src/kf_oj" +RUST_ALLOWED = { + "kf-contracts": set(), + "kf-sandbox": {"kf-contracts"}, + "kf-engine": {"kf-contracts"}, + "kf-coordinator": {"kf-contracts"}, + "kf-worker": {"kf-contracts", "kf-engine", "kf-sandbox"}, +} +PYTHON_ALLOWED = { + "contracts": {"contracts"}, + "core": {"core", "contracts"}, + "infra": {"infra", "core", "contracts"}, + "runtime": {"runtime", "core", "contracts"}, + "modules": {"modules", "runtime", "infra", "core", "contracts"}, + "api": {"api", "modules", "runtime", "infra", "core", "contracts"}, + "workers": {"workers", "modules", "runtime", "infra", "core", "contracts"}, +} + + +def imports(tree: ast.AST, package: str): + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + yield alias.name + elif isinstance(node, ast.ImportFrom): + if node.level: + parts = package.split(".") + base = ".".join(parts[: len(parts) - node.level + 1]) + target = ".".join(filter(None, [base, node.module])) + else: + target = node.module or "" + for alias in node.names: + yield target if alias.name == "*" else f"{target}.{alias.name}" + + +def python_violations() -> list[str]: + violations = [] + for path in sorted(PACKAGE.rglob("*.py")): + relative = path.relative_to(PACKAGE) + if len(relative.parts) < 2: + continue + # Offline build tooling composes the API to export its schema, not a runtime dependency. + if relative.as_posix() == "contracts/export.py": + continue + layer = relative.parts[0] + package = "kf_oj." + ".".join(relative.parts[:-1]) + for target in imports(ast.parse(path.read_text(encoding="utf-8")), package): + parts = target.split(".") + if len(parts) < 2 or parts[0] != "kf_oj" or parts[1] == "__version__": + continue + if parts[1] not in PYTHON_ALLOWED.get(layer, set()): + violations.append(f"{path.relative_to(ROOT)}: {layer} cannot import {target}") + elif layer == "modules" and parts[1] == "modules" and len(parts) >= 3: + own_module = relative.parts[1] + if parts[2] != own_module and (len(parts) < 4 or parts[3] != "public"): + violations.append( + f"{path.relative_to(ROOT)}: cross-module import must use public.py: " + f"{target}" + ) + return violations + + +def rust_violations() -> list[str]: + violations = [] + manifests = list((ROOT / "judge/crates").glob("*/Cargo.toml")) + found = set() + for path in manifests: + manifest = tomllib.loads(path.read_text(encoding="utf-8")) + name = manifest["package"]["name"] + found.add(name) + if name not in RUST_ALLOWED: + violations.append(f"{path.relative_to(ROOT)}: declare the new crate boundary first") + continue + groups = [manifest] + groups.extend(manifest.get("target", {}).values()) + for group in groups: + for section in ("dependencies", "dev-dependencies", "build-dependencies"): + for key, dependency in group.get(section, {}).items(): + actual = dependency.get("package", key) if isinstance(dependency, dict) else key + if actual.startswith("kf-") and actual not in RUST_ALLOWED[name]: + violations.append(f"{name} cannot depend on {actual}") + if name in {"kf-contracts", "kf-engine"}: + allowed_external = {"serde", "serde_json", "uuid"} + for key in manifest.get("dependencies", {}): + if not key.startswith("kf-") and key not in allowed_external: + violations.append(f"{name}: domain/contract crate cannot depend on {key}") + if found != set(RUST_ALLOWED): + violations.append("Rust workspace crates differ from the declared architecture") + return violations + + +def main() -> None: + violations = python_violations() + rust_violations() + if violations: + print("\n".join(violations), file=sys.stderr) + raise SystemExit(1) + print("Architecture boundaries: OK (Python imports + Rust crate graph)") + + +if __name__ == "__main__": + main() diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..79646a0 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,7 @@ +# 平台内置技能 + +此目录预留给 KF-OnlineJudge 自有的声明式内置技能:提交复盘、渐进式答疑、专项训练和阶段复习。 +这里的内容属于产品数据,不是指示开发工具运行的 Codex Skills。 + +包格式和许可规则在 SP-01 中固定后再添加实际技能。 +个人绑定、配置、发布记录和私有学习证据由后端对应业务模块管理;不存放在共享包中。 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..28ee8ff --- /dev/null +++ b/tests/README.md @@ -0,0 +1,7 @@ +# 跨工程测试材料 + +`fixtures/contracts/` 保存 Python/Rust 共用的合法与非法协议样例。 +各工程自身的测试仍放在 `backend/tests/` 与 Rust crate 的 `tests/` 中。 + +后续在这里增加构造题目、正常/异常程序及端到端夹具,不提交真实私有代码或隐藏题库。 +沙箱破坏性测试在明确的专用执行环境中进行,不由普通构建检查自动运行。 diff --git a/tests/fixtures/contracts/judge-task.invalid-extra.json b/tests/fixtures/contracts/judge-task.invalid-extra.json new file mode 100644 index 0000000..ba47cf6 --- /dev/null +++ b/tests/fixtures/contracts/judge-task.invalid-extra.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "kind": "judge.evaluation", + "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2", + "source_code": "not allowed in notification" +} diff --git a/tests/fixtures/contracts/judge-task.invalid-id.json b/tests/fixtures/contracts/judge-task.invalid-id.json new file mode 100644 index 0000000..9415eda --- /dev/null +++ b/tests/fixtures/contracts/judge-task.invalid-id.json @@ -0,0 +1,5 @@ +{ + "schema_version": 1, + "kind": "judge.evaluation", + "evaluation_id": "not-a-uuid" +} diff --git a/tests/fixtures/contracts/judge-task.invalid-kind.json b/tests/fixtures/contracts/judge-task.invalid-kind.json new file mode 100644 index 0000000..98df0fe --- /dev/null +++ b/tests/fixtures/contracts/judge-task.invalid-kind.json @@ -0,0 +1,5 @@ +{ + "schema_version": 1, + "kind": "learning.run", + "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2" +} diff --git a/tests/fixtures/contracts/judge-task.invalid-version.json b/tests/fixtures/contracts/judge-task.invalid-version.json new file mode 100644 index 0000000..750a63c --- /dev/null +++ b/tests/fixtures/contracts/judge-task.invalid-version.json @@ -0,0 +1,5 @@ +{ + "schema_version": 2, + "kind": "judge.evaluation", + "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2" +} diff --git a/tests/fixtures/contracts/judge-task.valid.json b/tests/fixtures/contracts/judge-task.valid.json new file mode 100644 index 0000000..0a8cc84 --- /dev/null +++ b/tests/fixtures/contracts/judge-task.valid.json @@ -0,0 +1,5 @@ +{ + "schema_version": 1, + "kind": "judge.evaluation", + "evaluation_id": "018f26d6-713c-7b7f-8d5d-5f832a5f47f2" +}