diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..137976e7 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,57 @@ +name: Run Pipeline Benchmark + +on: + pull_request: + branches: [ main, development ] + +jobs: + benchmark: + runs-on: ubuntu-latest + + services: + ollama: + image: ollama/ollama:latest + ports: + - 11434:11434 + + steps: + - name: Checkout PR Branch + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install pytest + + - name: Run PR Branch Benchmark + run: | + pytest benchmark/test_benchmark.py -v + mv benchmark/benchmark_report.json branch_report.json + + - name: Checkout Target Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + clean: false + + - name: Run Target Branch Benchmark + run: | + pytest benchmark/test_benchmark.py -v + mv benchmark/benchmark_report.json target_report.json + + - name: Compare Benchmarks + id: compare + run: | + python benchmark/compare_benchmarks.py branch_report.json target_report.json > comparison.md + cat comparison.md + + - name: Comment PR with results + uses: thollander/actions-comment-pull-request@v3 + with: + filePath: comparison.md diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 39bd9d86..25804171 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -15,4 +15,4 @@ jobs: - name: Build Docker image run: | - docker build -t fireform:test . \ No newline at end of file + docker build -t fireform:test -f docker/prod/Dockerfile . \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 93d41fbb..22edb610 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Lint on: push: - branches: [main] + branches: [main,development] pull_request: - branches: [main] + branches: [main,development] jobs: lint: @@ -19,11 +19,12 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install linter - run: | - python -m pip install --upgrade pip - pip install ruff + run: uv pip install --system ruff - name: Run linter run: | - ruff check src/ --output-format=github \ No newline at end of file + ruff check app/ --output-format=github \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d6d1f835..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release Electron App - -on: - push: - tags: ["v*"] - -jobs: - build: - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - runs-on: ${{ matrix.os }} - defaults: - run: - working-directory: frontend - - permissions: - contents: write - - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install Python dependencies - working-directory: . - shell: bash - run: | - python -m pip install --upgrade pip - if [ "$RUNNER_OS" == "macOS" ]; then - pip install pyinstaller - pip install -r requirements.txt - else - pip install torch --index-url https://download.pytorch.org/whl/cpu - pip install pyinstaller - pip install -r requirements.txt - fi - - - name: Build Python backend with PyInstaller - working-directory: . - shell: bash - run: | - pyinstaller --name api-backend --onefile api/main.py - mkdir -p frontend/bin - cp dist/api-backend* frontend/bin/ - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: npm ci - - - name: Build & publish - run: npx electron-builder --publish always - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c90b98e..281b6b16 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,14 +2,39 @@ name: Tests on: push: - branches: [main] + branches: [main,development] + paths: + - '**.py' + - 'requirements.txt' + - 'alembic.ini' + - '.github/workflows/tests.yml' pull_request: - branches: [main] + branches: [main,development] + paths: + - '**.py' + - 'requirements.txt' + - 'alembic.ini' + - '.github/workflows/tests.yml' jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: fireform + POSTGRES_PASSWORD: fireform + POSTGRES_DB: fireform + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U fireform" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - uses: actions/checkout@v4 @@ -18,13 +43,22 @@ jobs: with: python-version: "3.11" + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + run: uv pip install --system -r requirements.txt - name: Run tests env: PYTHONPATH: . run: | - python -m pytest tests/ -v --tb=short \ No newline at end of file + python -m pytest tests/ -v --tb=short + + - name: Check for un-migrated model changes + env: + PYTHONPATH: . + DATABASE_URL: postgresql://fireform:fireform@localhost:5432/fireform + run: | + python -m alembic upgrade head + python -m alembic check \ No newline at end of file diff --git a/.gitignore b/.gitignore index b5a9a904..1515bf38 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ src/inputs/*.pdf frontend/release/ # Local Claude Code instructions -CLAUDE.md \ No newline at end of file +CLAUDE.md + +*temp/ +*.env* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 154daf8a..d30e9b93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,25 +39,14 @@ If you have a great idea for FireForm, we'd love to hear it! Please open an issu 4. Make sure your code lints. 5. Issue that pull request! -## 🛠️ Local Development Setup - -FireForm uses Docker and Docker Compose for the backend to ensure a consistent environment. +**Issues are not formally assigned.** You are free to pick any open issue, work on it, and raise a PR directly. If multiple PRs address the same issue, the first one that actually fixes it generally gets preference. To avoid duplicating someone else's work, coordinate with other contributors on our [Discord](https://discord.gg/nBv5b6kF68) before starting. -### Prerequisites +## 💬 Community -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) -- `make` (optional, but recommended) -- [Node.js](https://nodejs.org/) 20+ (only needed for the desktop app) +Join our Discord server to ask questions, discuss issues, and coordinate work with other contributors: https://discord.gg/nBv5b6kF68 -### Desktop App Development - -The frontend is a vanilla HTML/CSS/JS app wrapped in Electron. To run it locally: +## 🛠️ Local Development Setup -```bash -cd frontend -npm install # one-time setup -npm start # launches the Electron desktop window -``` +See the [Setup Guide](docs/1.%20SETUP.md) for the full walkthrough: prerequisites, running the backend with Docker, testing endpoints via Swagger UI, day-to-day commands, and troubleshooting. -The backend (API + Ollama) must be running separately via Docker — see `make fireform`. +Before writing code, read the [Project Structure](docs/2.%20PROJECT_STRUCTURE.md) guide — it explains how the codebase is organized and where new code should go. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 282eb251..00000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ - -FROM python:3.11-slim - -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - curl \ - libgl1 \ - libglib2.0-0 \ - libxcb1 \ - && rm -rf /var/lib/apt/lists/* - -# Copy and install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copy application code -COPY . . - -# All imports use api.*, src.* which require the root to be on the path -ENV PYTHONPATH=/app - -# Expose FastAPI port -EXPOSE 8000 - -# Start the FastAPI server (not tail -f /dev/null which does nothing) -CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Makefile b/Makefile index 026a721a..fd8a6947 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -.PHONY: help build up down logs shell exec pull-model test clean fireform logs-app logs-ollama logs-frontend super-clean +.PHONY: help init fireform build up down logs logs-app logs-ollama shell pull-model test clean super-clean status ready-banner sync docs -# The extraction model pulled into Ollama and used by src/llm.py. Override with -# `make pull-model OLLAMA_MODEL=...`. A 1.5B model keeps per-field fills fast. -OLLAMA_MODEL ?= qwen2.5:1.5b +COMPOSE = docker compose -f docker/dev/compose.yml --env-file docker/.env.dev +ENV_DEV = docker/.env.dev + +# Read OLLAMA_MODEL from .env.dev at runtime; fall back to default if file absent. +OLLAMA_MODEL = $(shell grep -E '^OLLAMA_MODEL=' $(ENV_DEV) 2>/dev/null | cut -d= -f2 | tr -d '[:space:]' || echo qwen2.5:1.5b) help: @printf '%s\n' \ @@ -13,74 +15,111 @@ help: '/_/ /_//_/ \___/ /_/ \____/_/ /_/ /_/ /_/ ' \ '' @echo "" - @echo "Fireform Development Commands" + @echo "FireForm Development Commands" @echo "==============================" - @echo "make fireform - Build and start containers, then open a shell" + @echo "make init - First-time setup: check deps, create .env.dev, pick model" + @echo "make fireform - Build images, start containers, pull Ollama model" @echo "make build - Build Docker images" - @echo "make up - Start all containers" + @echo "make up - Start all containers (detached)" @echo "make down - Stop all containers" - @echo "make logs - View container logs" - @echo "make logs-app - View API container logs" - @echo "make logs-frontend - View frontend container logs" - @echo "make logs-ollama - View Ollama container logs" - @echo "make shell - Open Python shell in app container" - @echo "make exec - Execute Python script in container" - @echo "make pull-model - Pull the extraction model ($(OLLAMA_MODEL)) into Ollama" - @echo "make test - Run tests" - @echo "make clean - Remove containers" - @echo "make super-clean - [CAUTION] Use carefully. Cleans up ALL stopped containers, networks, build cache..." - -# Fix #382 — pull-model is now part of the main setup flow -# The extraction model is pulled automatically before you need it -fireform: build up pull-model + @echo "make sync - Fast-install new requirements.txt deps into running app (no rebuild)" + @echo "make status - Show compact container health summary" + @echo "make logs - Stream all container logs" + @echo "make logs-app - Stream app container logs" + @echo "make logs-ollama - Stream Ollama container logs" + @echo "make logs-worker - Stream Celery worker logs" + @echo "make shell - Open shell in running app container" + @echo "make pull-model - Pull Ollama model from .env.dev ($(OLLAMA_MODEL))" + @echo "make test - Run test suite" + @echo "make docs - Serve interactive API docs from the OpenAPI contract" + @echo "make migrate - Run pending Alembic migrations" + @echo "make migration - Generate new migration (msg='description')" + @echo "make clean - Stop containers (preserves volumes)" + @echo "make super-clean - [CAUTION] Stop containers, delete volumes, prune Docker" + +init: + @chmod +x scripts/check-deps.sh scripts/init-env.sh scripts/select-model.sh + @sh scripts/check-deps.sh + @sh scripts/init-env.sh + @sh scripts/select-model.sh + @printf "Build containers and pull model now? [y/N] "; \ + read answer; \ + case "$$answer" in \ + [yY]*) $(MAKE) fireform ;; \ + *) echo "Run 'make fireform' when ready." ;; \ + esac + +fireform: + @$(COMPOSE) up -d --build + @if $(COMPOSE) exec -T ollama ollama list 2>/dev/null | grep -q "^$(OLLAMA_MODEL)"; then \ + echo " Model $(OLLAMA_MODEL) already pulled."; \ + else \ + echo " Pulling $(OLLAMA_MODEL)..."; \ + $(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL); \ + fi + @$(MAKE) --no-print-directory ready-banner + +build: + @$(COMPOSE) build + +up: + @$(COMPOSE) up -d + @$(MAKE) --no-print-directory ready-banner + +# Fast path for "I added a package": install the delta into the running container +# (no image rebuild, no 1.6GB layer re-export). uv installs only what's missing in +sync: + @$(COMPOSE) exec -T app sh -c "UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt" + +status: + @$(COMPOSE) ps --format 'table {{.Service}}\t{{.Status}}' + +ready-banner: @echo "" - @echo "✅ FireForm is ready!" - @echo " Frontend: http://localhost:5173" + @echo "FireForm is ready!" @echo " API: http://localhost:8000" @echo " API Docs: http://localhost:8000/docs" @echo "" @echo "Run 'make logs' to view live logs, 'make down' to stop." -build: - docker compose build - -up: - docker compose up -d - down: - docker compose down + @$(COMPOSE) down --remove-orphans logs: - docker compose logs -f + @$(COMPOSE) logs -f logs-app: - docker compose logs -f app + @$(COMPOSE) logs -f app logs-ollama: - docker compose logs -f ollama + @$(COMPOSE) logs -f ollama -logs-frontend: - docker compose logs -f frontend +logs-worker: + @$(COMPOSE) logs -f celery-worker shell: - docker compose exec app /bin/bash - -# Start the FastAPI server inside the running container -run: - docker compose exec app uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload - -exec: - docker compose exec app python3 src/main.py + @$(COMPOSE) exec app /bin/sh pull-model: - docker compose exec ollama ollama pull $(OLLAMA_MODEL) + @$(COMPOSE) exec -T ollama ollama pull $(OLLAMA_MODEL) -# Fix — correct test directory (was src/test/ which doesn't exist) test: - docker compose exec app python3 -m pytest tests/ -v + @$(COMPOSE) exec -T app python3 -m pytest tests/ -v + +docs: + @echo "Serving Swagger UI from contracts/openapi.yaml at http://localhost:8088" + @npx --yes swagger-ui-watcher contracts/openapi.yaml --port 8088 + +migrate: + @$(COMPOSE) exec -T app alembic upgrade head + +migration: + @$(COMPOSE) exec -T app alembic revision --autogenerate -m "$(msg)" clean: - docker compose down -v + @$(COMPOSE) down + super-clean: - docker compose down -v - docker system prune + @echo "WARNING: this will delete all volumes (database, uploads, model weights)." + @$(COMPOSE) down -v + @docker system prune -f diff --git a/README.md b/README.md index 15a67a00..a07ea1f0 100644 --- a/README.md +++ b/README.md @@ -102,14 +102,74 @@ FireForm is designed from the ground up to ensure that all generated and collect ## 🔒 Privacy & Applicable Laws -FireForm is built on a local-first architecture, ensuring that all processing (including AI extraction) occurs locally on the user's hardware. We do not collect, process, or share any Personally Identifiable Information (PII) with third parties. By keeping data completely offline, FireForm natively assists deploying organizations in complying with strict data protection laws such as **HIPAA**, **CCPA**, and **GDPR**. +FireForm is built on a **local-first architecture**: all processing (including AI extraction) occurs locally on the operator's hardware and nothing is transmitted to external servers by default. However, operators should be aware of the following: -For complete details on our data handling, consent management procedures, and how the solution was designed to comply with HIPAA, CCPA, and GDPR, please review our public [Privacy Policy](https://fireform-core.github.io/FireForm/privacy.html). +- **PII is processed locally.** Incident descriptions, names, dates, and other personally identifiable information are extracted by the local LLM, written into filled PDF files, and persisted in the local database (`FormSubmission` records). These files and records remain on disk until explicitly deleted. +- **No external transmission.** Data does not leave the machine by default. Audio transcription (Whisper) and LLM inference (Ollama) are both local services. +- **Operator responsibility.** Because PII is stored locally, the security of that data depends entirely on the operator's host system, filesystem permissions, backup practices, and access controls. + +For complete details on data handling and compliance guidance, please review our public [Privacy Policy](https://fireform-core.github.io/FireForm/privacy.html). + +## 🗑️ Data Deletion & Retention + +FireForm provides API endpoints and an automated purge mechanism to manage stored PII. + +### Deleting individual records + +```bash +# Delete a single form submission (removes DB record + output PDF) +curl -X DELETE http://localhost:8000/api/v1/forms/{submission_id} \ + -H "X-API-Key: your-key" + +# Delete a template and all associated submissions +curl -X DELETE http://localhost:8000/api/v1/templates/{template_id} \ + -H "X-API-Key: your-key" +``` + +### Bulk purge + +```bash +# Purge all submissions older than 30 days +curl -X POST "http://localhost:8000/api/v1/forms/purge?days=30" \ + -H "X-API-Key: your-key" +``` + +### Automated retention + +Set `RETENTION_PERIOD_DAYS` in `docker/.env.dev` (default: `30`). Celery Beat will automatically run a purge job every night at 03:00 UTC: + +```env +RETENTION_PERIOD_DAYS=30 +``` + +To enable the scheduler: `celery -A app.core.celery beat -l info` + +## 🔑 Access Control + +Deletion and purge endpoints can be protected by setting an API key: + +```env +FIREFORM_API_KEY=your-strong-secret-key +``` + +When set, all `DELETE` and `POST /purge` requests must include one of: +- **Header:** `X-API-Key: your-strong-secret-key` +- **Bearer token:** `Authorization: Bearer your-strong-secret-key` + +Read-only endpoints (list, preview, fill) do **not** require authentication. + +## 🛡️ Operator Hardening Recommendations + +- **Filesystem permissions:** Restrict access to `data/inputs/` and `data/outputs/` to the application user only (`chmod 700`). +- **Backups:** Encrypt any backups containing output PDFs. +- **HTTPS:** Deploy behind a reverse proxy (e.g., nginx) with TLS enabled. +- **API key rotation:** Rotate `FIREFORM_API_KEY` regularly and never commit it to version control. +- **Retention policy:** Configure `RETENTION_PERIOD_DAYS` in line with applicable regulations (HIPAA, GDPR, CCPA). ## 🛡️ Do No Harm by Design FireForm is built to anticipate and prevent harm: -- **Data Privacy & Security (9A):** We handle sensitive incident data entirely offline. By never transmitting data to the cloud, we natively prevent online data breaches of PII. +- **Data Privacy & Security (9A):** We handle sensitive incident data entirely offline. By never transmitting data to the cloud, we natively prevent online data breaches of PII. Operators are responsible for securing local storage; see the Operator Hardening Recommendations above. - **Inappropriate Content (9B):** The application is an internal productivity tool without public social interactions or user-generated content hosting, completely mitigating the risks of public harassment or illegal content distribution. - **Protection from Harassment (9C):** For our open-source contributor community, we strictly enforce our [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a safe, harassment-free environment for all contributors. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..2788aaa8 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,37 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +path_separator = os + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[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/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..55888cee --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,59 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config, pool +from sqlmodel import SQLModel + +from alembic import context +from app.core.config import DATABASE_URL +from app.models import ( # noqa: F401 + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) + +config = context.config + +if not config.get_main_option("sqlalchemy.url"): + config.set_main_option("sqlalchemy.url", DATABASE_URL) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = SQLModel.metadata + + +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + connectable = config.attributes.get("connection", None) + + if connectable is None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + else: + context.configure(connection=connectable, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..6ce33510 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[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/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py new file mode 100644 index 00000000..04e6a1fc --- /dev/null +++ b/alembic/versions/001_initial_schema.py @@ -0,0 +1,65 @@ +"""Initial schema — Template, FormSubmission, and Job tables. + +Revision ID: 001 +Revises: +Create Date: 2026-06-22 + +""" +from collections.abc import Sequence + +import sqlalchemy as sa +import sqlmodel + +from alembic import op + +revision: str = "001" +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.create_table( + "template", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("name", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("fields", sa.JSON, nullable=False), + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("created_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "formsubmission", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=False), + sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("output_pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("created_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "job", + sa.Column("id", sa.Integer, primary_key=True, autoincrement=True), + sa.Column("job_id", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("celery_task_id", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("job_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("template_id", sa.Integer, sa.ForeignKey("template.id"), nullable=True), + sa.Column("input_text", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("progress_percent", sa.Integer, nullable=False), + sa.Column("result_url", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("error", sa.JSON, nullable=True), + sa.Column("model", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + op.create_index("ix_job_job_id", "job", ["job_id"], unique=True) + op.create_index("ix_job_celery_task_id", "job", ["celery_task_id"], unique=False) + + +def downgrade() -> None: + op.drop_index("ix_job_celery_task_id", table_name="job") + op.drop_index("ix_job_job_id", table_name="job") + op.drop_table("job") + op.drop_table("formsubmission") + op.drop_table("template") diff --git a/alembic/versions/002_v1_models.py b/alembic/versions/002_v1_models.py new file mode 100644 index 00000000..c943e9e8 --- /dev/null +++ b/alembic/versions/002_v1_models.py @@ -0,0 +1,145 @@ +"""v1 models — Input, Extraction, Incident, Form, Report tables. + +Revision ID: 002 +Revises: 001 +Create Date: 2026-06-26 + +JSON columns use sa.JSON (Postgres json type, not jsonb) for consistency with +migration 001 and SQLite test-harness compatibility. A follow-up can switch +high-query blobs (especially incident_contract) to jsonb via with_variant() if +containment operators or GIN indexing are needed. + +Form.job_id and Report.job_id are plain UUID columns with no FK constraint. +The contract Job model (UUID-PK, enum-typed) is a separate decision; the FK +will be added when that model is resolved. +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +import sqlmodel + +from alembic import op + +revision: str = "002" +down_revision: str | None = "001" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.create_table( + "inputs", + sa.Column("input_id", sa.Uuid(), primary_key=True), + sa.Column("input_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("transcript", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("original_filename", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("audio_duration_seconds", sa.Float, nullable=True), + sa.Column("character_count", sa.Integer, nullable=True), + sa.Column("word_count", sa.Integer, nullable=True), + sa.Column("station_id", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("responder_badge", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_date_hint", sa.Date, nullable=True), + sa.Column("error_detail", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "extractions", + sa.Column("extract_id", sa.Uuid(), primary_key=True), + sa.Column( + "input_id", + sa.Uuid(), + sa.ForeignKey("inputs.input_id"), + nullable=False, + ), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("started_at", sa.DateTime, nullable=True), + sa.Column("completed_at", sa.DateTime, nullable=True), + sa.Column("model_used", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("processing_time_seconds", sa.Float, nullable=True), + sa.Column("incident_contract", sa.JSON, nullable=True), + sa.Column("corrections", sa.JSON, nullable=True), + sa.Column("error_type", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("error_detail", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "incidents", + sa.Column("incident_id", sa.Uuid(), primary_key=True), + sa.Column( + "extract_id", + sa.Uuid(), + sa.ForeignKey("extractions.extract_id"), + nullable=False, + ), + sa.Column("incident_number", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("incident_name", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_type", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("incident_date", sa.Date, nullable=True), + sa.Column("tags", sa.JSON, nullable=True), + sa.Column("notes", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + sa.Column("deleted_at", sa.DateTime, nullable=True), + ) + + op.create_table( + "forms", + sa.Column("form_id", sa.Uuid(), primary_key=True), + sa.Column("form_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column( + "extract_id", + sa.Uuid(), + sa.ForeignKey("extractions.extract_id"), + nullable=False, + ), + sa.Column( + "incident_id", + sa.Uuid(), + sa.ForeignKey("incidents.incident_id"), + nullable=True, + ), + sa.Column("job_id", sa.Uuid(), nullable=True), # no FK — see module docstring + sa.Column("completed_at", sa.DateTime, nullable=True), + sa.Column("pdf_ready", sa.Boolean, nullable=False), + sa.Column("json_ready", sa.Boolean, nullable=False), + sa.Column("field_mapping_summary", sa.JSON, nullable=True), + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("json_data", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + op.create_table( + "reports", + sa.Column("report_id", sa.Uuid(), primary_key=True), + sa.Column("period_type", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("period_label", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("year", sa.Integer, nullable=False), + sa.Column("month", sa.Integer, nullable=True), + sa.Column("quarter", sa.Integer, nullable=True), + sa.Column("output_format", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("status", sqlmodel.sql.sqltypes.AutoString, nullable=False), + sa.Column("generated_at", sa.DateTime, nullable=True), + sa.Column("summary", sa.JSON, nullable=True), + sa.Column("job_id", sa.Uuid(), nullable=True), # no FK — see module docstring + sa.Column("pdf_path", sqlmodel.sql.sqltypes.AutoString, nullable=True), + sa.Column("json_data", sa.JSON, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False), + sa.Column("updated_at", sa.DateTime, nullable=False), + ) + + +def downgrade() -> None: + op.drop_table("reports") + op.drop_table("forms") + op.drop_table("incidents") + op.drop_table("extractions") + op.drop_table("inputs") diff --git a/api/db/database.py b/api/db/database.py deleted file mode 100644 index 97e6e622..00000000 --- a/api/db/database.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from sqlmodel import create_engine, Session - -# Define path to the database in the user's home directory -HOME_DIR = os.path.expanduser("~") -APP_DIR = os.path.join(HOME_DIR, ".fireform") -os.makedirs(APP_DIR, exist_ok=True) -DB_PATH = os.path.join(APP_DIR, "fireform.db") - -DATABASE_URL = f"sqlite:///{DB_PATH}" - -engine = create_engine( - DATABASE_URL, - echo=True, - connect_args={"check_same_thread": False}, -) - -def get_session(): - with Session(engine) as session: - yield session \ No newline at end of file diff --git a/api/db/init_db.py b/api/db/init_db.py deleted file mode 100644 index ea77cb62..00000000 --- a/api/db/init_db.py +++ /dev/null @@ -1,47 +0,0 @@ -import json -import datetime -from sqlmodel import SQLModel, Session, select -from api.db.database import engine -from api.db import models -from api.db.models import Template - -def seed_db(): - with Session(engine) as session: - # Check if we already have templates - statement = select(Template) - try: - results = session.exec(statement).first() - except Exception: - # Table might not exist yet if called at a weird time - results = None - - if not results: - print("Seeding database with default template...") - fields = { - "Employee's name": "string", - "Employee's job title": "string", - "Employee's department supervisor": "string", - "Employee's phone number": "string", - "Employee's email": "string", - "Signature": "string", - "Date": "string" - } - - # Using ID 2 as agreed to avoid any ID 1 corruption - default_template = Template( - id=2, - name="Manual Test Template", - fields=fields, - pdf_path="src/inputs/file_template_manual.pdf", - created_at=datetime.datetime.now() - ) - session.add(default_template) - session.commit() - print("Database seeded successfully.") - -def init_db(): - SQLModel.metadata.create_all(engine) - seed_db() - -if __name__ == "__main__": - init_db() \ No newline at end of file diff --git a/api/db/models.py b/api/db/models.py deleted file mode 100644 index 8f826556..00000000 --- a/api/db/models.py +++ /dev/null @@ -1,19 +0,0 @@ -from sqlmodel import SQLModel, Field -from sqlalchemy import Column, JSON -from datetime import datetime, timezone - - -class Template(SQLModel, table=True): - id: int | None = Field(default=None, primary_key=True) - name: str - fields: dict = Field(sa_column=Column(JSON)) - pdf_path: str - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - - -class FormSubmission(SQLModel, table=True): - id: int | None = Field(default=None, primary_key=True) - template_id: int = Field(foreign_key="template.id") - input_text: str - output_pdf_path: str - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/api/db/repositories.py b/api/db/repositories.py deleted file mode 100644 index a9ac3cfa..00000000 --- a/api/db/repositories.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlmodel import Session, select -from api.db.models import Template, FormSubmission - -# Templates -def create_template(session: Session, template: Template) -> Template: - session.add(template) - session.commit() - session.refresh(template) - return template - -def get_template(session: Session, template_id: int) -> Template | None: - return session.get(Template, template_id) - - -def list_templates(session: Session) -> list[Template]: - statement = select(Template).order_by(Template.created_at.desc(), Template.id.desc()) - return list(session.exec(statement)) - -# Forms -def create_form(session: Session, form: FormSubmission) -> FormSubmission: - session.add(form) - session.commit() - session.refresh(form) - return form diff --git a/api/deps.py b/api/deps.py deleted file mode 100644 index c2a0b748..00000000 --- a/api/deps.py +++ /dev/null @@ -1,4 +0,0 @@ -from api.db.database import get_session - -def get_db(): - yield from get_session() \ No newline at end of file diff --git a/api/errors/base.py b/api/errors/base.py deleted file mode 100644 index 1f81a082..00000000 --- a/api/errors/base.py +++ /dev/null @@ -1,4 +0,0 @@ -class AppError(Exception): - def __init__(self, message: str, status_code: int = 400): - self.message = message - self.status_code = status_code \ No newline at end of file diff --git a/api/errors/handlers.py b/api/errors/handlers.py deleted file mode 100644 index 903e744b..00000000 --- a/api/errors/handlers.py +++ /dev/null @@ -1,11 +0,0 @@ -from fastapi import Request -from fastapi.responses import JSONResponse -from api.errors.base import AppError - -def register_exception_handlers(app): - @app.exception_handler(AppError) - async def app_error_handler(request: Request, exc: AppError): - return JSONResponse( - status_code=exc.status_code, - content={"error": exc.message}, - ) \ No newline at end of file diff --git a/api/main.py b/api/main.py deleted file mode 100644 index 0c30bc5e..00000000 --- a/api/main.py +++ /dev/null @@ -1,42 +0,0 @@ -from contextlib import asynccontextmanager -import os - -# Disable CUDA to prevent PyTorch from trying to find NVIDIA drivers on Mac Silicon / Docker -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -from fastapi import FastAPI -from api.routes import templates, forms -from api.db.init_db import init_db -from api.errors.handlers import register_exception_handlers -from fastapi.middleware.cors import CORSMiddleware -from api.routes import forms, templates - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup: Initialize the database and seed it if necessary - print("Initializing database...") - init_db() - yield - # Shutdown logic goes here if needed - -app = FastAPI(lifespan=lifespan) - -register_exception_handlers(app) - -default_origins = "http://127.0.0.1:5173,http://localhost:5173" -allowed_origins = [ - origin.strip() - for origin in os.getenv("FRONTEND_ORIGINS", default_origins).split(",") - if origin.strip() -] - -app.add_middleware( - CORSMiddleware, - allow_origins=allowed_origins, - allow_credentials=False, - allow_methods=["*"], - allow_headers=["*"], -) - -app.include_router(templates.router) -app.include_router(forms.router) diff --git a/api/routes/__init__.py b/api/routes/__init__.py deleted file mode 100644 index e8fe8f45..00000000 --- a/api/routes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import templates, forms diff --git a/api/routes/forms.py b/api/routes/forms.py deleted file mode 100644 index 74069eaf..00000000 --- a/api/routes/forms.py +++ /dev/null @@ -1,107 +0,0 @@ -import os - -import requests -from fastapi import APIRouter, Depends, File, UploadFile -from sqlmodel import Session -from api.deps import get_db -from api.schemas.forms import ( - FormFill, - FormFillResponse, - ModelsResponse, - TranscriptionResponse, -) -from api.db.repositories import create_form, get_template -from api.db.models import FormSubmission -from api.errors.base import AppError -from src.controller import Controller - -router = APIRouter(prefix="/forms", tags=["forms"]) - - -@router.post("/fill", response_model=FormFillResponse) -def fill_form(form: FormFill, db: Session = Depends(get_db)): - - fetched_template = get_template(db, form.template_id) - if not fetched_template: - raise AppError("Template not found", status_code=404) - - controller = Controller() - try: - path = controller.fill_form( - user_input=form.input_text, - fields=fetched_template.fields, - pdf_form_path=fetched_template.pdf_path, - model=form.model, - ) - - # `model` is a runtime override, not a column — keep it out of the DB row. - submission = FormSubmission( - **form.model_dump(exclude={"model"}), output_pdf_path=path - ) - return create_form(db, submission) - except Exception as e: - raise AppError(str(e), status_code=500) - - -@router.get("/models", response_model=ModelsResponse) -def list_models(): - """List the Whisper-independent extraction models available in the local - Ollama instance, plus the configured default. Used by the Fill Form UI's - model picker. Falls back to just the default if Ollama is unreachable.""" - default_model = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") - - models: list[str] = [] - try: - response = requests.get(f"{ollama_host}/api/tags", timeout=10) - response.raise_for_status() - models = [m["name"] for m in response.json().get("models", []) if m.get("name")] - except requests.exceptions.RequestException: - models = [] - - # Always surface the configured default, even if Ollama hasn't pulled it yet. - if default_model not in models: - models.insert(0, default_model) - - return ModelsResponse(models=models, default=default_model) - - -@router.post("/transcribe", response_model=TranscriptionResponse) -def transcribe(audio: UploadFile = File(...)): - """Forward recorded audio to the local Whisper ASR sidecar and return text. - - Mirrors the Ollama wiring: WHISPER_HOST points at the whisper service - (http://whisper:9000 inside Docker, http://localhost:9000 otherwise). The - audio is streamed straight through to the local STT service and never - persisted — no PII leaves the machine. - """ - whisper_host = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") - whisper_url = f"{whisper_host}/asr" - - files = { - "audio_file": ( - audio.filename or "audio.wav", - audio.file.read(), - audio.content_type or "audio/wav", - ) - } - params = {"task": "transcribe", "output": "json", "encode": "true"} - - try: - response = requests.post(whisper_url, params=params, files=files, timeout=120) - response.raise_for_status() - except requests.exceptions.ConnectionError: - raise AppError( - f"Could not connect to the speech-to-text service at {whisper_url}. " - "Please ensure the whisper service is running.", - status_code=503, - ) - except requests.exceptions.RequestException as e: - raise AppError(f"Transcription failed: {e}", status_code=502) - - try: - text = (response.json().get("text") or "").strip() - except ValueError: - text = response.text.strip() - - return TranscriptionResponse(text=text) diff --git a/api/schemas/common.py b/api/schemas/common.py deleted file mode 100644 index 578cd2c0..00000000 --- a/api/schemas/common.py +++ /dev/null @@ -1,14 +0,0 @@ -from pydantic import BaseModel -from typing import Any, Optional - -class SuccessResponse(BaseModel): - success: bool = True - data: Any - -class ErrorDetail(BaseModel): - code: str - message: str - -class ErrorResponse(BaseModel): - success: bool = False - error: ErrorDetail \ No newline at end of file diff --git a/api/schemas/forms.py b/api/schemas/forms.py deleted file mode 100644 index ca996515..00000000 --- a/api/schemas/forms.py +++ /dev/null @@ -1,33 +0,0 @@ -from pydantic import BaseModel, field_validator - -class FormFill(BaseModel): - template_id: int - input_text: str - # Optional Ollama model override for this fill; falls back to OLLAMA_MODEL. - # Not persisted (no DB column) — excluded before building FormSubmission. - model: str | None = None - - @field_validator("input_text") - def validate_input_text(cls, value): - if not value or not value.strip(): - raise ValueError("Input text cannot be empty") - return value - - -class FormFillResponse(BaseModel): - id: int - template_id: int - input_text: str - output_pdf_path: str - - class Config: - from_attributes = True - - -class TranscriptionResponse(BaseModel): - text: str - - -class ModelsResponse(BaseModel): - models: list[str] - default: str \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 00000000..de9379be --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,8 @@ +"""FireForm backend application package.""" + +import os + +# Force CPU before any service module imports torch / rfdetr. Prevents PyTorch +# from probing for NVIDIA drivers on Mac Silicon and inside Docker. Runs here so +# it is guaranteed to execute before `app.main` imports the service layer. +os.environ["CUDA_VISIBLE_DEVICES"] = "" diff --git a/api/__init__.py b/app/api/__init__.py similarity index 100% rename from api/__init__.py rename to app/api/__init__.py diff --git a/app/api/deps.py b/app/api/deps.py new file mode 100644 index 00000000..7850dab0 --- /dev/null +++ b/app/api/deps.py @@ -0,0 +1,22 @@ +from fastapi import Header, Request + +from app.core.config import FIREFORM_API_KEY +from app.core.errors.base import AppError +from app.db.database import get_session + + +def get_db(): + yield from get_session() + +def verify_api_key(request: Request, x_api_key: str | None = Header(None)): + if not FIREFORM_API_KEY: + return + + provided_key = x_api_key + if not provided_key: + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + provided_key = auth_header[len("Bearer "):] + + if not provided_key or provided_key != FIREFORM_API_KEY: + raise AppError("Unauthorized", status_code=401, error_code="UNAUTHORIZED") \ No newline at end of file diff --git a/app/api/router.py b/app/api/router.py new file mode 100644 index 00000000..cc59847a --- /dev/null +++ b/app/api/router.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from app.api.routes import forms, input, jobs, system, templates, weather, zipcode +from app.core.config import API_PREFIX + +api_router = APIRouter() +api_router.include_router(templates.router, prefix=API_PREFIX) +api_router.include_router(forms.router, prefix=API_PREFIX) +api_router.include_router(system.router, prefix=API_PREFIX) +api_router.include_router(jobs.router, prefix=API_PREFIX) +api_router.include_router(weather.router, prefix=API_PREFIX) +api_router.include_router(zipcode.router, prefix=API_PREFIX) +api_router.include_router(input.router, prefix=API_PREFIX) \ No newline at end of file diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py new file mode 100644 index 00000000..d99c8894 --- /dev/null +++ b/app/api/routes/__init__.py @@ -0,0 +1,3 @@ +from . import forms, templates + +__all__ = ["forms", "templates"] diff --git a/app/api/routes/forms.py b/app/api/routes/forms.py new file mode 100644 index 00000000..60b8b577 --- /dev/null +++ b/app/api/routes/forms.py @@ -0,0 +1,234 @@ +from datetime import datetime, timedelta, timezone +from pathlib import Path + +import requests +from fastapi import APIRouter, Depends, File, Query, UploadFile +from sqlmodel import Session, select + +from app.api.deps import get_db, verify_api_key +from app.api.schemas.forms import ( + FormFill, + FormFillResponse, + ModelsResponse, + TranscriptionResponse, +) +from app.core.config import BASE_DIR, OLLAMA_HOST, OLLAMA_MODEL, RETENTION_PERIOD_DAYS +from app.core.errors.base import AppError +from app.db.repositories import ( + create_form, + delete_form_submission, + get_form_submission, + get_template, +) +from app.models import FormSubmission, Template +from app.services.controller import Controller +from app.services.whisper import call_whisper_asr + +PROJECT_ROOT = BASE_DIR + +def _resolve_project_file(file_path: str) -> Path: + raw_path = (file_path or "").strip() + if not raw_path: + raise AppError("Path is required", status_code=400) + + candidate = Path(raw_path) + if not candidate.is_absolute(): + candidate = (PROJECT_ROOT / candidate).resolve() + else: + candidate = candidate.resolve() + + if candidate != PROJECT_ROOT and PROJECT_ROOT not in candidate.parents: + raise AppError("Path must be inside the project", status_code=400) + + return candidate + +router = APIRouter(prefix="/forms", tags=["forms"]) + + +@router.post("/fill", response_model=FormFillResponse) +def fill_form(form: FormFill, db: Session = Depends(get_db)): + + fetched_template = get_template(db, form.template_id) + if not fetched_template: + raise AppError("Template not found", status_code=404, error_code="TEMPLATE_NOT_FOUND") + + controller = Controller() + try: + path = controller.fill_form( + user_input=form.input_text, + fields=fetched_template.fields, + pdf_form_path=fetched_template.pdf_path, + model=form.model, + ) + + # `model` is a runtime override, not a column — keep it out of the DB row. + submission = FormSubmission( + **form.model_dump(exclude={"model"}), output_pdf_path=path + ) + return create_form(db, submission) + except Exception as e: + raise AppError(str(e), status_code=500, error_code="FORM_FILL_ERROR") + + +@router.get("/models", response_model=ModelsResponse) +def list_models(): + """List the Whisper-independent extraction models available in the local + Ollama instance, plus the configured default. Used by the Fill Form UI's + model picker. Falls back to just the default if Ollama is unreachable.""" + default_model = OLLAMA_MODEL + + models: list[str] = [] + try: + response = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=10) + response.raise_for_status() + models = [m["name"] for m in response.json().get("models", []) if m.get("name")] + except requests.exceptions.RequestException: + models = [] + + # Always surface the configured default, even if Ollama hasn't pulled it yet. + if default_model not in models: + models.insert(0, default_model) + + return ModelsResponse(models=models, default=default_model) + + +@router.post("/transcribe", response_model=TranscriptionResponse) +def transcribe(audio: UploadFile = File(...)): + """Forward recorded audio to the local Whisper ASR sidecar and return text. + + Mirrors the Ollama wiring: WHISPER_HOST points at the whisper service + (http://whisper:9000 inside Docker, http://localhost:9000 otherwise). The + audio is streamed straight through to the local STT service and never + persisted — no PII leaves the machine. + """ + try: + text = call_whisper_asr( + audio.file.read(), + audio.filename or "audio.wav", + audio.content_type or "audio/wav", + ) + except ConnectionError as exc: + raise AppError(str(exc), status_code=503, error_code="STT_UNAVAILABLE") + except RuntimeError as exc: + raise AppError(str(exc), status_code=502, error_code="TRANSCRIPTION_FAILED") + + return TranscriptionResponse(text=text) + + +@router.delete("/{submission_id}", dependencies=[Depends(verify_api_key)]) +def delete_submission_endpoint(submission_id: int, db: Session = Depends(get_db)): + sub = get_form_submission(db, submission_id) + if not sub: + raise AppError("Submission not found", status_code=404, error_code="SUBMISSION_NOT_FOUND") + + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + + delete_form_submission(db, sub) + return {"status": "success", "message": "Submission and associated output file deleted"} + + +@router.post("/purge", dependencies=[Depends(verify_api_key)]) +def purge_submissions_endpoint(days: int = Query(default=None), db: Session = Depends(get_db)): + retention_days = days if days is not None else RETENTION_PERIOD_DAYS + cutoff_date = datetime.now(timezone.utc) - timedelta(days=retention_days) + + statement = select(FormSubmission).where(FormSubmission.created_at < cutoff_date) + submissions = list(db.exec(statement)) + + purged_count = 0 + for sub in submissions: + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + delete_form_submission(db, sub) + purged_count += 1 + + return {"status": "success", "purged_count": purged_count, "retention_days_used": retention_days} + + + +@router.get("/submissions") +def get_submissions(db: Session = Depends(get_db)): + from sqlmodel import select + statement = ( + select(FormSubmission, Template.name) + .join(Template, FormSubmission.template_id == Template.id, isouter=True) + .order_by(FormSubmission.created_at.desc(), FormSubmission.id.desc()) + ) + results = db.exec(statement).all() + return [ + { + "id": sub.id, + "template_id": sub.template_id, + "template_name": name or "Unknown Template", + "input_text": sub.input_text, + "output_pdf_path": sub.output_pdf_path, + "created_at": sub.created_at.isoformat() if sub.created_at else None, + } + for sub, name in results + ] + + +@router.get("/submissions/analytics") +def get_submissions_analytics(db: Session = Depends(get_db)): + import re + from collections import Counter + + from sqlmodel import select + + statement = select(FormSubmission, Template.name).join( + Template, FormSubmission.template_id == Template.id, isouter=True + ) + results = db.exec(statement).all() + + total_submissions = len(results) + + template_counts = Counter() + daily_counts = Counter() + words = [] + + stopwords = { + "the", "and", "a", "of", "to", "in", "is", "that", "it", "was", "for", "on", + "as", "with", "by", "at", "an", "be", "this", "are", "from", "or", "have", + "has", "had", "but", "not", "he", "she", "they", "we", "i", "you", "my", "his", + "her", "their", "our", "me", "him", "them", "us", "about", "there", "were", "been", "would", "could", "should", "will", "can", "no", "yes", "any", + "so", "very", "patient", "presents", "reported", "history", "shows", + "left", "right", "pain", "due", "after", "before", "emergency", "department", + "medical", "clinical" + } + + for sub, name in results: + template_name = name or "Unknown Template" + template_counts[template_name] += 1 + + if sub.created_at: + date_str = sub.created_at.strftime("%Y-%m-%d") + daily_counts[date_str] += 1 + + if sub.input_text: + found_words = re.findall(r"\b[a-zA-Z]{3,15}\b", sub.input_text.lower()) + for w in found_words: + if w not in stopwords: + words.append(w) + + sorted_daily = [{"date": k, "count": v} for k, v in sorted(daily_counts.items())] + sorted_templates = [{"template_name": k, "count": v} for k, v in template_counts.most_common()] + common_terms = [{"word": k, "count": v} for k, v in Counter(words).most_common(12)] + + return { + "total_submissions": total_submissions, + "by_template": sorted_templates, + "by_date": sorted_daily, + "common_terms": common_terms, + } + diff --git a/app/api/routes/input.py b/app/api/routes/input.py new file mode 100644 index 00000000..d0afce64 --- /dev/null +++ b/app/api/routes/input.py @@ -0,0 +1,173 @@ +from datetime import date +from pathlib import Path +from uuid import UUID + +from fastapi import APIRouter, Depends, File, Form, UploadFile +from fastapi.exceptions import RequestValidationError +from sqlmodel import Session + +from app.api.deps import get_db +from app.api.schemas.enums import InputStatus +from app.api.schemas.input import ( + InputRecordResponse, + TextInputRequest, + TextInputResponse, + VoiceInputResponse, +) +from app.core.config import ( + ALLOWED_AUDIO_EXTENSIONS, + AUDIO_CONTENT_TYPES, + ESTIMATED_TRANSCRIPTION_SECONDS, + INPUT_POLL_INTERVAL_SECONDS, +) +from app.core.errors.base import AppError +from app.db.repositories import create_input +from app.db.repositories import get_input as repo_get_input +from app.services.input import InputService +from app.services.whisper import check_whisper_available + +router = APIRouter(prefix="/input", tags=["input"]) + +_MAX_AUDIO_BYTES = 500 * 1024 * 1024 # 500 MB + + +@router.post("/voice", response_model=VoiceInputResponse, status_code=201) +def submit_voice_input( + audio_file: UploadFile = File(...), + station_id: str | None = Form(default=None), + responder_badge: str | None = Form(default=None), + incident_date_hint: date | None = Form(default=None), + db: Session = Depends(get_db), +): + # 415 — format check (HTTP concern, before any I/O) + filename = audio_file.filename or "" + ext = Path(filename).suffix.lstrip(".").lower() + if not ext or ext not in ALLOWED_AUDIO_EXTENSIONS: + raise AppError( + "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm", + status_code=415, + error_code="UNSUPPORTED_FORMAT", + detail={"accepted_formats": list(AUDIO_CONTENT_TYPES)}, + ) + + # 413 — size check: fast path via Content-Length (no read), fallback via len(bytes) + if audio_file.size is not None and audio_file.size > _MAX_AUDIO_BYTES: + raise AppError( + "Audio file exceeds maximum size of 500MB", + status_code=413, + error_code="FILE_TOO_LARGE", + detail={"max_size_bytes": _MAX_AUDIO_BYTES, "received_size_bytes": audio_file.size}, + ) + content = audio_file.file.read() + if len(content) > _MAX_AUDIO_BYTES: + raise AppError( + "Audio file exceeds maximum size of 500MB", + status_code=413, + error_code="FILE_TOO_LARGE", + detail={"max_size_bytes": _MAX_AUDIO_BYTES, "received_size_bytes": len(content)}, + ) + + # 503 — Whisper availability (HTTP concern) + if not check_whisper_available(): + raise AppError( + "Whisper transcription service is not available", + status_code=503, + error_code="STT_UNAVAILABLE", + ) + + svc = InputService() + record, job = svc.process_voice_upload( + session=db, + audio_content=content, + ext=ext, + filename=filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + ) + + return VoiceInputResponse( + input_id=record.input_id, + status=record.status, + input_type=record.input_type, + job_id=job.job_id, + poll_url=f"/api/v1/input/{record.input_id}", + estimated_processing_seconds=ESTIMATED_TRANSCRIPTION_SECONDS, + created_at=record.created_at, + ) + + +@router.post("/text", response_model=TextInputResponse, status_code=201) +def submit_text_input(body: TextInputRequest, db: Session = Depends(get_db)): + if len(body.narrative) > 50_000: + raise AppError( + "Narrative exceeds maximum length of 50,000 characters", + status_code=413, + error_code="NARRATIVE_TOO_LONG", + detail={"max_characters": 50_000, "received_characters": len(body.narrative)}, + ) + + svc = InputService() + try: + record = svc.build_text_input( + narrative=body.narrative, + station_id=body.station_id, + responder_badge=body.responder_badge, + incident_date_hint=body.incident_date_hint, + ) + except ValueError as exc: + raise RequestValidationError( + errors=[ + { + "loc": ("body", "narrative"), + "msg": str(exc), + "input": body.narrative, + "type": "value_error", + } + ] + ) + + record = create_input(db, record) + + return TextInputResponse( + input_id=record.input_id, + status=record.status, + input_type=record.input_type, + character_count=record.character_count, + word_count=record.word_count, + created_at=record.created_at, + ) + + +@router.get("/{input_id}", response_model=InputRecordResponse) +def get_input(input_id: UUID, db: Session = Depends(get_db)): + record = repo_get_input(db, input_id) + if record is None: + raise AppError( + f"Input with ID {input_id} not found", + status_code=404, + error_code="INPUT_NOT_FOUND", + ) + + retry_after = ( + INPUT_POLL_INTERVAL_SECONDS + if record.status in (InputStatus.queued, InputStatus.transcribing) + else None + ) + return InputRecordResponse( + input_id=record.input_id, + input_type=record.input_type, + status=record.status, + transcript=record.transcript, + original_filename=record.original_filename, + audio_duration_seconds=record.audio_duration_seconds, + character_count=record.character_count, + word_count=record.word_count, + station_id=record.station_id, + responder_badge=record.responder_badge, + incident_date_hint=record.incident_date_hint, + error_detail=record.error_detail, + retry_after_seconds=retry_after, + created_at=record.created_at, + updated_at=record.updated_at, + ) diff --git a/app/api/routes/jobs.py b/app/api/routes/jobs.py new file mode 100644 index 00000000..5af90087 --- /dev/null +++ b/app/api/routes/jobs.py @@ -0,0 +1,60 @@ +from fastapi import APIRouter, Depends +from sqlmodel import Session + +from app.api.deps import get_db +from app.api.schemas.forms import ( + AsyncFormFill, + AsyncFormFillResponse, + AsyncJobSubmitResponse, + JobResponse, +) +from app.core.errors.base import AppError +from app.db.repositories import create_job, get_job_by_uuid, get_template +from app.models import Job +from app.tasks.fill import fill_form_task + +router = APIRouter(tags=["jobs"]) + + +@router.get("/jobs/{job_id}", response_model=JobResponse) +def get_job_status(job_id: str, db: Session = Depends(get_db)): + job = get_job_by_uuid(db, job_id) + if not job: + raise AppError("Job not found", status_code=404) + return JobResponse( + job_id=job.job_id, + job_type=job.job_type, + status=job.status, + progress_percent=job.progress_percent, + result_url=job.result_url, + error=job.error, + created_at=job.created_at.isoformat() if job.created_at else None, + updated_at=job.updated_at.isoformat() if job.updated_at else None, + ) + + +@router.post("/forms/jobs", response_model=AsyncFormFillResponse) +def submit_async_form_fill(form: AsyncFormFill, db: Session = Depends(get_db)): + for tid in form.template_ids: + if not get_template(db, tid): + raise AppError(f"Template {tid} not found", status_code=404) + + jobs: list[AsyncJobSubmitResponse] = [] + for tid in form.template_ids: + result = fill_form_task.delay(tid, form.input_text, form.model) + job = Job( + celery_task_id=result.id, + job_type="form_generation", + template_id=tid, + input_text=form.input_text, + status="queued", + model=form.model, + ) + job = create_job(db, job) + jobs.append(AsyncJobSubmitResponse( + job_id=job.job_id, + status=job.status, + poll_url=f"/api/v1/jobs/{job.job_id}", + )) + + return AsyncFormFillResponse(jobs=jobs) diff --git a/app/api/routes/system.py b/app/api/routes/system.py new file mode 100644 index 00000000..eadd1d9f --- /dev/null +++ b/app/api/routes/system.py @@ -0,0 +1,181 @@ +"""System endpoints: GET /api/v1/health, /schema/incident, /schema/incident/versions. + +Health contract: contracts/path/system.yaml + contracts/schemas/system.yaml +""" + +import shutil +import time + +import requests +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from sqlalchemy import text + +from app.api.schemas.system import ( + ComponentHealth, + HealthComponents, + HealthStatus, + ModelInfo, +) +from app.core.config import APP_VERSION, DATA_DIR, OLLAMA_HOST, WHISPER_HOST +from app.db.database import engine + +router = APIRouter(tags=["system"]) + +_START_TIME = time.monotonic() + +# Tune-able thresholds. _SLOW_MS is patched in tests to trigger degraded +# without mocking wall-clock time. +_PROBE_TIMEOUT = 5 +_SLOW_MS = 5_000 + + +def _check_database() -> ComponentHealth: + t0 = time.monotonic() + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + elapsed = int((time.monotonic() - t0) * 1000) + return ComponentHealth(status="healthy", response_time_ms=elapsed) + except Exception as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_ollama() -> ComponentHealth: + t0 = time.monotonic() + try: + tags_resp = requests.get(f"{OLLAMA_HOST}/api/tags", timeout=_PROBE_TIMEOUT) + tags_resp.raise_for_status() + elapsed = int((time.monotonic() - t0) * 1000) + + tags_data = tags_resp.json() + + ollama_version: str | None = None + try: + ver_resp = requests.get(f"{OLLAMA_HOST}/api/version", timeout=_PROBE_TIMEOUT) + ver_resp.raise_for_status() + ollama_version = ver_resp.json().get("version") + except Exception: + pass + + running_names: set[str] = set() + model_loaded: str | None = None + try: + ps_resp = requests.get(f"{OLLAMA_HOST}/api/ps", timeout=_PROBE_TIMEOUT) + ps_resp.raise_for_status() + running = ps_resp.json().get("models", []) + running_names = {m.get("name", "") for m in running} + if running: + model_loaded = running[0].get("name") + except Exception: + pass + + raw_models = tags_data.get("models", []) + models_available: list[ModelInfo] = [] + for m in raw_models: + name = m.get("name", "") + size_gb = round(m.get("size", 0) / (1024 ** 3), 2) + quantization = m.get("details", {}).get("quantization_level") + models_available.append( + ModelInfo(name=name, size_gb=size_gb, quantization=quantization, loaded=name in running_names) + ) + + status = "degraded" if elapsed > _SLOW_MS else "healthy" + + return ComponentHealth( + status=status, + response_time_ms=elapsed, + model_loaded=model_loaded, + ollama_version=ollama_version, + models_available=models_available if models_available else None, + # current_load is always None — Ollama exposes no queue-depth API; + # populating it would require fabricating data. + current_load=None, + ) + except requests.exceptions.RequestException as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_whisper() -> ComponentHealth: + # The onerahmet/openai-whisper-asr-webservice image does not expose /health. + # Both compose files probe /docs (a FastAPI auto-endpoint always present). + t0 = time.monotonic() + try: + resp = requests.get(f"{WHISPER_HOST}/docs", timeout=_PROBE_TIMEOUT) + resp.raise_for_status() + elapsed = int((time.monotonic() - t0) * 1000) + return ComponentHealth(status="healthy", response_time_ms=elapsed) + except requests.exceptions.RequestException as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +def _check_storage() -> ComponentHealth: + try: + usage = shutil.disk_usage(DATA_DIR) + disk_free_gb = round(usage.free / (1024 ** 3), 2) + return ComponentHealth(status="healthy", disk_free_gb=disk_free_gb) + except OSError as exc: + return ComponentHealth(status="unhealthy", detail=str(exc)) + + +@router.get( + "/health", + responses={ + 200: {"model": HealthStatus, "description": "System healthy or degraded"}, + 503: {"model": HealthStatus, "description": "System unhealthy, cannot serve requests"}, + }, + summary="System health check", +) +def get_health(): + database = _check_database() + ollama = _check_ollama() + whisper = _check_whisper() + storage = _check_storage() + + components = HealthComponents( + database=database, ollama=ollama, whisper=whisper, storage=storage + ) + + statuses = {database.status, ollama.status, whisper.status, storage.status} + + if database.status == "unhealthy": + overall = "unhealthy" + elif "unhealthy" in statuses or "degraded" in statuses: + overall = "degraded" + else: + overall = "healthy" + + body = HealthStatus( + status=overall, + version=APP_VERSION, + uptime_seconds=int(time.monotonic() - _START_TIME), + components=components, + ) + + http_status = 503 if overall == "unhealthy" else 200 + return JSONResponse( + content=body.model_dump(exclude_none=True), + status_code=http_status, + ) + + +@router.get("/schema/incident", summary="Get canonical incident JSON Schema") +def get_schema_incident(): + return JSONResponse( + status_code=501, + content={ + "error_code": "NOT_IMPLEMENTED", + "message": "Incident schema not yet finalised — see issue #555", + }, + ) + + +@router.get("/schema/incident/versions", summary="Get incident schema version history") +def get_schema_versions(): + return JSONResponse( + status_code=501, + content={ + "error_code": "NOT_IMPLEMENTED", + "message": "Schema version history not yet available — see issue #555", + }, + ) diff --git a/api/routes/templates.py b/app/api/routes/templates.py similarity index 78% rename from api/routes/templates.py rename to app/api/routes/templates.py index 64db2e05..d5cd3c2b 100644 --- a/api/routes/templates.py +++ b/app/api/routes/templates.py @@ -4,22 +4,28 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from fastapi.responses import FileResponse -from sqlmodel import Session -from api.deps import get_db -from api.schemas.templates import ( +from sqlmodel import Session, select + +from app.api.deps import get_db, verify_api_key +from app.api.schemas.templates import ( + MakeFillableRequest, + MakeFillableResponse, TemplateCreate, TemplateResponse, TemplateUploadResponse, - MakeFillableRequest, - MakeFillableResponse, ) -from api.db.repositories import create_template, list_templates -from api.db.models import Template -from src.controller import Controller +from app.core.config import BASE_DIR, DEFAULT_TEMPLATE_DIR +from app.db.repositories import ( + create_template, + delete_template, + get_template, + list_templates, +) +from app.models import FormSubmission, Job, Template +from app.services.controller import Controller router = APIRouter(prefix="/templates", tags=["templates"]) -PROJECT_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_TEMPLATE_DIR = "src/inputs" +PROJECT_ROOT = BASE_DIR def _resolve_target_directory(directory: str) -> Path: @@ -196,3 +202,43 @@ def make_fillable(req: MakeFillableRequest): pdf_path=relative_path, field_count=_count_pdf_widgets(relative_path), ) + + +@router.delete("/{template_id}", dependencies=[Depends(verify_api_key)]) +def delete_template_endpoint(template_id: int, db: Session = Depends(get_db)): + template = get_template(db, template_id) + if not template: + raise HTTPException(status_code=404, detail="Template not found") + + # 1. Clean up associated submissions and their generated PDFs + sub_stmt = select(FormSubmission).where(FormSubmission.template_id == template_id) + submissions = list(db.exec(sub_stmt)) + for sub in submissions: + if sub.output_pdf_path: + try: + resolved_out = _resolve_project_file(sub.output_pdf_path) + if resolved_out.exists() and resolved_out.is_file(): + resolved_out.unlink() + except Exception: + pass + db.delete(sub) + + # 2. Clean up associated jobs + job_stmt = select(Job).where(Job.template_id == template_id) + jobs = list(db.exec(job_stmt)) + for job in jobs: + db.delete(job) + + # 3. Delete template PDF file + if template.pdf_path: + try: + resolved_pdf = _resolve_project_file(template.pdf_path) + if resolved_pdf.exists() and resolved_pdf.is_file(): + resolved_pdf.unlink() + except Exception: + pass + + # 4. Delete the template itself + delete_template(db, template) + return {"status": "success", "message": "Template and all associated data deleted"} + diff --git a/app/api/routes/weather.py b/app/api/routes/weather.py new file mode 100644 index 00000000..5426dcdb --- /dev/null +++ b/app/api/routes/weather.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter + +from app.core.errors.base import AppError +from app.services.controller import Controller + +router = APIRouter(prefix="/weather", tags=["weather"]) + +ALL_HOURLY_FIELDS = [ + "temperature_2m", + "relative_humidity_2m", + "rain", + "apparent_temperature", + "precipitation_probability", + "precipitation", + "wind_direction_10m", + "wind_speed_10m", + "soil_temperature_0cm", + "uv_index", +] + + +@router.get("/forecast") +def get_weather_forecast( + latitude: float, + longitude: float, + start_date: str, + end_date: str, + fields: str | None = None, +): + """ + Fetch hourly weather forecast data for the given coordinates and date range. + + Coordinates must be supplied as **decimal degrees** — not degree/minute/second notation. E.g. -122.4194, 37.7749. + Dates must use the **YYYY-MM-DD** format (ISO 8601). + """ + controller = Controller() + try: + requested = [f.strip() for f in fields.split(",") if f.strip()] if fields else [] + weather_data = controller.get_weather(latitude, longitude, start_date, end_date, hourly_fields=requested) + return weather_data + except Exception as e: + raise AppError(str(e), status_code=500) \ No newline at end of file diff --git a/app/api/routes/zipcode.py b/app/api/routes/zipcode.py new file mode 100644 index 00000000..c54c2873 --- /dev/null +++ b/app/api/routes/zipcode.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter + +from app.core.errors.base import AppError +from app.services.controller import Controller + +router = APIRouter(prefix="/zipcode", tags=["zipcode"]) + + +@router.get("/lookup-address") +def lookup_address(address: str): + """ + Resolve a free-form address to one or more geographic locations including + postal code, latitude, longitude, place name, state, county, and country. + + Returns a list of matching results ordered by relevance (most relevant first). + Each result is guaranteed to include `latitude` and `longitude`; `postal_code` + is present when the geocoding service can determine it for the given address. + + Example: + address = "1600 Amphitheatre Parkway, Mountain View, CA, US" + """ + controller = Controller() + try: + return controller.lookup_address(address) + except TimeoutError as e: + raise AppError(str(e), status_code=504) + except Exception as e: + raise AppError(str(e), status_code=500) diff --git a/api/db/__init__.py b/app/api/schemas/__init__.py similarity index 100% rename from api/db/__init__.py rename to app/api/schemas/__init__.py diff --git a/app/api/schemas/common.py b/app/api/schemas/common.py new file mode 100644 index 00000000..8d29d318 --- /dev/null +++ b/app/api/schemas/common.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from app.api.schemas.enums import JobStatus, JobType + + +class ValidationErrorItem(BaseModel): + model_config = ConfigDict() + field: str | None = None + issue: str | None = None + value: Any = None + + +class ErrorResponse(BaseModel): + model_config = ConfigDict() + error_code: str + message: str + detail: dict[str, Any] | None = None + retry_after_seconds: int | None = None + validation_errors: list[ValidationErrorItem] | None = None + + +class Pagination(BaseModel): + model_config = ConfigDict() + total: int + page: int + per_page: int + total_pages: int + has_next: bool + has_prev: bool + + +class AsyncJobResponse(BaseModel): + model_config = ConfigDict() + job_id: str + job_type: JobType | None = None + status: JobStatus + estimated_seconds: int | None = None + poll_url: str | None = None + + +class Job(BaseModel): + model_config = ConfigDict() + job_id: str + job_type: JobType + status: JobStatus + progress_percent: int | None = None + result_url: str | None = None + error: ErrorResponse | None = None + created_at: datetime | None = None + updated_at: datetime | None = None diff --git a/app/api/schemas/enums.py b/app/api/schemas/enums.py new file mode 100644 index 00000000..b6335389 --- /dev/null +++ b/app/api/schemas/enums.py @@ -0,0 +1,116 @@ +from enum import Enum + + +class InputType(str, Enum): + voice = "voice" + text = "text" + + +class InputStatus(str, Enum): + queued = "queued" + transcribing = "transcribing" + ready = "ready" + failed = "failed" + + +class ExtractionStatus(str, Enum): + processing = "processing" + completed = "completed" + failed = "failed" + needs_review = "needs_review" + + +class FormStatus(str, Enum): + queued = "queued" + generating = "generating" + completed = "completed" + failed = "failed" + + +class ReportStatus(str, Enum): + draft = "draft" + under_review = "under_review" + approved = "approved" + submitted = "submitted" + + +class JobStatus(str, Enum): + queued = "queued" + processing = "processing" + completed = "completed" + failed = "failed" + + +class JobType(str, Enum): + transcription = "transcription" + extraction = "extraction" + form_generation = "form_generation" + batch_form_generation = "batch_form_generation" + report_generation = "report_generation" + + +class FormType(str, Enum): + neris = "neris" + nemsis_epcr = "nemsis_epcr" + nibrs = "nibrs" + nfirs_basic = "nfirs_basic" + nfirs_fire = "nfirs_fire" + nfirs_structure = "nfirs_structure" + nfirs_wildland = "nfirs_wildland" + nfirs_ems = "nfirs_ems" + nfirs_hazmat = "nfirs_hazmat" + nfirs_apparatus = "nfirs_apparatus" + nfirs_personnel = "nfirs_personnel" + nfirs_arson = "nfirs_arson" + nfirs_casualty_civilian = "nfirs_casualty_civilian" + nfirs_casualty_responder = "nfirs_casualty_responder" + cal_fire_ics209 = "cal_fire_ics209" + osha_301 = "osha_301" + un_ssirs = "un_ssirs" + state_georgia = "state_georgia" + state_california = "state_california" + state_new_york = "state_new_york" + + +class IncidentCategory(str, Enum): + fire = "fire" + ems = "ems" + rescue = "rescue" + hazardous_conditions = "hazardous_conditions" + service_call = "service_call" + good_intent = "good_intent" + false_alarm = "false_alarm" + law_enforcement = "law_enforcement" + + +class CauseCertainty(str, Enum): + confirmed = "confirmed" + probable = "probable" + suspected = "suspected" + undetermined = "undetermined" + + +class InjurySeverity(str, Enum): + minor = "minor" + moderate = "moderate" + severe = "severe" + fatal = "fatal" + + +class RateOfSpread(str, Enum): + slow = "slow" + moderate = "moderate" + rapid = "rapid" + extreme = "extreme" + + +class PeriodType(str, Enum): + monthly = "monthly" + quarterly = "quarterly" + annual = "annual" + + +class OutputFormat(str, Enum): + pdf = "pdf" + json = "json" + both = "both" diff --git a/app/api/schemas/forms.py b/app/api/schemas/forms.py new file mode 100644 index 00000000..155b14f0 --- /dev/null +++ b/app/api/schemas/forms.py @@ -0,0 +1,77 @@ +from pydantic import BaseModel, field_validator + + +class FormFill(BaseModel): + template_id: int + input_text: str + model: str | None = None + + @field_validator("input_text") + def validate_input_text(cls, value): + if not value or not value.strip(): + raise ValueError("Input text cannot be empty") + return value + + +class FormFillResponse(BaseModel): + id: int + template_id: int + input_text: str + output_pdf_path: str + + class Config: + from_attributes = True + + +class TranscriptionResponse(BaseModel): + text: str + + +class ModelsResponse(BaseModel): + models: list[str] + default: str + + +class AsyncFormFill(BaseModel): + template_ids: list[int] + input_text: str + model: str | None = None + + @field_validator("input_text") + def validate_input_text(cls, value): + if not value or not value.strip(): + raise ValueError("Input text cannot be empty") + return value + + @field_validator("template_ids") + def validate_template_ids(cls, value): + if not value: + raise ValueError("template_ids cannot be empty") + return value + + +class JobResponse(BaseModel): + job_id: str + job_type: str + status: str + progress_percent: int = 0 + result_url: str | None = None + error: dict | None = None + created_at: str | None = None + updated_at: str | None = None + + class Config: + from_attributes = True + + +class AsyncJobSubmitResponse(BaseModel): + job_id: str + status: str + poll_url: str + + class Config: + from_attributes = True + + +class AsyncFormFillResponse(BaseModel): + jobs: list[AsyncJobSubmitResponse] \ No newline at end of file diff --git a/app/api/schemas/input.py b/app/api/schemas/input.py new file mode 100644 index 00000000..9d8b908c --- /dev/null +++ b/app/api/schemas/input.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from datetime import date, datetime +from uuid import UUID + +from pydantic import BaseModel, Field + +from app.api.schemas.enums import InputStatus, InputType + + +class VoiceInputResponse(BaseModel): + input_id: UUID + status: InputStatus + input_type: InputType + estimated_processing_seconds: int | None = None + created_at: datetime | None = None + job_id: str | None = None + poll_url: str | None = None + + +class TextInputRequest(BaseModel): + narrative: str = Field(min_length=20) + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + + +class TextInputResponse(BaseModel): + input_id: UUID + status: InputStatus + input_type: InputType + character_count: int | None = None + word_count: int | None = None + created_at: datetime | None = None + + +class InputRecordResponse(BaseModel): + input_id: UUID + input_type: InputType + status: InputStatus + transcript: str | None = None + original_filename: str | None = None + audio_duration_seconds: float | None = None + character_count: int | None = None + word_count: int | None = None + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + error_detail: str | None = None + retry_after_seconds: int | None = None + created_at: datetime | None = None + updated_at: datetime | None = None diff --git a/app/api/schemas/system.py b/app/api/schemas/system.py new file mode 100644 index 00000000..bafa5cfc --- /dev/null +++ b/app/api/schemas/system.py @@ -0,0 +1,45 @@ +from pydantic import BaseModel + + +class ModelInfo(BaseModel): + name: str + size_gb: float + quantization: str | None = None + loaded: bool + + +class CurrentLoad(BaseModel): + active_requests: int + queued_requests: int + + +class ComponentHealth(BaseModel): + status: str + response_time_ms: int | None = None + detail: str | None = None + model_loaded: str | None = None + ollama_version: str | None = None + models_available: list[ModelInfo] | None = None + current_load: CurrentLoad | None = None + disk_free_gb: float | None = None + + +class HealthComponents(BaseModel): + database: ComponentHealth + ollama: ComponentHealth + whisper: ComponentHealth + storage: ComponentHealth + + +class HealthStatus(BaseModel): + status: str + version: str + uptime_seconds: int | None = None + components: HealthComponents | None = None + + +class SchemaVersion(BaseModel): + version: str + released_at: str + changelog: str | None = None + breaking_changes: bool | None = None diff --git a/api/schemas/templates.py b/app/api/schemas/templates.py similarity index 99% rename from api/schemas/templates.py rename to app/api/schemas/templates.py index df39832b..351b1870 100644 --- a/api/schemas/templates.py +++ b/app/api/schemas/templates.py @@ -1,5 +1,6 @@ from pydantic import BaseModel + class TemplateCreate(BaseModel): name: str pdf_path: str diff --git a/api/errors/__init__.py b/app/core/__init__.py similarity index 100% rename from api/errors/__init__.py rename to app/core/__init__.py diff --git a/app/core/celery.py b/app/core/celery.py new file mode 100644 index 00000000..e42504b8 --- /dev/null +++ b/app/core/celery.py @@ -0,0 +1,30 @@ +from celery import Celery + +from app.core.config import CELERY_BROKER_URL, CELERY_RESULT_BACKEND + +celery_app = Celery( + "fireform", + broker=CELERY_BROKER_URL, + backend=CELERY_RESULT_BACKEND, +) + +celery_app.conf.update( + task_serializer="json", + result_serializer="json", + accept_content=["json"], + task_track_started=True, + result_expires=86400, +) + +celery_app.conf.include = ["app.tasks.fill", "app.tasks.purge", "app.tasks.transcribe"] + +# Optional Celery Beat schedule — runs purge_old_submissions once a day. +# Enable by running: celery -A app.core.celery beat +from celery.schedules import crontab + +celery_app.conf.beat_schedule = { + "daily-submission-purge": { + "task": "purge_old_submissions", + "schedule": crontab(hour=3, minute=0), # 03:00 UTC daily + }, +} diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 00000000..4a7ca897 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,87 @@ +"""Central configuration. + +Single source of truth for paths, the database URL, external service hosts and +CORS. Read environment once here so the rest of the app imports settings instead +of calling os.getenv() in scattered places. +""" + +import os +from pathlib import Path + +# Repo root. config.py lives at app/core/config.py -> parents[2] is the repo root. +BASE_DIR = Path(__file__).resolve().parents[2] + +# --- App metadata --------------------------------------------------------- +APP_TITLE = "FireForm API" +APP_VERSION = "1.1.0" + +# --- Runtime data paths --------------------------------------------------- +# Uploaded templates and generated PDFs. Project-relative paths the API echoes +# back to the client are resolved against BASE_DIR (the "inside the project" +# guard in the templates routes). Override the data dir with FIREFORM_DATA_DIR. +DATA_DIR = Path(os.getenv("FIREFORM_DATA_DIR", BASE_DIR / "data")).resolve() + +# Directory new uploads land in, as a project-relative string (was "src/inputs" +# before the restructure). Override with FIREFORM_TEMPLATE_DIR. +DEFAULT_TEMPLATE_DIR = os.getenv("FIREFORM_TEMPLATE_DIR", "data/inputs") + +# --- Database ------------------------------------------------------------- +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql://fireform:fireform@localhost:5432/fireform", +) +DB_ECHO = os.getenv("FIREFORM_DB_ECHO", "true").lower() == "true" + +# --- External services ---------------------------------------------------- +OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") +OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") +WHISPER_HOST = os.getenv("WHISPER_HOST", "http://localhost:9000").rstrip("/") + +# --- Celery / Redis ------------------------------------------------------- +CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/0") +CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/0") + +# --- CORS ----------------------------------------------------------------- +_DEFAULT_ORIGINS = "http://127.0.0.1:5173,http://localhost:5173" +ALLOWED_ORIGINS = [ + origin.strip() + for origin in os.getenv("FRONTEND_ORIGINS", _DEFAULT_ORIGINS).split(",") + if origin.strip() +] + +# --- Error handling ------------------------------------------------------- +# Advisory Retry-After sent to clients on 503 responses. This is a client +# hint, not a measured backpressure value — the app has no queue-depth signal. +RETRY_AFTER_SECONDS = 30 + +# Polling hint returned by GET /input/{id} when a voice input is still queued +# or transcribing. Value matches the contract example (contracts/path/input.yaml). +INPUT_POLL_INTERVAL_SECONDS = 5 + +# --- API Versioning ------------------------------------------------------- +API_PREFIX = "/api/v1" + +# --- Security & Access Control --------------------------------------------- +FIREFORM_API_KEY = os.getenv("FIREFORM_API_KEY", "") + +# --- Data Retention -------------------------------------------------------- +RETENTION_PERIOD_DAYS = int(os.getenv("RETENTION_PERIOD_DAYS", "30")) + +# --- Audio storage -------------------------------------------------------- +# Voice input audio files land here: {AUDIO_DIR}/{input_id}.{ext} +AUDIO_DIR = DATA_DIR / "audio" + +# Advisory estimate returned in VoiceInputResponse.estimated_processing_seconds. +ESTIMATED_TRANSCRIPTION_SECONDS = int(os.getenv("ESTIMATED_TRANSCRIPTION_SECONDS", "30")) + +# Canonical audio format mapping — single source of truth for both the route +# (membership check, 415 detail list) and the task (content-type lookup). +# Dict insertion order gives the stable list shown in error responses. +AUDIO_CONTENT_TYPES: dict[str, str] = { + "wav": "audio/wav", + "mp3": "audio/mpeg", + "m4a": "audio/m4a", + "ogg": "audio/ogg", + "webm": "audio/webm", +} +ALLOWED_AUDIO_EXTENSIONS: frozenset[str] = frozenset(AUDIO_CONTENT_TYPES) \ No newline at end of file diff --git a/api/schemas/__init__.py b/app/core/errors/__init__.py similarity index 100% rename from api/schemas/__init__.py rename to app/core/errors/__init__.py diff --git a/app/core/errors/base.py b/app/core/errors/base.py new file mode 100644 index 00000000..d317736a --- /dev/null +++ b/app/core/errors/base.py @@ -0,0 +1,23 @@ +def _default_error_code(status_code: int) -> str: + return { + 400: "BAD_REQUEST", + 404: "NOT_FOUND", + 422: "VALIDATION_ERROR", + 500: "INTERNAL_ERROR", + 502: "BAD_GATEWAY", + 503: "SERVICE_UNAVAILABLE", + }.get(status_code, "ERROR") + + +class AppError(Exception): + def __init__( + self, + message: str, + status_code: int = 400, + error_code: str | None = None, + detail: dict | None = None, + ): + self.message = message + self.status_code = status_code + self.error_code = error_code or _default_error_code(status_code) + self.detail = detail diff --git a/app/core/errors/handlers.py b/app/core/errors/handlers.py new file mode 100644 index 00000000..ce8988ad --- /dev/null +++ b/app/core/errors/handlers.py @@ -0,0 +1,37 @@ +from fastapi import Request +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +from app.core.config import RETRY_AFTER_SECONDS +from app.core.errors.base import AppError + + +def register_exception_handlers(app): + @app.exception_handler(AppError) + async def app_error_handler(request: Request, exc: AppError): + body: dict = {"error_code": exc.error_code, "message": exc.message} + if exc.detail is not None: + body["detail"] = exc.detail + if exc.status_code == 503: + body["retry_after_seconds"] = RETRY_AFTER_SECONDS + return JSONResponse(status_code=exc.status_code, content=body) + + @app.exception_handler(RequestValidationError) + async def validation_error_handler(request: Request, exc: RequestValidationError): + validation_errors = [] + for error in exc.errors(): + loc = error.get("loc", ()) + field = ".".join(str(x) for x in loc if x != "body") + validation_errors.append({ + "field": field or None, + "issue": error.get("msg"), + "value": error.get("input"), + }) + return JSONResponse( + status_code=422, + content={ + "error_code": "VALIDATION_ERROR", + "message": "Request validation failed", + "validation_errors": validation_errors, + }, + ) diff --git a/app/core/lifespan.py b/app/core/lifespan.py new file mode 100644 index 00000000..7f842739 --- /dev/null +++ b/app/core/lifespan.py @@ -0,0 +1,17 @@ +"""Application lifespan: startup and shutdown hooks.""" + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from app.db.init_db import init_db + +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("Initializing database...") + init_db() + yield diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 00000000..48d28a8a --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,14 @@ +"""Logging setup. Call setup_logging() once at app startup.""" + +import logging + + +def setup_logging(level: str = "INFO") -> None: + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)-8s %(name)s: %(message)s", + ) + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) diff --git a/src/__init__.py b/app/db/__init__.py similarity index 100% rename from src/__init__.py rename to app/db/__init__.py diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 00000000..08f95293 --- /dev/null +++ b/app/db/database.py @@ -0,0 +1,10 @@ +from sqlmodel import Session, create_engine + +from app.core.config import DATABASE_URL, DB_ECHO + +engine = create_engine(DATABASE_URL, echo=DB_ECHO) + + +def get_session(): + with Session(engine) as session: + yield session diff --git a/app/db/init_db.py b/app/db/init_db.py new file mode 100644 index 00000000..54ba084b --- /dev/null +++ b/app/db/init_db.py @@ -0,0 +1,67 @@ +import datetime +import logging +from pathlib import Path + +from alembic.config import Config +from sqlmodel import Session, select + +from alembic import command +from app.core.config import DEFAULT_TEMPLATE_DIR +from app.db.database import engine +from app.models import FormSubmission, Template # noqa: F401 + +logger = logging.getLogger(__name__) + +ALEMBIC_INI = str(Path(__file__).resolve().parents[2] / "alembic.ini") + + +def _alembic_cfg() -> Config: + cfg = Config(ALEMBIC_INI) + return cfg + + +def run_migrations(): + logger.info("Running Alembic migrations...") + command.upgrade(_alembic_cfg(), "head") + logger.info("Migrations complete.") + + +def seed_db(): + with Session(engine) as session: + statement = select(Template) + try: + results = session.exec(statement).first() + except Exception: + results = None + + if not results: + logger.info("Seeding database with default template...") + fields = { + "Employee's name": "string", + "Employee's job title": "string", + "Employee's department supervisor": "string", + "Employee's phone number": "string", + "Employee's email": "string", + "Signature": "string", + "Date": "string", + } + + default_template = Template( + id=2, + name="Manual Test Template", + fields=fields, + pdf_path=f"{DEFAULT_TEMPLATE_DIR}/file_template_manual.pdf", + created_at=datetime.datetime.now(), + ) + session.add(default_template) + session.commit() + logger.info("Database seeded successfully.") + + +def init_db(): + run_migrations() + seed_db() + + +if __name__ == "__main__": + init_db() diff --git a/app/db/repositories.py b/app/db/repositories.py new file mode 100644 index 00000000..d53b197e --- /dev/null +++ b/app/db/repositories.py @@ -0,0 +1,91 @@ +from uuid import UUID + +from sqlmodel import Session, select + +from app.models import FormSubmission, Input, Job, Template + + +# Templates +def create_template(session: Session, template: Template) -> Template: + session.add(template) + session.commit() + session.refresh(template) + return template + +def get_template(session: Session, template_id: int) -> Template | None: + return session.get(Template, template_id) + + +def list_templates(session: Session) -> list[Template]: + statement = select(Template).order_by(Template.created_at.desc(), Template.id.desc()) + return list(session.exec(statement)) + +# Forms +def create_form(session: Session, form: FormSubmission) -> FormSubmission: + session.add(form) + session.commit() + session.refresh(form) + return form + + +# Jobs +def create_job(session: Session, job: Job) -> Job: + session.add(job) + session.commit() + session.refresh(job) + return job + + +def get_job(session: Session, job_id: int) -> Job | None: + return session.get(Job, job_id) + + +def get_job_by_uuid(session: Session, job_uuid: str) -> Job | None: + statement = select(Job).where(Job.job_id == job_uuid) + return session.exec(statement).first() + + +def get_job_by_celery_id(session: Session, celery_task_id: str) -> Job | None: + statement = select(Job).where(Job.celery_task_id == celery_task_id) + return session.exec(statement).first() + + +def update_job(session: Session, job: Job) -> Job: + session.add(job) + session.commit() + session.refresh(job) + return job + + +def delete_template(session: Session, template: Template) -> None: + session.delete(template) + session.commit() + + +def get_form_submission(session: Session, submission_id: int) -> FormSubmission | None: + return session.get(FormSubmission, submission_id) + + +def delete_form_submission(session: Session, submission: FormSubmission) -> None: + session.delete(submission) + session.commit() + + +# Inputs +def create_input(session: Session, input_obj: Input) -> Input: + session.add(input_obj) + session.commit() + session.refresh(input_obj) + return input_obj + + +def get_input(session: Session, input_id: UUID) -> Input | None: + return session.get(Input, input_id) + + +def update_input(session: Session, input_obj: Input) -> Input: + session.add(input_obj) + session.commit() + session.refresh(input_obj) + return input_obj + diff --git a/app/main.py b/app/main.py new file mode 100644 index 00000000..3d3bdad8 --- /dev/null +++ b/app/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.router import api_router +from app.core import config +from app.core.errors.handlers import register_exception_handlers +from app.core.lifespan import lifespan +from app.core.logging import setup_logging + + +def create_app() -> FastAPI: + setup_logging() + + app = FastAPI( + title=config.APP_TITLE, + version=config.APP_VERSION, + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=config.ALLOWED_ORIGINS, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + + register_exception_handlers(app) + app.include_router(api_router) + + return app + + +app = create_app() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000..9e736ec7 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,23 @@ +"""ORM models. Import from here: `from app.models import Template`.""" + +from app.models.models import ( + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) + +__all__ = [ + "Extraction", + "Form", + "FormSubmission", + "Incident", + "Input", + "Job", + "Report", + "Template", +] diff --git a/app/models/models.py b/app/models/models.py new file mode 100644 index 00000000..8358b08d --- /dev/null +++ b/app/models/models.py @@ -0,0 +1,167 @@ +import uuid as uuid_mod +from datetime import date, datetime, timezone +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel +from sqlmodel.sql.sqltypes import AutoString + +from app.api.schemas.enums import ( + ExtractionStatus, + FormStatus, + FormType, + InputStatus, + InputType, + JobStatus, + OutputFormat, + PeriodType, + ReportStatus, +) + + +class Template(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + fields: dict = Field(sa_column=Column(JSON, nullable=False)) + pdf_path: str + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class FormSubmission(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + template_id: int = Field(foreign_key="template.id") + input_text: str + output_pdf_path: str + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Job(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + job_id: str = Field(default_factory=lambda: str(uuid_mod.uuid4()), index=True, unique=True) + celery_task_id: str = Field(index=True) + job_type: str = Field(default="form_generation") + template_id: int | None = Field(default=None, foreign_key="template.id") + input_text: str | None = None + status: str = Field(default="queued") + progress_percent: int = Field(default=0) + result_url: str | None = None + error: dict | None = Field(default=None, sa_column=Column(JSON)) + model: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +# --------------------------------------------------------------------------- +# v1 contract models +# --------------------------------------------------------------------------- + +class Input(SQLModel, table=True): + __tablename__ = "inputs" + + input_id: UUID = Field(default_factory=uuid4, primary_key=True) + # sa_column required on all str-Enum fields: without it SQLModel emits + # sa.Enum(native_enum=True) which creates a Postgres ENUM type — hard to + # migrate and inconsistent with the VARCHAR approach used in migration 001. + input_type: InputType = Field(sa_column=Column(AutoString, nullable=False)) + status: InputStatus = Field( + default=InputStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + transcript: str | None = None + original_filename: str | None = None + audio_duration_seconds: float | None = None + character_count: int | None = None + word_count: int | None = None + station_id: str | None = None + responder_badge: str | None = None + incident_date_hint: date | None = None + error_detail: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Extraction(SQLModel, table=True): + __tablename__ = "extractions" + + extract_id: UUID = Field(default_factory=uuid4, primary_key=True) + input_id: UUID = Field(foreign_key="inputs.input_id") + status: ExtractionStatus = Field( + default=ExtractionStatus.processing, sa_column=Column(AutoString, nullable=False) + ) + started_at: datetime | None = None + completed_at: datetime | None = None + model_used: str | None = None + processing_time_seconds: float | None = None + # Full IncidentContract superset blob; stores partial result while processing, + # final canonical JSON when status=completed. + incident_contract: dict | None = Field(default=None, sa_column=Column(JSON)) + # Audit trail of manual corrections applied via PATCH /extract/{id}. + corrections: list | None = Field(default=None, sa_column=Column(JSON)) + error_type: str | None = None + error_detail: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Incident(SQLModel, table=True): + __tablename__ = "incidents" + + incident_id: UUID = Field(default_factory=uuid4, primary_key=True) + extract_id: UUID = Field(foreign_key="extractions.extract_id") + incident_number: str | None = None + status: ReportStatus = Field( + default=ReportStatus.draft, sa_column=Column(AutoString, nullable=False) + ) + incident_name: str | None = None + incident_type: str | None = None + incident_date: date | None = None + tags: list | None = Field(default=None, sa_column=Column(JSON)) + notes: str | None = None + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + deleted_at: datetime | None = None + + +class Form(SQLModel, table=True): + __tablename__ = "forms" + + form_id: UUID = Field(default_factory=uuid4, primary_key=True) + form_type: FormType = Field(sa_column=Column(AutoString, nullable=False)) + status: FormStatus = Field( + default=FormStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + extract_id: UUID = Field(foreign_key="extractions.extract_id") + incident_id: UUID | None = Field(default=None, foreign_key="incidents.incident_id") + # Plain UUID, no FK constraint — pending contract Job model resolution (#544 decision A). + job_id: UUID | None = None + completed_at: datetime | None = None + pdf_ready: bool = Field(default=False) + json_ready: bool = Field(default=False) + field_mapping_summary: dict | None = Field(default=None, sa_column=Column(JSON)) + pdf_path: str | None = None + json_data: dict | None = Field(default=None, sa_column=Column(JSON)) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + + +class Report(SQLModel, table=True): + __tablename__ = "reports" + + report_id: UUID = Field(default_factory=uuid4, primary_key=True) + period_type: PeriodType = Field(sa_column=Column(AutoString, nullable=False)) + period_label: str | None = None + year: int + month: int | None = None + quarter: int | None = None + # Named output_format (not format) to avoid shadowing the Python builtin. + output_format: OutputFormat = Field(sa_column=Column(AutoString, nullable=False)) + status: JobStatus = Field( + default=JobStatus.queued, sa_column=Column(AutoString, nullable=False) + ) + generated_at: datetime | None = None + summary: dict | None = Field(default=None, sa_column=Column(JSON)) + # Plain UUID, no FK constraint — pending contract Job model resolution (#544 decision A). + job_id: UUID | None = None + pdf_path: str | None = None + json_data: dict | None = Field(default=None, sa_column=Column(JSON)) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/services/controller.py b/app/services/controller.py new file mode 100644 index 00000000..2a9efcbb --- /dev/null +++ b/app/services/controller.py @@ -0,0 +1,20 @@ +from app.services.external_apis_coordinator import ExternalAPIsCoordinator +from app.services.file_manipulator import FileManipulator + + +class Controller: + def __init__(self): + self.file_manipulator = FileManipulator() + self.external_apis_coordinator = ExternalAPIsCoordinator() + + def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): + return self.file_manipulator.fill_form(user_input, fields, pdf_form_path, model=model) + + def prepare_fillable(self, pdf_path: str): + return self.file_manipulator.prepare_fillable(pdf_path) + + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list = None): + return self.external_apis_coordinator.get_weather(latitude, longitude, start_date, end_date, hourly_fields) + + def lookup_address(self, address: str) -> list[dict]: + return self.external_apis_coordinator.lookup_address(address) \ No newline at end of file diff --git a/app/services/external_apis/weather_api.py b/app/services/external_apis/weather_api.py new file mode 100644 index 00000000..8f0e0ab2 --- /dev/null +++ b/app/services/external_apis/weather_api.py @@ -0,0 +1,83 @@ +import openmeteo_requests +import pandas as pd +import requests_cache +from retry_requests import retry + +# All supported hourly fields exposed by this service +ALL_HOURLY_FIELDS = [ + "temperature_2m", + "relative_humidity_2m", + "rain", + "apparent_temperature", + "precipitation_probability", + "precipitation", + "wind_direction_10m", + "wind_speed_10m", + "soil_temperature_0cm", + "uv_index", +] + +class WeatherAPI: + def __init__(self): + pass + + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): + """ + Get weather data for a given latitude and longitude. + + Args: + latitude (float): The latitude of the location. + longitude (float): The longitude of the location. + hourly_fields (list[str] | None): Subset of hourly variables to request. + Defaults to ALL_HOURLY_FIELDS when None or empty. + + Returns: + dict: A dictionary containing the weather data. + """ + requested = [f for f in (hourly_fields or []) if f in ALL_HOURLY_FIELDS] + if not requested: + requested = list(ALL_HOURLY_FIELDS) + + # Setup the Open-Meteo API client with cache and retry on error + cache_session = requests_cache.CachedSession('.cache', expire_after=3600) + retry_session = retry(cache_session, retries=5, backoff_factor=0.2) + openmeteo = openmeteo_requests.Client(session=retry_session) + + url = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": latitude, + "longitude": longitude, + "hourly": requested, + "start_date": start_date, + "end_date": end_date, + } + responses = openmeteo.weather_api(url, params=params) + + response = responses[0] + + hourly = response.Hourly() + + hourly_data: dict = { + "date": pd.date_range( + start=pd.to_datetime(hourly.Time(), unit="s", utc=True), + end=pd.to_datetime(hourly.TimeEnd(), unit="s", utc=True), + freq=pd.Timedelta(seconds=hourly.Interval()), + inclusive="left", + ) + } + + for i, field in enumerate(requested): + hourly_data[field] = hourly.Variables(i).ValuesAsNumpy().tolist() + + # Convert pandas Timestamp objects to ISO strings for JSON serialization + hourly_data["date"] = [d.isoformat() for d in hourly_data["date"]] + + return { + "latitude": response.Latitude(), + "longitude": response.Longitude(), + "elevation": response.Elevation(), + "timezone": response.Timezone(), + "timezone_abbreviation": response.TimezoneAbbreviation(), + "utc_offset_seconds": response.UtcOffsetSeconds(), + "hourly": hourly_data, + } \ No newline at end of file diff --git a/app/services/external_apis/zipcode_api.py b/app/services/external_apis/zipcode_api.py new file mode 100644 index 00000000..094b6d5d --- /dev/null +++ b/app/services/external_apis/zipcode_api.py @@ -0,0 +1,72 @@ +from geopy.exc import GeocoderServiceError, GeocoderTimedOut +from geopy.geocoders import Nominatim + +from app.core.logging import get_logger + +logger = get_logger(__name__) + +_GEOLOCATOR = Nominatim(user_agent="fireform", timeout=10) + + +class ZipCodeAPI: + def __init__(self): + pass + + def lookup_address(self, address: str) -> list[dict]: + """ + Look up an address string via OpenStreetMap Nominatim and return a list + of matching locations with their postal codes, coordinates, and names. + + Args: + address: Full address string, e.g. "1600 Amphitheatre Parkway, Mountain View, CA, US" + + Returns: + A list of dicts, each containing: + - postal_code (str | None) + - place_name (str) + - latitude (float) + - longitude (float) + - display_name (str) + - country (str | None) + - state (str | None) + - county (str | None) + """ + try: + locations = _GEOLOCATOR.geocode( + address, + exactly_one=False, + addressdetails=True, + limit=10, + ) + except GeocoderTimedOut: + logger.error("Nominatim geocoding timed out for address: %s", address) + raise TimeoutError(f"Geocoding service timed out for address: '{address}'") + except GeocoderServiceError as exc: + logger.error("Nominatim geocoding service error: %s", exc) + raise RuntimeError(f"Geocoding service error: {exc}") + + if not locations: + logger.info("No results found for address: %s", address) + return [] + + results = [] + for loc in locations: + addr = loc.raw.get("address", {}) + results.append({ + "postal_code": addr.get("postcode"), + "place_name": addr.get("city") + or addr.get("town") + or addr.get("village") + or addr.get("hamlet") + or addr.get("municipality") + or "", + "latitude": loc.latitude, + "longitude": loc.longitude, + "display_name": loc.address, + "country": addr.get("country"), + "state": addr.get("state"), + "county": addr.get("county"), + }) + logger.debug("Geocoded result: %s", results[-1]) + + return results \ No newline at end of file diff --git a/app/services/external_apis_coordinator.py b/app/services/external_apis_coordinator.py new file mode 100644 index 00000000..1e7654f1 --- /dev/null +++ b/app/services/external_apis_coordinator.py @@ -0,0 +1,14 @@ +from app.services.external_apis.weather_api import WeatherAPI +from app.services.external_apis.zipcode_api import ZipCodeAPI + + +class ExternalAPIsCoordinator: + def __init__(self): + self.weather_api = WeatherAPI() + self.zipcode_api = ZipCodeAPI() + + def get_weather(self, latitude: float, longitude: float, start_date: str, end_date: str, hourly_fields: list[str] | None = None): + return self.weather_api.get_weather(latitude, longitude, start_date, end_date, hourly_fields) + + def lookup_address(self, address: str) -> list[dict]: + return self.zipcode_api.lookup_address(address) \ No newline at end of file diff --git a/src/file_manipulator.py b/app/services/file_manipulator.py similarity index 78% rename from src/file_manipulator.py rename to app/services/file_manipulator.py index 8d1f3a0a..e4267971 100644 --- a/src/file_manipulator.py +++ b/app/services/file_manipulator.py @@ -1,6 +1,10 @@ import os -from src.filler import Filler -from src.llm import LLM + +from app.core.logging import get_logger +from app.services.filler import Filler +from app.services.llm import LLM + +logger = get_logger(__name__) class FileManipulator: @@ -39,25 +43,23 @@ def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: st It receives the raw data, runs the PDF filling logic, and returns the path to the newly created file. """ - print("[1] Received request from frontend.") - print(f"[2] PDF template path: {pdf_form_path}") + logger.info("[1] Received request from frontend.") + logger.info("[2] PDF template path: %s", pdf_form_path) if not os.path.exists(pdf_form_path): raise FileNotFoundError(f"PDF template not found at {pdf_form_path}") - print("[3] Starting extraction and PDF filling process...") + logger.info("[3] Starting extraction and PDF filling process...") try: self.llm._target_fields = fields self.llm._transcript_text = user_input self.llm._model = model output_name = self.filler.fill_form(pdf_form=pdf_form_path, llm=self.llm) - print("\n----------------------------------") - print("✅ Process Complete.") - print(f"Output saved to: {output_name}") + logger.info("Process complete. Output saved to: %s", output_name) return output_name except Exception as e: - print(f"An error occurred during PDF generation: {e}") + logger.error("An error occurred during PDF generation: %s", e) raise e diff --git a/src/filler.py b/app/services/filler.py similarity index 97% rename from src/filler.py rename to app/services/filler.py index 7f738c24..148b2ff6 100644 --- a/src/filler.py +++ b/app/services/filler.py @@ -1,7 +1,9 @@ -from pdfrw import PdfReader, PdfWriter -from src.llm import LLM from datetime import datetime +from pdfrw import PdfReader, PdfWriter + +from app.services.llm import LLM + class Filler: def __init__(self): diff --git a/app/services/input.py b/app/services/input.py new file mode 100644 index 00000000..bdd51c43 --- /dev/null +++ b/app/services/input.py @@ -0,0 +1,91 @@ +from datetime import date, datetime, timezone + +from sqlmodel import Session + +from app.api.schemas.enums import InputStatus, InputType +from app.core.config import AUDIO_DIR +from app.db.repositories import create_input, create_job, update_job +from app.models import Input, Job +from app.tasks.transcribe import transcribe_audio_task + + +class InputService: + def build_voice_input( + self, + original_filename: str, + station_id: str | None = None, + responder_badge: str | None = None, + incident_date_hint: date | None = None, + ) -> Input: + now = datetime.now(timezone.utc) + return Input( + input_type=InputType.voice, + status=InputStatus.queued, + original_filename=original_filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + created_at=now, + updated_at=now, + ) + + def process_voice_upload( + self, + session: Session, + audio_content: bytes, + ext: str, + filename: str, + station_id: str | None, + responder_badge: str | None, + incident_date_hint: date | None, + ) -> tuple[Input, Job]: + record = self.build_voice_input( + original_filename=filename, + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + ) + record = create_input(session, record) + + AUDIO_DIR.mkdir(parents=True, exist_ok=True) + audio_path = AUDIO_DIR / f"{record.input_id}.{ext}" + audio_path.write_bytes(audio_content) + + # Create Job, dispatch, and backfill celery_task_id. + # On any failure after the file write, remove the orphaned audio file. + try: + job = Job(celery_task_id="", job_type="transcription", status="queued") + job = create_job(session, job) + result = transcribe_audio_task.delay(str(record.input_id), str(audio_path), job.job_id) + job.celery_task_id = result.id + job = update_job(session, job) + except Exception: + if audio_path.exists(): + audio_path.unlink() + raise + + return record, job + + def build_text_input( + self, + narrative: str, + station_id: str | None = None, + responder_badge: str | None = None, + incident_date_hint: date | None = None, + ) -> Input: + words = narrative.split() + if len(words) < 10: + raise ValueError("Must contain at least 10 words") + now = datetime.now(timezone.utc) + return Input( + input_type=InputType.text, + status=InputStatus.ready, + transcript=narrative, + character_count=len(narrative), + word_count=len(words), + station_id=station_id, + responder_badge=responder_badge, + incident_date_hint=incident_date_hint, + created_at=now, + updated_at=now, + ) diff --git a/src/inputs/input.txt b/app/services/inputs/input.txt similarity index 100% rename from src/inputs/input.txt rename to app/services/inputs/input.txt diff --git a/src/llm.py b/app/services/llm.py similarity index 79% rename from src/llm.py rename to app/services/llm.py index 053b8836..85e168c7 100644 --- a/src/llm.py +++ b/app/services/llm.py @@ -1,7 +1,13 @@ import json import os + import requests -from requests.exceptions import Timeout, RequestException +from requests.exceptions import RequestException, Timeout + +from app.core.config import OLLAMA_HOST, OLLAMA_MODEL +from app.core.logging import get_logger + +logger = get_logger(__name__) class LLM: @@ -31,9 +37,8 @@ def main_loop(self): total_fields = len(self._target_fields) for i, (field, field_type) in enumerate(self._target_fields.items(), 1): prompt = self.build_prompt(field, field_type if isinstance(field_type, str) else "string") - ollama_host = os.getenv("OLLAMA_HOST", "http://localhost:11434").rstrip("/") - ollama_url = f"{ollama_host}/api/generate" - ollama_model = self._model or os.getenv("OLLAMA_MODEL", "qwen2.5:1.5b") + ollama_url = f"{OLLAMA_HOST}/api/generate" + ollama_model = self._model or OLLAMA_MODEL payload = { "model": ollama_model, @@ -50,9 +55,9 @@ def main_loop(self): json_data = response.json() break except Timeout: - print(f"[LOG]: Ollama request timed out (attempt {attempt+1}) for field '{field}'. Retrying...") + logger.warning("Ollama request timed out (attempt %d) for field '%s'. Retrying...", attempt + 1, field) except RequestException as e: - print(f"[LOG]: Ollama request failed: {e}") + logger.error("Ollama request failed: %s", e) except requests.exceptions.ConnectionError: raise ConnectionError( f"Could not connect to Ollama at {ollama_url}. " @@ -66,12 +71,9 @@ def main_loop(self): else: parsed_response = json_data["response"] self.add_response_to_json(field, parsed_response) - print(f"[{i}/{total_fields}] Extracted data for field '{field}' successfully.") + logger.info("[%d/%d] Extracted data for field '%s' successfully.", i, total_fields, field) - print("----------------------------------") - print("\t[LOG] Resulting JSON created from the input text:") - print(json.dumps(self._json, indent=2)) - print("--------- extracted data ---------") + logger.info("Resulting JSON created from the input text:\n%s", json.dumps(self._json, indent=2)) return self @@ -91,7 +93,6 @@ def add_response_to_json(self, field: str, value: str): else: self._json[field] = parsed_value - return def get_data(self): diff --git a/src/outputs/test_output_1.json b/app/services/outputs/test_output_1.json similarity index 100% rename from src/outputs/test_output_1.json rename to app/services/outputs/test_output_1.json diff --git a/src/prompt.txt b/app/services/prompt.txt similarity index 100% rename from src/prompt.txt rename to app/services/prompt.txt diff --git a/src/test/example_template.json b/app/services/test/example_template.json similarity index 100% rename from src/test/example_template.json rename to app/services/test/example_template.json diff --git a/src/test/test_output_1.json b/app/services/test/test_output_1.json similarity index 100% rename from src/test/test_output_1.json rename to app/services/test/test_output_1.json diff --git a/app/services/whisper.py b/app/services/whisper.py new file mode 100644 index 00000000..118af8ec --- /dev/null +++ b/app/services/whisper.py @@ -0,0 +1,38 @@ +import requests + +from app.core.config import WHISPER_HOST + + +def call_whisper_asr(audio_bytes: bytes, filename: str, content_type: str) -> str: + """Post audio to the local Whisper ASR sidecar and return the transcript. + + Raises ConnectionError if the service is unreachable, RuntimeError for any + other HTTP failure. Callers map these to their own error codes. + """ + whisper_url = f"{WHISPER_HOST}/asr" + files = {"audio_file": (filename, audio_bytes, content_type)} + params = {"task": "transcribe", "output": "json", "encode": "true"} + + try: + response = requests.post(whisper_url, params=params, files=files, timeout=120) + response.raise_for_status() + except requests.exceptions.ConnectionError as exc: + raise ConnectionError( + f"Could not connect to the speech-to-text service at {whisper_url}. " + "Please ensure the whisper service is running." + ) from exc + except requests.exceptions.RequestException as exc: + raise RuntimeError(f"Transcription failed: {exc}") from exc + + try: + return (response.json().get("text") or "").strip() + except ValueError: + return response.text.strip() + + +def check_whisper_available() -> bool: + """Return True if the Whisper sidecar responds with a successful status.""" + try: + return requests.get(WHISPER_HOST, timeout=3).ok + except requests.exceptions.RequestException: + return False diff --git a/app/tasks/__init__.py b/app/tasks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/tasks/fill.py b/app/tasks/fill.py new file mode 100644 index 00000000..61e4cbff --- /dev/null +++ b/app/tasks/fill.py @@ -0,0 +1,68 @@ +import logging +from datetime import datetime, timezone + +from app.core.celery import celery_app +from app.db.database import get_session +from app.db.repositories import ( + create_form, + get_job_by_celery_id, + get_template, + update_job, +) +from app.models import FormSubmission +from app.services.controller import Controller + +logger = logging.getLogger(__name__) + + +@celery_app.task(bind=True, name="fill_form") +def fill_form_task(self, template_id: int, input_text: str, model: str | None = None): + session = next(get_session()) + try: + job = get_job_by_celery_id(session, self.request.id) + if not job: + raise RuntimeError(f"No job row for celery task {self.request.id}") + + job.status = "processing" + job.progress_percent = 10 + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + + template = get_template(session, template_id) + if not template: + raise ValueError(f"Template {template_id} not found") + + controller = Controller() + path = controller.fill_form( + user_input=input_text, + fields=template.fields, + pdf_form_path=template.pdf_path, + model=model, + ) + + submission = FormSubmission( + template_id=template_id, + input_text=input_text, + output_pdf_path=path, + ) + create_form(session, submission) + + job.status = "completed" + job.progress_percent = 100 + job.result_url = f"/api/v1/forms/{submission.id}/download" + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + + return {"job_id": job.job_id, "result_url": job.result_url} + + except Exception as e: + logger.exception("fill_form_task failed") + job = get_job_by_celery_id(session, self.request.id) + if job: + job.status = "failed" + job.error = {"error_code": "TASK_FAILED", "message": str(e)} + job.updated_at = datetime.now(timezone.utc) + update_job(session, job) + raise + finally: + session.close() diff --git a/app/tasks/purge.py b/app/tasks/purge.py new file mode 100644 index 00000000..0639f525 --- /dev/null +++ b/app/tasks/purge.py @@ -0,0 +1,91 @@ +"""Celery beat task: purge form submissions older than RETENTION_PERIOD_DAYS. + +Runs automatically if Celery Beat is configured. Can also be triggered manually +via the API endpoint POST /api/v1/forms/purge. +""" + +import logging +from datetime import datetime, timedelta, timezone +from pathlib import Path + +from sqlmodel import select + +from app.core.celery import celery_app +from app.core.config import BASE_DIR, RETENTION_PERIOD_DAYS +from app.db.database import get_session +from app.db.repositories import delete_form_submission +from app.models import FormSubmission + +logger = logging.getLogger(__name__) + +PROJECT_ROOT = BASE_DIR + + +def _safe_delete_file(file_path: str) -> bool: + """Delete a project-relative or absolute file safely. Returns True if deleted.""" + try: + candidate = Path(file_path) + if not candidate.is_absolute(): + candidate = (PROJECT_ROOT / candidate).resolve() + else: + candidate = candidate.resolve() + + # Safety: must stay inside project root + if candidate != PROJECT_ROOT and PROJECT_ROOT not in candidate.parents: + logger.warning("Skipping file outside project root: %s", candidate) + return False + + if candidate.exists() and candidate.is_file(): + candidate.unlink() + return True + except Exception as exc: + logger.error("Failed to delete file %s: %s", file_path, exc) + return False + + +@celery_app.task(name="purge_old_submissions") +def purge_old_submissions(retention_days: int | None = None) -> dict: + """Delete form submissions (and associated output PDFs) older than retention_days. + + Args: + retention_days: Number of days to retain data. Defaults to the + RETENTION_PERIOD_DAYS environment variable (default: 30). + + Returns: + dict with purged_count and retention_days_used. + """ + days = retention_days if retention_days is not None else RETENTION_PERIOD_DAYS + cutoff = datetime.now(timezone.utc) - timedelta(days=days) + logger.info("Purging submissions older than %s days (cutoff: %s)", days, cutoff) + + session = next(get_session()) + purged_count = 0 + files_deleted = 0 + + try: + statement = select(FormSubmission).where(FormSubmission.created_at < cutoff) + submissions = list(session.exec(statement)) + + for sub in submissions: + if sub.output_pdf_path: + if _safe_delete_file(sub.output_pdf_path): + files_deleted += 1 + delete_form_submission(session, sub) + purged_count += 1 + + logger.info( + "Purge complete: removed %d submissions and %d output files", + purged_count, + files_deleted, + ) + return { + "purged_count": purged_count, + "files_deleted": files_deleted, + "retention_days_used": days, + } + + except Exception as exc: + logger.exception("purge_old_submissions task failed: %s", exc) + raise + finally: + session.close() diff --git a/app/tasks/transcribe.py b/app/tasks/transcribe.py new file mode 100644 index 00000000..91999c65 --- /dev/null +++ b/app/tasks/transcribe.py @@ -0,0 +1,102 @@ +import logging +import wave +from datetime import datetime, timezone +from pathlib import Path +from uuid import UUID + +from app.api.schemas.enums import InputStatus +from app.core.celery import celery_app +from app.core.config import AUDIO_CONTENT_TYPES +from app.db.database import get_session +from app.db.repositories import get_input, get_job_by_uuid, update_input, update_job +from app.services.whisper import call_whisper_asr + +logger = logging.getLogger(__name__) + + +def _wav_duration(path: Path) -> float | None: + """Return duration in seconds for a WAV file; None for all other formats or on error.""" + if path.suffix.lower() != ".wav": + return None + try: + with wave.open(str(path)) as wf: + return wf.getnframes() / wf.getframerate() + except Exception: + return None + + +def _fail_records( + session, + input_id: UUID, + job_id_str: str, + error_code: str, + message: str, +) -> None: + now = datetime.now(timezone.utc) + record = get_input(session, input_id) + if record: + record.status = InputStatus.failed + record.error_detail = message + record.updated_at = now + update_input(session, record) + job = get_job_by_uuid(session, job_id_str) + if job: + job.status = "failed" + job.error = {"error_code": error_code, "message": message} + job.updated_at = now + update_job(session, job) + + +@celery_app.task(name="transcribe_audio") +def transcribe_audio_task(input_id_str: str, audio_path: str, job_id_str: str) -> dict: + session = next(get_session()) + input_id = UUID(input_id_str) + try: + input_record = get_input(session, input_id) + job = get_job_by_uuid(session, job_id_str) + + # start: mark both records in-flight + now = datetime.now(timezone.utc) + input_record.status = InputStatus.transcribing + input_record.updated_at = now + update_input(session, input_record) + + job.status = "processing" + job.updated_at = now + update_job(session, job) + + # transcribe + p = Path(audio_path) + ext = p.suffix.lstrip(".") + text = call_whisper_asr(p.read_bytes(), p.name, AUDIO_CONTENT_TYPES.get(ext, "audio/wav")) + + # success + words = text.split() + now = datetime.now(timezone.utc) + input_record.status = InputStatus.ready + input_record.transcript = text + input_record.character_count = len(text) + input_record.word_count = len(words) + input_record.audio_duration_seconds = _wav_duration(p) + input_record.updated_at = now + update_input(session, input_record) + + job.status = "completed" + job.result_url = f"/api/v1/input/{input_id_str}" + job.updated_at = now + update_job(session, job) + + return {"input_id": input_id_str, "job_id": job_id_str} + + except ConnectionError as exc: + logger.exception("transcribe_audio_task: Whisper unavailable for input %s", input_id_str) + _fail_records(session, input_id, job_id_str, "STT_UNAVAILABLE", str(exc)) + raise + + except RuntimeError as exc: + logger.exception("transcribe_audio_task: transcription failed for input %s", input_id_str) + _fail_records(session, input_id, job_id_str, "TRANSCRIPTION_FAILED", str(exc)) + raise + + finally: + session.close() diff --git a/benchmark/ICS_DATASET_GENERATOR_PROMPT.md b/benchmark/ICS_DATASET_GENERATOR_PROMPT.md new file mode 100644 index 00000000..aef2b99a --- /dev/null +++ b/benchmark/ICS_DATASET_GENERATOR_PROMPT.md @@ -0,0 +1,183 @@ +# ICS Benchmark Dataset Generator — System Prompt + +## Role +You are an **ICS (Incident Command System) Benchmark Dataset Specialist**. Your job is to analyze an official ICS form PDF and produce a complete, high-quality benchmark dataset for that form type. The dataset consists of three deliverables per form type: + +1. **A JSON Template Schema** (`ics_XXX.json`) — the canonical field structure for the form +2. **5 Narrative Files** (`icsXXX_N.txt`) — realistic, richly detailed incident briefing narratives +3. **5 Ground Truth JSON Files** (`icsXXX_N.json`) — structured JSON objects that exactly match the data in the corresponding narrative + +All files must be internally consistent: every field in the ground truth JSON must be directly extractable from the narrative text. + +--- + +## Step 1 — Extract the Template Schema from the PDF + +Read the provided ICS form PDF carefully. Identify every labeled field, section, sub-section, and repeating row. Then produce a **JSON template schema** where: + +- Every field is represented with its **canonical key name** (snake_case, prefixed with the section number when appropriate, e.g. `"1_incident_name"`, `"2_operational_period"`) +- Every field value is typed as a **placeholder string** `"string"`, `"boolean"`, or an **array** `[]` or **nested object** `{}` +- Repeating rows (e.g. resource tables, radio channels, personnel lists) are represented as **arrays of objects** +- Nested sections (e.g. branches with sub-groups) use **nested objects or arrays** +- No fields from the PDF are omitted — capture every labeled input box, checkbox, and table column + +### Output location +Save the template as: +``` +benchmark/datasets/templates/ics_XXX.json +``` + +--- + +## Step 2 — Generate 5 Distinct Mock Incidents + +Create **5 unique, realistic emergency incidents** to populate your dataset. Each incident must: + +- Be a **different incident type** (e.g. chemical spill, tanker collision, pipeline rupture, railcar derailment, industrial release) +- Involve a **different geography** (different U.S. states/regions/waterways) +- Involve **different response agencies** (USCG, EPA, Cal OES, State DEP, County Fire, etc.) +- Involve **different hazardous materials** (Benzene, Styrene, Chlorine, Anhydrous Ammonia, Crude Oil, Sulfuric Acid, etc.) +- Use **different operational contexts** — day shift vs. night shift, maritime vs. inland, urban vs. mountain, etc. +- Reference the **same cast of personnel across forms** — if an incident has a Planning Section Chief named "Rachel Brooks" in the ICS 201, she must appear in the ICS 202, 203, 204, etc. for that same incident + +--- + +## Step 3 — Write the Narrative (`.txt` file) + +For each incident, write a **formal ICS narrative** that reads like an official briefing document. Adhere to these rules: + +### Style & Tone +- Professional, authoritative emergency management language +- Written in full paragraphs (not bullet points), as if read aloud at a shift briefing +- Dense with operational detail — specific numbers, times, distances, frequencies, names, unit identifiers + +### Structure +The narrative must organically embed **every data field** from the template schema, in natural prose, without using JSON-style formatting or field labels. A downstream LLM must be able to read this narrative and extract every ground truth value from it. + +### Required Detail Level +- **Named personnel** with full titles and affiliations for every position +- **Specific radio frequencies** (e.g. `462.5500 MHz`, `Tone 114.8`) +- **Unit identifiers** (e.g. `HZM-01`, `ENG-41`, `RV-LC-01`) +- **Exact times** for all actions and operational periods +- **Specific geographic references** (mile markers, lat/lon, staging area locations) +- **Specific hazmat parameters** (flash points, IDLH thresholds, PPE levels, decon procedures) +- **Specific quantities** (gallons released, feet of boom, lbs of chemical, number of personnel) + +### Output location +Save each narrative as: +``` +benchmark/datasets/narratives/icsXXX_N.txt +``` +Where `XXX` is the form number (e.g. `201`, `202`, `205a`) and `N` is 1–5. + +--- + +## Step 4 — Write the Ground Truth JSON (`.json` file) + +For each narrative, produce a **strictly schema-compliant JSON object** that: + +- Uses the **exact same field structure** as the template schema in Step 1 +- Is wrapped in a top-level key: `"ics_XXX_ground_truth": { ... }` +- Populates **every field** with the exact value as stated in the narrative (no paraphrasing, no invention) +- Uses `true`/`false` (not strings) for boolean fields (e.g. `"arrived": true`) +- Uses arrays correctly for repeating elements +- Leaves fields as `""` only if the narrative explicitly states the position is unfilled — never omit fields from the schema +- All string values preserve the exact formatting used in the narrative (e.g. `"07/07/2026"` not `"2026-07-07"`) + +### Output location +Save each ground truth file as: +``` +benchmark/datasets/ground_truth/icsXXX_N.json +``` + +--- + +## Step 5 — Cross-Consistency Requirements + +Apply these rules across ALL files you create: + +| Rule | Description | +|---|---| +| **Name consistency** | Every person's name, title, and affiliation must be identical across all forms for the same incident | +| **Time consistency** | Operational period dates/times must match across ICS 202, 203, 204, 205, 205A for the same incident | +| **Unit consistency** | Resource identifiers (e.g. `HZM-01`) must match across ICS 201, 204 for the same incident | +| **Frequency consistency** | Radio frequencies in ICS 205 must match those referenced in ICS 204 and ICS 205A | +| **Incident name consistency** | The exact same incident name string must appear in every form for that incident | +| **IAP page number** | Each form type has a conventional page position in the IAP — assign realistic sequential page numbers | + +--- + +## File Naming Convention + +``` +Templates: benchmark/datasets/templates/ics_XXX.json +Narratives: benchmark/datasets/narratives/icsXXX_N.txt +Ground Truth: benchmark/datasets/ground_truth/icsXXX_N.json +``` + +Where: +- `XXX` = form number: `201`, `202`, `203`, `204`, `205`, `205a`, `206`, `207`, `208`, etc. +- `N` = incident index: `1` through `5` + +--- + +## Form-Specific Guidance + +### ICS 201 — Incident Briefing +Fields cover: incident name/number, initiation date/time, map/sketch details (area of operations, impacted areas, trajectories, shorelines), situation summary, health/safety hazards, protective measures, preparer info, objectives list, chronological tactics table, command/general staff organization (with additional positions), and full resource summary table (identifier, leader, ordered time, ETA, arrived boolean, notes). + +### ICS 202 — Incident Objectives +Fields cover: incident name, operational period (from/to date and time), objectives list (SMART-formatted strings), operational period command emphasis paragraph, general situational awareness paragraph, site safety plan required (boolean), safety plan location, IAP attachments checklist (ICS 203–208, map/chart, weather, other), preparer info, incident commander approval info, and IAP page number. + +### ICS 203 — Organization Assignment List +Fields cover: incident name, operational period, command staff (IC/UC list, deputy, safety officer, PIO, liaison), agency/organization representatives table, planning section (chief, deputy, unit leaders, technical specialists), logistics section (chief, deputy, support branch with sub-units, service branch with sub-units), operations section (chief, deputy, staging area, branches with directors/deputies/divisions/groups, air ops branch), finance/admin section (chief, deputy, unit leaders), preparer info, and IAP page number. + +### ICS 204 — Assignment List +Fields cover: incident name, operational period, branch/division/group/staging area identifiers, operations personnel (ops chief, branch director, div/group supervisor — each with name and contact), resources assigned table (identifier, leader, persons, contact, reporting location/equipment/notes), work assignments paragraph, special instructions paragraph, communications table (function/name and primary contact), preparer info, and IAP page number. + +### ICS 205 — Incident Radio Communications Plan +Fields cover: incident name, date/time prepared, operational period, radio channel table (zone/group, channel number, function, channel name/talkgroup, assignment, RX frequency with N/W, RX tone/NAC, TX frequency with N/W, TX tone/NAC, mode A/D/M, remarks), special instructions, preparer info (name, signature, date/time), and IAP page number. + +### ICS 205A — Communications List +Fields cover: incident name, operational period, basic local communications table (incident assigned position, name, methods of contact), preparer info (name, position title, signature, date/time), and IAP page number. + +--- + +## Quality Checklist + +Before finalizing, verify: + +- [ ] Template schema captures every labeled field in the PDF +- [ ] All 5 narratives reference different incidents, regions, chemicals, and agencies +- [ ] Each narrative is rich enough that a downstream LLM can extract every ground truth field from prose alone +- [ ] Every ground truth JSON is 100% schema-compliant with the template +- [ ] All string values in JSON match the narrative exactly (same spelling, same format) +- [ ] Personnel names, unit IDs, frequencies, and times are internally consistent within each incident +- [ ] Boolean fields use `true`/`false`, not `"true"`/`"false"` +- [ ] Files are saved in the correct directories with the correct naming convention + +--- + +## Example Excerpt (ICS 205) + +**Narrative excerpt:** +> *Channel 2 (Zone A): Functioned for Tactical operations, channel name HAZ-TAC-1, assigned to the Hot Zone Entry Group. Operates on RX frequency 467.7750 N with DCS tone D023 and TX frequency 467.7750 N with DCS tone D023 in Digital mode, restricting use to intrinsically safe radios only.* + +**Corresponding ground truth:** +```json +{ + "zone_grp": "Zone A", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "HAZ-TAC-1", + "assignment": "Hot Zone Entry Group", + "rx_frequency_n_or_w": "467.7750 N", + "rx_tone_nac": "D023", + "tx_frequency_n_or_w": "467.7750 N", + "tx_tone_nac": "D023", + "mode_a_d_or_m": "D", + "remarks": "Intrinsically safe radios only" +} +``` + +This demonstrates the core principle: **every JSON value must be explicitly readable in the narrative prose**. diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..b1e52701 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,22 @@ +# FireForm Extraction Pipeline Benchmarking Suite + +This module contains the infrastructure to evaluate and compare different extraction pipelines across branches. + +## Structure +- `/datasets/`: Contains the evaluation narratives, ground truth JSON outputs, and form template schema descriptors. +- `/evaluators/`: Defines custom comparison metrics (fuzzy matching, exact matching). +- `/runners/`: Executable scripts to run pipelines against datasets. +- `test_benchmark.py`: Pytest suite to run the evaluation dynamically on whichever pipeline is checked out. +- `compare_benchmarks.py`: Tool to format a markdown diff report comparing two output JSON report files. + +## Running Locally +Install test requirements: +```bash +pip install pytest +``` + +Execute the local benchmark: +```bash +pytest benchmark/test_benchmark.py -v -s +``` +This produces `benchmark/benchmark_report.json` containing metrics (accuracy, latency) and full result logs. diff --git a/benchmark/__init__.py b/benchmark/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/benchmark_report.json b/benchmark/benchmark_report.json new file mode 100644 index 00000000..a93e32aa --- /dev/null +++ b/benchmark/benchmark_report.json @@ -0,0 +1,240 @@ +{ + "pipeline_name": "Pipeline_2026-08-03_18h_16m_49s", + "metrics": { + "total_latency_seconds": 1.1920928955078125e-06, + "average_accuracy": 0.0 + }, + "results": [ + { + "case_id": "ics201_1", + "latency_seconds": 1.1920928955078125e-06, + "accuracy_score": 0.0, + "extracted_fields": {}, + "ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_incident_number": "US-EPA-R4-2026-0707", + "3_date_time_initiated": { + "date": "07/07/2026", + "time": "06:30" + }, + "4_map_sketch": { + "total_area_of_operations": "1.5-mile radius from the intersection of CSX Rail Milepost 142.5 and Highway 411", + "incident_site_area": "Rail Milepost 142.5; 7 cars derailed; Car #14 breached and leaking Benzene; Car #15 LPG threatened by fire", + "impacted_areas": "500 meters downstream of Whispering Pines Creek actively sheening; air monitoring shows elevated VOCs within 300 feet downwind East", + "threatened_areas": "Whispering Pines Municipal Water Intake (2 miles downstream); Whispering Pines Nature Reserve Wetlands (1 mile downstream)", + "overflight_results": "Drone mapping downstream sheen trajectory and pinpointing structural damage to Car #14", + "trajectories": "Plume traveling East-Southeast at 5 mph; water containment trajectory moving at 1.5 knots East", + "impacted_shorelines": "Whispering Pines Creek banks", + "graphics_and_symbology": "North at top, showing County Line Road, Creek Road, Highway 411, CSX Rail Line, Staging Area at County Fairgrounds, and Boom locations 1 and 2" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 05:45 hours, a CSX freight train suffered a mechanical failure leading to the derailment of 7 cars. Car #14 is breached, leaking Benzene into Whispering Pines Creek at an estimated rate of 20 gpm. A secondary brush fire (~0.5 acres) is burning adjacent to an intact LPG tank car. Local residential evacuation of a 0.5-mile downwind radius is underway.", + "health_and_safety_hazards": "Benzene exposure (carcinogen, vapor hazard, inhalation/dermal risk), flash fire hazard from pooling product, heavy machinery/rail movement, heat stress (88\u00b0F), and uneven terrain near the creek bed.", + "necessary_measures": "Isolate and deny entry with a 300-foot Exclusion Zone; continuous air monitoring for LEL, O2, and PID; Level B PPE (SCBA + chemical-resistant clothing) for Hot Zone, Level C for warm zone (<1 ppm VOCs); full structural turnout gear with SCBA for fire personnel; establish a formal wet-decon corridor at the Warm/Cold zone interface." + }, + "6_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief (Initial)", + "signature": "Marcus Vance", + "date_time": "07/07/2026 08:00" + }, + "7_current_and_planned_objectives": [ + "Ensure the life safety of all emergency responders and the public within the affected perimeter by 09:00 hours.", + "Complete evacuation of the 0.5-mile downwind hazard zone by 09:30 hours.", + "Suppress the brush fire adjacent to the LPG tank car to prevent thermal impingement by 10:00 hours.", + "Deploy containment and deflection booming across Whispering Pines Creek at designated points to prevent downstream migration to the municipal water intake.", + "Establish a Unified Command structure involving Federal, State, Local, and Responsible Party entities by 10:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "06:00", + "actions": "Initial dispatch of County Fire and Sheriff Dept. Exclusion zone established at 500 feet." + }, + { + "time": "06:15", + "actions": "Command established by Fire Chief Thomas. Notification sent to State DEQ, EPA, and CSX Railroad." + }, + { + "time": "06:45", + "actions": "Sheriff Dept begins door-to-door mandatory evacuations of 45 homes within the downwind path." + }, + { + "time": "07:15", + "actions": "Hazmat Team 1 arrives. Begins setting up air monitoring perimeter and evaluating tank car integrity." + }, + { + "time": "07:30", + "actions": "Engine 41 and Tanker 5 begin defensive fire suppression on the right-of-way brush fire using foam blankets." + }, + { + "time": "07:45", + "actions": "State DEQ and CSX advance response teams arrive on scene. Initiated creek booming strategy." + }, + { + "time": "08:15", + "actions": "Planned: Deploy Spill Response Team to install 500 feet of deflection boom at Boom Site 1." + }, + { + "time": "08:45", + "actions": "Planned: Initiate drone overflight to map downstream sheen trajectory and pinpoint structural damage to Car #14." + }, + { + "time": "09:30", + "actions": "Planned: Complete transition from Local Command to Unified Command at the designated Mobile Command Post." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County)", + "liaison_officer": "Inspector G. Sims (SO)", + "operations_section_chief": "B. Reynolds (State)", + "planning_section_chief": "Marcus Vance", + "logistics_section_chief": "K. Dunavan", + "finance_administration_section_chief": "", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lt. T. Kincaid (Regional Team 1)" + }, + { + "position": "Fire Suppression Division Commander", + "name": "Batt. Chief E. Walters" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Type 1", + "resource_identifier": "Regional Hazmat Team 1", + "date_time_ordered": "07/07/2026 06:00", + "eta": "07:00", + "arrived": true, + "notes": "Positioned at Cold Zone boundary. Conducting air monitoring and plugging entry." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 41", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:00", + "arrived": true, + "notes": "Assigned to Fire Suppression Division. Suppressing brush fire near rail line." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 45", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:05", + "arrived": true, + "notes": "Providing water supply support to Engine 41." + }, + { + "resource": "Water Tender", + "resource_identifier": "County Tanker 5", + "date_time_ordered": "07/07/2026 06:02", + "eta": "06:20", + "arrived": true, + "notes": "Supplying continuous water flow for foam application." + }, + { + "resource": "Law Enforcement", + "resource_identifier": "County Sheriff (4 Units)", + "date_time_ordered": "07/07/2026 05:48", + "eta": "05:55", + "arrived": true, + "notes": "Establishing traffic control points, roadblocks on Creek Rd, and evacuating residents." + }, + { + "resource": "Spill Contractor", + "resource_identifier": "HEPACO Spill Team A", + "date_time_ordered": "07/07/2026 06:45", + "eta": "08:15", + "arrived": false, + "notes": "En route with 1,000 ft containment boom and vacuum trucks. Assigned to Ops / Creek booming." + }, + { + "resource": "UAS (Drone)", + "resource_identifier": "State DEQ Drone Unit 1", + "date_time_ordered": "07/07/2026 07:10", + "eta": "08:30", + "arrived": false, + "notes": "En route. Will launch from Staging Area to provide real-time thermal imaging and plume visuals." + }, + { + "resource": "Ambulance", + "resource_identifier": "County EMS Unit 12", + "date_time_ordered": "07/07/2026 06:00", + "eta": "06:12", + "arrived": true, + "notes": "Standing by at Rehab/Staging for emergency medical support or responder heat issues." + }, + { + "resource": "Vacuum Truck", + "resource_identifier": "CSX Contractor TechRad", + "date_time_ordered": "07/07/2026 07:15", + "eta": "09:30", + "arrived": false, + "notes": "Ordered by Responsible Party for offloading breached car contents once stable." + } + ] + } + }, + { + "case_id": "ics202_1", + "latency_seconds": 0.0, + "accuracy_score": 0.0, + "extracted_fields": {}, + "ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62\u00b0F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } + } + ] +} \ No newline at end of file diff --git a/benchmark/compare_benchmarks.py b/benchmark/compare_benchmarks.py new file mode 100644 index 00000000..25e1d15b --- /dev/null +++ b/benchmark/compare_benchmarks.py @@ -0,0 +1,45 @@ +import json +import os +import sys + + +def main(): + if len(sys.argv) < 3: + print("Usage: python compare_benchmarks.py ") + sys.exit(1) + + branch_file = sys.argv[1] + target_file = sys.argv[2] + + if not os.path.exists(branch_file) or not os.path.exists(target_file): + print(f"Error: One or both files do not exist: {branch_file}, {target_file}") + sys.exit(1) + + with open(branch_file, "r") as f: + branch_data = json.load(f) + + with open(target_file, "r") as f: + target_data = json.load(f) + + b_metrics = branch_data.get("metrics", {}) + t_metrics = target_data.get("metrics", {}) + + b_acc = b_metrics.get("average_accuracy", 0.0) + t_acc = t_metrics.get("average_accuracy", 0.0) + + b_lat = b_metrics.get("total_latency_seconds", 0.0) + t_lat = t_metrics.get("total_latency_seconds", 0.0) + + # Format Markdown comparison + markdown = f""" +## Pipeline Benchmark Comparison Report + +| Metric | Target Branch ({target_data.get('pipeline_name', 'Unknown')}) | PR Branch ({branch_data.get('pipeline_name', 'Unknown')}) | Difference | +|---|---|---|---| +| **Average Accuracy** | {t_acc:.2%} | {b_acc:.2%} | {b_acc - t_acc:+.2%} | +| **Total Latency** | {t_lat:.2f}s | {b_lat:.2f}s | {b_lat - t_lat:+.2f}s | +""" + print(markdown) + +if __name__ == "__main__": + main() diff --git a/benchmark/datasets/__init__.py b/benchmark/datasets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/datasets/ground_truth/ics201_1.json b/benchmark/datasets/ground_truth/ics201_1.json new file mode 100644 index 00000000..ff15a1b3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_1.json @@ -0,0 +1,221 @@ +{ + "ics_201_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_incident_number": "US-EPA-R4-2026-0707", + "3_date_time_initiated": { + "date": "07/07/2026", + "time": "06:30" + }, + "4_map_sketch": { + "total_area_of_operations": "1.5-mile radius from the intersection of CSX Rail Milepost 142.5 and Highway 411", + "incident_site_area": "Rail Milepost 142.5; 7 cars derailed; Car #14 breached and leaking Benzene; Car #15 LPG threatened by fire", + "impacted_areas": "500 meters downstream of Whispering Pines Creek actively sheening; air monitoring shows elevated VOCs within 300 feet downwind East", + "threatened_areas": "Whispering Pines Municipal Water Intake (2 miles downstream); Whispering Pines Nature Reserve Wetlands (1 mile downstream)", + "overflight_results": "Drone mapping downstream sheen trajectory and pinpointing structural damage to Car #14", + "trajectories": "Plume traveling East-Southeast at 5 mph; water containment trajectory moving at 1.5 knots East", + "impacted_shorelines": "Whispering Pines Creek banks", + "graphics_and_symbology": "North at top, showing County Line Road, Creek Road, Highway 411, CSX Rail Line, Staging Area at County Fairgrounds, and Boom locations 1 and 2" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 05:45 hours, a CSX freight train suffered a mechanical failure leading to the derailment of 7 cars. Car #14 is breached, leaking Benzene into Whispering Pines Creek at an estimated rate of 20 gpm. A secondary brush fire (~0.5 acres) is burning adjacent to an intact LPG tank car. Local residential evacuation of a 0.5-mile downwind radius is underway.", + "health_and_safety_hazards": "Benzene exposure (carcinogen, vapor hazard, inhalation/dermal risk), flash fire hazard from pooling product, heavy machinery/rail movement, heat stress (88°F), and uneven terrain near the creek bed.", + "necessary_measures": "Isolate and deny entry with a 300-foot Exclusion Zone; continuous air monitoring for LEL, O2, and PID; Level B PPE (SCBA + chemical-resistant clothing) for Hot Zone, Level C for warm zone (<1 ppm VOCs); full structural turnout gear with SCBA for fire personnel; establish a formal wet-decon corridor at the Warm/Cold zone interface." + }, + "6_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief (Initial)", + "signature": "Marcus Vance", + "date_time": "07/07/2026 08:00" + }, + "7_current_and_planned_objectives": [ + "Ensure the life safety of all emergency responders and the public within the affected perimeter by 09:00 hours.", + "Complete evacuation of the 0.5-mile downwind hazard zone by 09:30 hours.", + "Suppress the brush fire adjacent to the LPG tank car to prevent thermal impingement by 10:00 hours.", + "Deploy containment and deflection booming across Whispering Pines Creek at designated points to prevent downstream migration to the municipal water intake.", + "Establish a Unified Command structure involving Federal, State, Local, and Responsible Party entities by 10:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "06:00", + "actions": "Initial dispatch of County Fire and Sheriff Dept. Exclusion zone established at 500 feet." + }, + { + "time": "06:15", + "actions": "Command established by Fire Chief Thomas. Notification sent to State DEQ, EPA, and CSX Railroad." + }, + { + "time": "06:45", + "actions": "Sheriff Dept begins door-to-door mandatory evacuations of 45 homes within the downwind path." + }, + { + "time": "07:15", + "actions": "Hazmat Team 1 arrives. Begins setting up air monitoring perimeter and evaluating tank car integrity." + }, + { + "time": "07:30", + "actions": "Engine 41 and Tanker 5 begin defensive fire suppression on the right-of-way brush fire using foam blankets." + }, + { + "time": "07:45", + "actions": "State DEQ and CSX advance response teams arrive on scene. Initiated creek booming strategy." + }, + { + "time": "08:15", + "actions": "Planned: Deploy Spill Response Team to install 500 feet of deflection boom at Boom Site 1." + }, + { + "time": "08:45", + "actions": "Planned: Initiate drone overflight to map downstream sheen trajectory and pinpoint structural damage to Car #14." + }, + { + "time": "09:30", + "actions": "Planned: Complete transition from Local Command to Unified Command at the designated Mobile Command Post." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County)", + "liaison_officer": "Inspector G. Sims (SO)", + "operations_section_chief": "B. Reynolds (State)", + "planning_section_chief": "Marcus Vance", + "logistics_section_chief": "K. Dunavan", + "finance_administration_section_chief": "", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lt. T. Kincaid (Regional Team 1)" + }, + { + "position": "Fire Suppression Division Commander", + "name": "Batt. Chief E. Walters" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Type 1", + "resource_identifier": "Regional Hazmat Team 1", + "date_time_ordered": "07/07/2026 06:00", + "eta": "07:00", + "arrived": true, + "notes": "Positioned at Cold Zone boundary. Conducting air monitoring and plugging entry." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 41", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:00", + "arrived": true, + "notes": "Assigned to Fire Suppression Division. Suppressing brush fire near rail line." + }, + { + "resource": "Engine Co.", + "resource_identifier": "County Engine 45", + "date_time_ordered": "07/07/2026 05:48", + "eta": "06:05", + "arrived": true, + "notes": "Providing water supply support to Engine 41." + }, + { + "resource": "Water Tender", + "resource_identifier": "County Tanker 5", + "date_time_ordered": "07/07/2026 06:02", + "eta": "06:20", + "arrived": true, + "notes": "Supplying continuous water flow for foam application." + }, + { + "resource": "Law Enforcement", + "resource_identifier": "County Sheriff (4 Units)", + "date_time_ordered": "07/07/2026 05:48", + "eta": "05:55", + "arrived": true, + "notes": "Establishing traffic control points, roadblocks on Creek Rd, and evacuating residents." + }, + { + "resource": "Spill Contractor", + "resource_identifier": "HEPACO Spill Team A", + "date_time_ordered": "07/07/2026 06:45", + "eta": "08:15", + "arrived": false, + "notes": "En route with 1,000 ft containment boom and vacuum trucks. Assigned to Ops / Creek booming." + }, + { + "resource": "UAS (Drone)", + "resource_identifier": "State DEQ Drone Unit 1", + "date_time_ordered": "07/07/2026 07:10", + "eta": "08:30", + "arrived": false, + "notes": "En route. Will launch from Staging Area to provide real-time thermal imaging and plume visuals." + }, + { + "resource": "Ambulance", + "resource_identifier": "County EMS Unit 12", + "date_time_ordered": "07/07/2026 06:00", + "eta": "06:12", + "arrived": true, + "notes": "Standing by at Rehab/Staging for emergency medical support or responder heat issues." + }, + { + "resource": "Vacuum Truck", + "resource_identifier": "CSX Contractor TechRad", + "date_time_ordered": "07/07/2026 07:15", + "eta": "09:30", + "arrived": false, + "notes": "Ordered by Responsible Party for offloading breached car contents once stable." + } + ] + }, + "ics_202_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews must remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_2.json b/benchmark/datasets/ground_truth/ics201_2.json new file mode 100644 index 00000000..403dcadc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_2.json @@ -0,0 +1,139 @@ +{ + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_incident_number": "US-EPA-R5-2026-0813", + "3_date_time_initiated": { + "date": "August 13, 2026", + "time": "07:15 hours" + }, + "4_map_sketch": { + "total_area_of_operations": "3.0-mile radius around the Blackwood Industrial Corridor", + "incident_site_area": "Pipeline Valve Station 14-B along Blackwood River Road", + "impacted_areas": "150-foot radius surrounding the breached 8-inch pressurized line releasing anhydrous ammonia gas", + "threatened_areas": "Oakridge Residential Subdivision 0.75 miles downwind to the East; Valley Water Authority Municipal Intake Facility 1.5 miles downstream", + "overflight_results": "Dense, low-hanging vapor cloud hugging the ground and moving steadily downwind, along with a localized surface sheen on the riverbank", + "trajectories": "Toxic vapor plume moving East-Northeast at 6 mph; waterborne chemical runoff traveling downstream at approximately 2.0 knots", + "impacted_shorelines": "1,000 yards down the adjacent Blackwood River", + "graphics_and_symbology": "Standard graphics and GIS symbology, oriented with North at top" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "Third-party excavation crew accidentally punctured the main 8-inch transfer pipeline at 06:50 hours, causing an uncontrolled high-pressure release of hazardous anhydrous ammonia gas.", + "health_and_safety_hazards": "Severe atmospheric exposure to anhydrous ammonia vapor, cryogenic freeze burns upon direct contact with liquid leakage, respiratory injury, structural eye damage, responder heat stress in 85°F temperatures, and slip-and-fall risks near steep river embankment slopes.", + "necessary_measures": "Immediate establishment of a 500-foot Exclusion Zone, mandatory Level A vapor-tight PPE with SCBA for Hot Zone entry, continuous PID air monitoring at control perimeters, water curtain deployment to knock down vapor clouds, and compulsory full-body chemical decontamination at the Warm-to-Cold zone boundary prior to site exit." + }, + "6_prepared_by": { + "name": "Sarah Jenkins", + "position_title": "Initial Planning Section Chief", + "signature": "Sarah Jenkins", + "date_time": "August 13, 2026, at 08:30 hours" + }, + "7_current_and_planned_objectives": [ + "Achieve complete isolation of the pipeline segment by closing upstream valves by 09:30 hours.", + "Finish the sheltering-in-place order and partial evacuation of 120 homes in the downwind Oakridge subdivision by 10:00 hours.", + "Deploy absorbent and containment booming across the Blackwood River above the municipal water intake by 10:30 hours.", + "Fully establish a Multi-Agency Unified Command at the Regional Emergency Operations Center by 11:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "07:00 hours", + "actions": "Initial dispatch sent Blackwood Fire Department and Local Sheriff units to establish an outer safety perimeter." + }, + { + "time": "07:20 hours", + "actions": "Fire Chief R. Vance established Incident Command and ordered an immediate downwind shelter-in-place alert." + }, + { + "time": "07:45 hours", + "actions": "Hazmat Team 5 arrived on scene to conduct initial perimeter air monitoring and assist in establishing control zones." + }, + { + "time": "08:15 hours", + "actions": "Pipeline technicians confirmed valve isolation procedures were initiated, while fire crews set up unmanned monitor nozzles to disperse airborne vapors." + }, + { + "time": "09:00 hours", + "actions": "Deploy the Regional Spill Response Team to launch containment boom at River Boom Site Alpha." + }, + { + "time": "09:30 hours", + "actions": "Conduct a secondary drone air sampling flight." + }, + { + "time": "10:00 hours", + "actions": "Finalize the shift to a Unified Command structure at the Regional Emergency Operations Center." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief R. Vance (Blackwood Fire Department)", + "Officer M. Ross (State Environmental Protection Agency)", + "J. Vance (Blackwood Pipeline Co., Responsible Party)" + ], + "safety_officer": "Captain L. Hayes", + "public_information_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller", + "operations_section_chief": "D. Kowalski", + "planning_section_chief": "Sarah Jenkins", + "logistics_section_chief": "T. Bradley", + "finance_administration_section_chief": "A. Patel", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Lieutenant C. Webb" + }, + { + "position": "Air Monitoring Specialist", + "name": "Dr. H. Thorne" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Team 5", + "resource_identifier": "HZM-05", + "date_time_ordered": "August 13, 2026, 07:00 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Executing air monitoring and entry ops at the Hot Zone perimeter." + }, + { + "resource": "Blackwood Engine 12", + "resource_identifier": "ENG-12", + "date_time_ordered": "August 13, 2026, 06:55 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Actively operating water curtains for vapor suppression." + }, + { + "resource": "County Water Tender 3", + "resource_identifier": "WT-03", + "date_time_ordered": "August 13, 2026, 07:10 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Supplying continuous water flow to Engine 12." + }, + { + "resource": "Sheriff Patrol Unit Group Alpha", + "resource_identifier": "SO-ALPHA", + "date_time_ordered": "August 13, 2026, 06:55 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Maintaining road closures and executing downwind evacuation notices." + }, + { + "resource": "CleanHarbor Spill Response Team", + "resource_identifier": "CH-SRT-1", + "date_time_ordered": "August 13, 2026, 07:30 hours", + "eta": "09:00 hours", + "arrived": false, + "notes": "En route with 1,500 feet of river containment boom and vacuum recovery equipment." + }, + { + "resource": "State EPA Air Monitoring Drone Unit", + "resource_identifier": "EPA-DRONE-2", + "date_time_ordered": "August 13, 2026, 07:40 hours", + "eta": "08:45 hours", + "arrived": false, + "notes": "En route to provide real-time thermal and chemical plume mapping." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_3.json b/benchmark/datasets/ground_truth/ics201_3.json new file mode 100644 index 00000000..e80a5e49 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_3.json @@ -0,0 +1,139 @@ +{ + "1_incident_name": "Cypress Bend Tanker Collision", + "2_incident_number": "US-CG-D8-2026-0921", + "3_date_time_initiated": { + "date": "September 21, 2026", + "time": "14:15 hours" + }, + "4_map_sketch": { + "total_area_of_operations": "4.0-mile radius centered at Intracoastal Waterway Mile Marker 245", + "incident_site_area": "Confluence of the Intracoastal Waterway and Cypress Bayou", + "impacted_areas": "300-yard containment zone surrounding a punctured double-hulled barge discharging Heavy Fuel Oil (HFO 380)", + "threatened_areas": "Grand Cypress Mangrove Sanctuary 2.0 miles south-southeast; Delta Commercial Oyster Leases 3.5 miles down-river", + "overflight_results": "Surface slick measuring 2.0 miles long by 100 yards wide moving with the ebb tide, with visible heavy sheen accumulating along the shoreline", + "trajectories": "Waterborne oil plume moving South-Southeast at 1.8 knots; local coastal winds blowing from Northwest at 12 knots", + "impacted_shorelines": "1.5 miles south along the eastern bank of Cypress Bayou", + "graphics_and_symbology": "Standard navigational charts and GIS symbology, oriented with North at top" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "Tugboat towing two loaded asphalt barges collided with an anchored commercial tanker at 13:50 hours, rupturing Cargo Tank #2 starboard and releasing heavy fuel oil.", + "health_and_safety_hazards": "Hydrogen sulfide gas accumulation, hydrocarbon inhalation, direct dermal exposure, slip-and-fall hazards on oily/muddy surfaces, responder heat exhaustion in 91°F heat, and water drowning risks.", + "necessary_measures": "Establishment of a 1,000-foot Maritime Safety Zone, mandatory Level C PPE with half-mask organic vapor respirators and PFDs for over-water personnel, continuous H2S/PID air monitoring, mandatory work-rest hydration cycles, and vessel-based decontamination station." + }, + "6_prepared_by": { + "name": "Commander Elena Rostova", + "position_title": "Initial Planning Section Chief", + "signature": "Commander Elena Rostova", + "date_time": "September 21, 2026, at 15:30 hours" + }, + "7_current_and_planned_objectives": [ + "Complete primary deflection boom deployment across the mouth of Cypress Bayou by 16:30 hours.", + "Secure mechanical patching or product transfer (lightering) of Cargo Tank #2 by 18:00 hours.", + "Deploy skimming vessels to recover free-floating surface product before nightfall at 19:30 hours.", + "Establish a full Joint Information Center to handle media inquiries by 20:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "14:00 hours", + "actions": "Coast Guard Station Cypress Bend dispatched Sector Patrol Craft and issued a Notice to Mariners closing the waterway." + }, + { + "time": "14:30 hours", + "actions": "Captain M. Thorne established Incident Command and deployed first-responder skimmer boats." + }, + { + "time": "15:00 hours", + "actions": "Port Authority Hazmat Teams completed initial safety sweeps and confirmed zero structural fire threats." + }, + { + "time": "15:30 hours", + "actions": "Marine salvage engineers boarded the barge to inspect damage and prep transfer pumps." + }, + { + "time": "16:00 hours", + "actions": "Drop 2,000 feet of hard boom across the mangrove inlet." + }, + { + "time": "17:00 hours", + "actions": "Initiate lightering operations to pump fuel out of the damaged tank." + }, + { + "time": "18:30 hours", + "actions": "Deploy an evening drone overflight to track thermal slick drift patterns." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain M. Thorne (US Coast Guard)", + "Director D. Sterling (State Department of Environmental Quality)", + "H. Vance (Cypress Towing Co., Responsible Party)" + ], + "safety_officer": "Lieutenant Commander J. Ruiz", + "public_information_officer": "C. Alvarez", + "liaison_officer": "Agent P. Brooks", + "operations_section_chief": "G. Morales", + "planning_section_chief": "Commander Elena Rostova", + "logistics_section_chief": "R. O'Connor", + "finance_administration_section_chief": "M. Lin", + "additional_positions": [ + { + "position": "Salvage & Engineering Group Supervisor", + "name": "Chief Specialist K. Vance" + }, + { + "position": "Wildlife Rescue Leader", + "name": "Dr. A. Mercer" + } + ] + }, + "10_resource_summary": [ + { + "resource": "USCG Marine Safety Detachment 1", + "resource_identifier": "USCG-MSD-01", + "date_time_ordered": "September 21, 2026, 14:00 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Enforcing maritime safety zone and conducting gas monitoring." + }, + { + "resource": "Cypress Port Skimmer Vessel 4", + "resource_identifier": "SKIM-04", + "date_time_ordered": "September 21, 2026, 14:10 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Actively skimming free surface product around the barge stern." + }, + { + "resource": "Delta Salvage Tug 'Warrior'", + "resource_identifier": "TUG-WARRIOR", + "date_time_ordered": "September 21, 2026, 14:30 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Providing stabilizing push-support and powering transfer pumps." + }, + { + "resource": "State DEQ Water Sampling Craft", + "resource_identifier": "DEQ-BOAT-2", + "date_time_ordered": "September 21, 2026, 14:20 hours", + "eta": "Arrived", + "arrived": true, + "notes": "Collecting downstream water samples and turbidity readings." + }, + { + "resource": "National Spill Response Team Barge 8", + "resource_identifier": "NSRT-B-08", + "date_time_ordered": "September 21, 2026, 14:45 hours", + "eta": "16:30 hours", + "arrived": false, + "notes": "En route with 3,000 feet of ocean containment boom and heavy skimmers." + }, + { + "resource": "Gulf Coast Wildlife Rescue Unit", + "resource_identifier": "GC-WILD-1", + "date_time_ordered": "September 21, 2026, 15:10 hours", + "eta": "17:15 hours", + "arrived": false, + "notes": "En route to set up an oily bird stabilization and staging facility." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_4.json b/benchmark/datasets/ground_truth/ics201_4.json new file mode 100644 index 00000000..60d81c6e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_4.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Blackwood River Chemical Spill", + "2_incident_number": "USCG-EPA-R4-2026-0813", + "3_date_time_initiated": { + "date": "08/13/2026", + "time": "07:15" + }, + "4_map_sketch": { + "total_area_of_operations": "3.5-mile radius encompassing Blackwood Industrial Park, Mile Marker 42 of the Blackwood River, and State Route 104 corridor.", + "incident_site_area": "Storage Tank #4 at Apex Chemical Tank Farm (MM 42.1, East Bank), involving a structural fracture leaking concentrated Styrene Monomer.", + "impacted_areas": "1,200 meters of Blackwood River surface exhibiting heavy product sheening and a continuous vapor plume extending 800 yards downwind across State Route 104.", + "threatened_areas": "Blackwood Municipal Drinking Water Intake (3 miles downstream at MM 39.1) and Blackwood Marsh Ecological Reserve (1.5 miles downstream).", + "overflight_results": "USCG Aux Drone Flight #1 confirmed a 200-yard wide slick migrating south-southwest at 1.8 knots with moderate shore oiling along the east riverbank.", + "trajectories": "Airborne volatile organic plume tracking East-Northeast at 6 mph; aquatic slick migrating South-Southwest at 1.8 knots downstream.", + "impacted_shorelines": "East bank riprap shoreline and adjacent low-lying mudflats from MM 42.1 to MM 41.3.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area A at Westside High School, Boom Site 1 (Deflection), Boom Site 2 (Containment), Command Post at County Fire Station 12, Exclusion Zone boundary (500 ft radius), and water intake protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 06:45 EDT, a catastrophic seal failure occurred on Tank #4 at Apex Chemical Facility, releasing approximately 8,500 gallons of liquid Styrene Monomer into secondary containment, with overflow entering Blackwood River via storm drain #3 at an estimated 30 gpm. Secondary vapor cloud formed over Route 104, triggering immediate road closures and evacuation of 60 surrounding industrial properties.", + "health_and_safety_hazards": "Inhalation of toxic styrene vapors (narcotic, central nervous system depressant, respiratory irritant), flammability risk (flash point 88°F), dermal contact hazards, slip/trip/fall hazards along wet river banks, and severe heat stress (ambient temp 91°F).", + "necessary_measures": "Establish 500-foot Exclusion Zone; mandate Level B PPE (SCBA with continuous air monitor PID/LEL/O2) within Hot Zone; Level C PPE (full-face APR with organic vapor cartridges) within Warm Zone; continuous air monitoring required at CP and downwind perimeters; establish wet-decontamination corridor at Gate 2." + }, + "6_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 09:30" + }, + "7_current_and_planned_objectives": [ + "Maintain life safety and enforce 500-foot exclusion perimeter by 08:00 hours.", + "Complete containment booming at Boom Site 1 (MM 41.5) by 10:30 hours to protect downstream drinking water intake.", + "Secure leak source on Apex Tank #4 and complete product transfer by 12:00 hours.", + "Perform continuous air monitoring downwind along Route 104 and publish safety advisories by 11:00 hours.", + "Transition to full Unified Command (USCG, EPA, State DEQ, County Hazmat) by 10:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "07:15", + "actions": "Initial alarm dispatch; County Fire Engine 12 and Hazmat 1 respond. Initial 500-foot perimeter established." + }, + { + "time": "07:30", + "actions": "Local Command established by Chief Henderson; Apex facility emergency shutdown initiated. Route 104 closed to traffic." + }, + { + "time": "07:55", + "actions": "Regional Hazmat Team 4 arrives; initiates perimeter PID air monitoring and establishes decontamination corridor at Gate 2." + }, + { + "time": "08:20", + "actions": "USCG Sector Strike Team and EPA On-Scene Coordinator arrive on scene; initiate Unified Command setup at Fire Station 12." + }, + { + "time": "08:45", + "actions": "Marine Spill Response Vessel River Guardian deploys 1,000 feet of hard containment boom at MM 41.5 (Boom Site 1)." + }, + { + "time": "09:15", + "actions": "Planned: Entry Team 1 to enter Hot Zone under Level B PPE to secure Tank #4 bottom manifold valve." + }, + { + "time": "10:00", + "actions": "Planned: Deploy secondary sorbent deflection boom array at Boom Site 2 (MM 40.2) upstream of drinking water intake." + }, + { + "time": "11:30", + "actions": "Planned: Vacuum truck operations commence skimmer recovery of pooled styrene at storm drain outfall." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain M. Ross (USCG Unified Command IC)", + "OSC E. Vance (EPA Co-IC)", + "Chief R. Henderson (County Fire IC)", + "J. Mercer (Apex Facility RP IC)" + ], + "safety_officer": "Lt. Commander David Miller", + "public_information_officer": "Elena Gomez (County OEM)", + "liaison_officer": "Captain Arthur Pendelton (State Police)", + "operations_section_chief": "Battalion Chief Marcus Brody", + "planning_section_chief": "Sarah L. Jenkins", + "logistics_section_chief": "Karen Albright", + "finance_administration_section_chief": "Robert Sterling", + "additional_positions": [ + { + "position": "Hazmat Group Supervisor", + "name": "Captain Donald Kross (Hazmat 1)" + }, + { + "position": "Waterborne Ops Branch Director", + "name": "Lt. Timothy Vance (USCG)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Hazmat Unit", + "resource_identifier": "HM-01", + "date_time_ordered": "08/13/2026 07:20", + "eta": "N/A", + "arrived": true, + "notes": "Conducted initial air monitoring; operating Hot Zone entry." + }, + { + "resource": "Fire Engine", + "resource_identifier": "ENG-12", + "date_time_ordered": "08/13/2026 07:15", + "eta": "N/A", + "arrived": true, + "notes": "Securing perimeter and providing fire standby protection." + }, + { + "resource": "USCG Strike Team", + "resource_identifier": "USCG-ST-04", + "date_time_ordered": "08/13/2026 07:40", + "eta": "N/A", + "arrived": true, + "notes": "Assisting with Unified Command and waterborne ops supervision." + }, + { + "resource": "Spill Response Vessel", + "resource_identifier": "RV-RG-02", + "date_time_ordered": "08/13/2026 08:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 1,000 ft containment boom at Boom Site 1." + }, + { + "resource": "Vacuum Truck Unit", + "resource_identifier": "VAC-99", + "date_time_ordered": "08/13/2026 08:30", + "eta": "08/13/2026 10:45", + "arrived": false, + "notes": "En route to perform liquid product recovery at outfall." + }, + { + "resource": "Air Monitoring Unit", + "resource_identifier": "AMR-03", + "date_time_ordered": "08/13/2026 08:15", + "eta": "08/13/2026 09:45", + "arrived": false, + "notes": "Transporting high-sensitivity PID and Jerome mercury/VOC analyzers." + } + ] +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics201_5.json b/benchmark/datasets/ground_truth/ics201_5.json new file mode 100644 index 00000000..b99d02e0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_5.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_incident_number": "USCG-EPA-R6-2026-1014", + "3_date_time_initiated": { + "date": "10/14/2026", + "time": "11:20" + }, + "4_map_sketch": { + "total_area_of_operations": "2.5-mile radius centered around Berth 42 of the Houston Ship Channel Tank Terminal.", + "incident_site_area": "Tanker Berth 42 (MM 51.2, West Bank), where a manifold failure on chemical parcel tanker M/T Stolt Synergy released concentrated 98% sulfuric acid.", + "impacted_areas": "500-yard perimeter along Berth 42 exhibiting localized water discoloration and acidic vapor mist, extending 400 yards downwind across adjacent industrial docks.", + "threatened_areas": "San Jacinto Battleground Historic Site 1.2 miles down-river and Channelview Residential Neighborhood 2.0 miles downwind to the North-Northwest.", + "overflight_results": "USCG Air Station Houston Helicopter 6512 confirmed a localized 150-yard plume dissipating in the ship channel with continuous water sampling underway.", + "trajectories": "Acidic vapor mist tracking North-Northwest at 8 mph; waterborne runoff moving with ebb tide East-Southeast at 1.2 knots.", + "impacted_shorelines": "400 yards of concrete bulkhead and wooden fender piling at Berth 42.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Bravo at Jacintoport Terminal, Spill Boom Site 1, Command Post at USCG Sector Houston-Galveston, Exclusion Zone boundaries (1,000 ft radius), and ship channel navigation safety zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 10:50 CDT on 10/14/2026, a high-pressure discharge hose burst during offloading operations at Berth 42, spilling approximately 4,200 gallons of 98% sulfuric acid into secondary containment and the ship channel, generating a dense corrosive acid mist cloud.", + "health_and_safety_hazards": "Severe skin necrosis upon contact, permanent ocular damage, pulmonary edema from acid mist inhalation, extreme heat reaction when mixed with water, and slip hazards on degraded berth decking.", + "necessary_measures": "Enforce 1,000-foot Exclusion Zone; Level A chemical suit protection with SCBA for Hot Zone entries; Level B protective suits with SCBA for Warm Zone support; continuous pH water sampling and real-time acid mist sensor monitoring; mandatory dual-stage lime neutralization decontamination wash at Gate 4." + }, + "6_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 13:00" + }, + "7_current_and_planned_objectives": [ + "Enforce a 1,000-foot Exclusion Zone around Berth 42 and secure vessel offloading by 12:00 hours.", + "Complete neutralization of deck spill using sodium bicarbonate slurry by 14:00 hours.", + "Deploy chemical neutralization boom array at Berth 42 slip by 13:30 hours.", + "Monitor atmospheric acid mist levels along Channelview perimeter to ensure public safety by 15:00 hours.", + "Transition to Unified Command (USCG, EPA, TCEQ, RP) by 12:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "11:20", + "actions": "Initial emergency response initiated; Port Authority Fire Engines 81 and 82 respond; 1,000-foot safety perimeter set up." + }, + { + "time": "11:45", + "actions": "Sector Command established by Captain H. Vance; Houston Ship Channel traffic restricted to one-way slow speed." + }, + { + "time": "12:15", + "actions": "Port Hazmat Team 1 arrives on scene to establish pH monitoring grid and set up neutralization decon corridor." + }, + { + "time": "12:50", + "actions": "USCG Strike Team arrives; initiates shipboard inspection and containment audit." + }, + { + "time": "13:15", + "actions": "Stolt Tankers RP response vessel deploys chemical sorbent boom around M/T Stolt Synergy." + }, + { + "time": "14:00", + "actions": "Planned: Entry Team Alpha under Level A PPE executes emergency valve shutdown on shipboard manifold." + }, + { + "time": "14:30", + "actions": "Planned: Begin high-volume sodium bicarbonate dry-powder application on vessel deck." + }, + { + "time": "16:00", + "actions": "Planned: Perform secondary overflight to confirm complete plume dissipation." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "safety_officer": "Commander Thomas Blake", + "public_information_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)", + "operations_section_chief": "Battalion Chief A. Ross", + "planning_section_chief": "Commander Richard Croft", + "logistics_section_chief": "Sandra Keller", + "finance_administration_section_chief": "Wayne Miller", + "additional_positions": [ + { + "position": "Chemical Hazards Specialist", + "name": "Dr. V. Patel" + }, + { + "position": "Vessel Boarding Group Supervisor", + "name": "Lt. J. Thorne" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Port Authority Hazmat Team", + "resource_identifier": "HZM-PORT-1", + "date_time_ordered": "10/14/2026 11:25", + "eta": "N/A", + "arrived": true, + "notes": "Executing pH sampling grid and Hot Zone entry prep." + }, + { + "resource": "Port Fire Engine", + "resource_identifier": "ENG-81", + "date_time_ordered": "10/14/2026 11:20", + "eta": "N/A", + "arrived": true, + "notes": "Securing 1,000 ft landward perimeter and foam standby." + }, + { + "resource": "USCG Gulf Strike Team", + "resource_identifier": "USCG-GST-02", + "date_time_ordered": "10/14/2026 11:50", + "eta": "N/A", + "arrived": true, + "notes": "Supervising vessel entry protocol and safety verification." + }, + { + "resource": "Chemical Response Vessel", + "resource_identifier": "RV-AC-01", + "date_time_ordered": "10/14/2026 12:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed chemical sorbent boom around vessel berth." + }, + { + "resource": "Lime Neutralization Truck Unit", + "resource_identifier": "NEUT-TRK-05", + "date_time_ordered": "10/14/2026 12:30", + "eta": "10/14/2026 14:15", + "arrived": false, + "notes": "Delivering 10 tons of dry sodium bicarbonate slurry." + }, + { + "resource": "Mobile Air Sampling Van", + "resource_identifier": "AIR-SAM-02", + "date_time_ordered": "10/14/2026 12:10", + "eta": "10/14/2026 13:45", + "arrived": false, + "notes": "Transporting real-time SO3/acid mist photoionization sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_6.json b/benchmark/datasets/ground_truth/ics201_6.json new file mode 100644 index 00000000..d22b858a --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_6.json @@ -0,0 +1,144 @@ +{ + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_incident_number": "NTSB-EPA-R10-2026-1102", + "3_date_time_initiated": { + "date": "11/02/2026", + "time": "04:45" + }, + "4_map_sketch": { + "total_area_of_operations": "5.0-mile radius centered around BNSF Railway Milepost 218.4 near Skykomish, WA.", + "incident_site_area": "BNSF Rail Milepost 218.4; 5 freight cars derailed including pressurized tank car DOT-105J500W (BNSF-77402) containing liquefied chlorine gas with damaged protective valve housing.", + "impacted_areas": "1,500-foot vapor dispersion zone along the rail right-of-way and adjacent State Route 2.", + "threatened_areas": "Town of Skykomish (1.8 miles West downwind) and South Fork Skykomish River salmon spawning habitat (300 feet South).", + "overflight_results": "WSP FLIR Helicopter WSP-AIR-2 confirmed a localized green-yellow chlorine cloud hugging the ravine floor and drifting West-Northwest at 4 mph.", + "trajectories": "Airborne toxic plume tracking West-Northwest along State Route 2 corridor at 4 mph; zero liquid chemical runoff entering waterways.", + "impacted_shorelines": "None; ground contamination restricted to northern ravine embankment slopes.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Charlie at Stevens Pass Maintenance Yard, Mobile Command Post at Skykomish School District Gym, 1.5-mile Exclusion Zone boundary, and State Route 2 traffic closure checkpoints." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 04:15 PST on 11/02/2026, a mountain derailment caused 5 freight cars to leave the tracks. Tank car BNSF-77402 sustained severe dome housing damage, resulting in a low-rate pressurized vapor release of toxic chlorine at approximately 15 lbs/min. State Route 2 was closed and mandatory evacuation issued for 250 residents of Skykomish.", + "health_and_safety_hazards": "Severe pulmonary edema risk from chlorine gas inhalation, ocular and skin chemical burns, acute respiratory collapse, mountain hypothermia (ambient temp 34°F), and steep terrain slip hazards.", + "necessary_measures": "Enforce 1.5-mile Exclusion Zone; mandatory Level A vapor-tight encapsulated suits with SCBA for Hot Zone entry personnel; Level B suits for Warm Zone support; continuous electrochemical chlorine sensor monitoring at CP baselines; mandatory indoor shelter-in-place for outer perimeters; heated wet-decontamination trailer wash at Staging Area Charlie." + }, + "6_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 07:15" + }, + "7_current_and_planned_objectives": [ + "Complete mandatory evacuation of Skykomish township within 1.5-mile radius by 07:30 hours.", + "Perform Hot Zone entry to apply Emergency B-Kit capping assembly on chlorine railcar dome by 09:30 hours.", + "Establish continuous multi-point perimeter air monitoring along State Route 2 by 08:00 hours.", + "Secure rail right-of-way and establish Unified Command by 07:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "04:45", + "actions": "Initial alarm dispatch; Skykomish Fire and King County Sheriff units respond, closing State Route 2 at Milepost 215." + }, + { + "time": "05:15", + "actions": "Local Command established by Chief D. Olson; mandatory evacuation sirens activated across Skykomish." + }, + { + "time": "05:50", + "actions": "Regional Hazmat Team 3 and BNSF Response Team arrived on scene to initiate perimeter air testing." + }, + { + "time": "06:30", + "actions": "WSP FLIR helicopter completed thermal overflight mapping plume dispersion." + }, + { + "time": "07:00", + "actions": "Unified Command established at Skykomish School Gym with EPA Region 10 and BNSF RP." + }, + { + "time": "08:15", + "actions": "Planned: Entry Team 1 under Level A PPE initiates B-Kit capping tool installation on railcar valve dome." + }, + { + "time": "09:30", + "actions": "Planned: Perform pressure test and seal verification of capping assembly." + }, + { + "time": "11:00", + "actions": "Planned: Complete environmental health review to evaluate lifting outer zone evacuation orders." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "safety_officer": "Captain Marcus Vance", + "public_information_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)", + "operations_section_chief": "Battalion Chief T. Higgins", + "planning_section_chief": "Lt. Colonel Alan Vance", + "logistics_section_chief": "David Sterling", + "finance_administration_section_chief": "Patricia Ross", + "additional_positions": [ + { + "position": "Rail Hazmat Specialist", + "name": "G. Peterson (BNSF)" + }, + { + "position": "Evacuation Group Supervisor", + "name": "Sgt. H. Lin (KCSO)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "King County Hazmat 3", + "resource_identifier": "HZM-33", + "date_time_ordered": "11/02/2026 05:00", + "eta": "N/A", + "arrived": true, + "notes": "Preparing Level A Entry Team 1 and Emergency B-Kit capping assembly." + }, + { + "resource": "Skykomish Engine 11", + "resource_identifier": "ENG-11", + "date_time_ordered": "11/02/2026 04:45", + "eta": "N/A", + "arrived": true, + "notes": "Securing State Route 2 East closure checkpoint." + }, + { + "resource": "BNSF Emergency Response Unit", + "resource_identifier": "BNSF-ER-01", + "date_time_ordered": "11/02/2026 05:15", + "eta": "N/A", + "arrived": true, + "notes": "On scene with railcar specialized capping kits." + }, + { + "resource": "WSP Aviation FLIR Helicopter", + "resource_identifier": "WSP-AIR-2", + "date_time_ordered": "11/02/2026 05:30", + "eta": "N/A", + "arrived": true, + "notes": "Completed thermal imagery mapping of plume drift." + }, + { + "resource": "Mobile Decontamination Trailer", + "resource_identifier": "DECON-04", + "date_time_ordered": "11/02/2026 05:45", + "eta": "11/02/2026 08:30", + "arrived": false, + "notes": "En route to establish warm water decon at Staging Area Charlie." + }, + { + "resource": "Hazmat Air Monitoring Recon", + "resource_identifier": "AIR-MON-10", + "date_time_ordered": "11/02/2026 06:00", + "eta": "11/02/2026 07:45", + "arrived": false, + "notes": "En route with multi-gas chlorine sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_7.json b/benchmark/datasets/ground_truth/ics201_7.json new file mode 100644 index 00000000..99dcd09e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_7.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_incident_number": "CalOES-EPA-R9-2026-0518", + "3_date_time_initiated": { + "date": "05/18/2026", + "time": "13:10" + }, + "4_map_sketch": { + "total_area_of_operations": "4.0-mile radius including Trans-California Pipeline MM 87.3, Sawpit Canyon, and Silverwood Reservoir basin.", + "incident_site_area": "Valve Station 12 along Trans-California Pipeline MM 87.3 in Sawpit Canyon, where a 12-inch pressurized crude line ruptured.", + "impacted_areas": "800 yards of Sawpit Canyon creek bed carrying crude oil towards Silverwood Lake, with a 30-acre surface oil sheen in reservoir South arm.", + "threatened_areas": "Mojave Water Agency Municipal Pumping Plant (1.5 miles North) and Silverwood Lake State Recreation Area campgrounds (0.8 miles West).", + "overflight_results": "Cal FIRE Recon Aircraft 240 confirmed heavy black crude pooling in Sawpit Canyon and a 400-yard wide oil ribbon entering Silverwood Lake.", + "trajectories": "Waterborne crude oil slick moving North at 1.0 knot towards main reservoir basin; airborne volatile organic cloud tracking Southeast at 7 mph into canyon slopes.", + "impacted_shorelines": "1.2 miles of rocky reservoir shoreline and marsh vegetation along Sawpit Canyon inlet.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Delta at Silverwood Lake Marina, Incident Command Post at Hesperia Fire Station 30, 1,000-foot Exclusion Zone perimeter, Deflection Boom Sites A and B, and municipal water intake protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 12:40 PDT on 05/18/2026, a hillside land movement ruptured a 12-inch crude oil pipeline at MM 87.3, discharging approximately 12,000 gallons of heavy crude oil into Sawpit Canyon creek before automated valves shut off flow. The oil flowed downstream into Silverwood Reservoir, threatening southern California drinking water supplies.", + "health_and_safety_hazards": "Toxic benzene vapor inhalation, volatile organic compound exposure, acute flammability (flash point 65°F), wildfire ignition risk in dry brush, slip-and-fall hazards on oily terrain, and severe heat exhaustion (ambient temp 94°F).", + "necessary_measures": "Mandatory 1,000-foot Exclusion Zone; Level B PPE with SCBA for initial Hot Zone creek entry; Level C PPE with organic vapor respirators for shoreline booming teams; continuous PID air monitoring downwind; mandatory wildland fire standby team with AFFF foam; establishment of dual-basin wash decontamination at Marina Ramp 2." + }, + "6_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 15:30" + }, + "7_current_and_planned_objectives": [ + "Enforce 1,000-foot Exclusion Zone and secure pipeline isolation by 14:00 hours.", + "Deploy 2,000 feet of containment boom across Sawpit Canyon inlet by 16:30 hours to prevent oil migration to water intake.", + "Evacuate Silverwood Lake State Recreation Area campgrounds by 15:00 hours.", + "Initiate skimmer boat oil recovery in South arm of reservoir by 17:00 hours.", + "Establish Unified Command (Cal OES, EPA R9, USFS, San Bernardino County Fire, Pipeline RP) by 14:30 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "13:10", + "actions": "Alarm dispatch; San Bernardino County Fire Engine 30 and USFS Crew 4 respond to set up initial safety perimeter." + }, + { + "time": "13:40", + "actions": "Local Command established by Chief M. Thorne; Trans-California Pipeline operators confirm remote valve isolation at MM 85 and MM 90." + }, + { + "time": "14:15", + "actions": "State Parks Rangers complete full evacuation of Silverwood Lake campgrounds (150 visitors evacuated)." + }, + { + "time": "14:45", + "actions": "Cal OES and EPA OSC arrive on scene; Unified Command established at Station 30." + }, + { + "time": "15:15", + "actions": "Clean Harbors Spill Response Vessel deploys 1,200 feet of hard boom at Sawpit Inlet (Boom Site A)." + }, + { + "time": "16:00", + "actions": "Planned: Deploy secondary sorbent deflection boom array at Boom Site B upstream of Mojave Water Intake." + }, + { + "time": "17:00", + "actions": "Planned: Vacuum skimmer vessel Lake Clean 1 initiates surface crude extraction in South arm." + }, + { + "time": "18:30", + "actions": "Planned: Complete initial shoreline oiling assessment along Sawpit Canyon inlet." + } + ], + "9_current_organization": { + "incident_commanders": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "safety_officer": "Captain Gregory Hall", + "public_information_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)", + "operations_section_chief": "Battalion Chief Kevin Ross", + "planning_section_chief": "Captain Rachel Brooks", + "logistics_section_chief": "Megan Taylor", + "finance_administration_section_chief": "Jason Wu", + "additional_positions": [ + { + "position": "Environmental Unit Leader", + "name": "Dr. L. Arispe (USFS)" + }, + { + "position": "Waterborne Recovery Supervisor", + "name": "Captain B. Walsh (Clean Harbors)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "San Bernardino Hazmat Engine", + "resource_identifier": "ENG-30", + "date_time_ordered": "05/18/2026 13:10", + "eta": "N/A", + "arrived": true, + "notes": "Operating perimeter vapor monitoring and fire standby." + }, + { + "resource": "USFS Wildland Handcrew", + "resource_identifier": "CREW-04", + "date_time_ordered": "05/18/2026 13:15", + "eta": "N/A", + "arrived": true, + "notes": "Clearing brush and building containment dikes in Sawpit Canyon." + }, + { + "resource": "State Parks Ranger Unit", + "resource_identifier": "PARK-12", + "date_time_ordered": "05/18/2026 13:20", + "eta": "N/A", + "arrived": true, + "notes": "Completed campground evacuations and road closures." + }, + { + "resource": "Clean Harbors Spill Vessel", + "resource_identifier": "RV-LC-01", + "date_time_ordered": "05/18/2026 14:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 1,200 ft hard boom at Sawpit Canyon inlet." + }, + { + "resource": "Heavy Vacuum Skimmer Boat", + "resource_identifier": "SKIM-02", + "date_time_ordered": "05/18/2026 14:30", + "eta": "05/18/2026 16:45", + "arrived": false, + "notes": "En route from San Pedro for reservoir skimmer recovery." + }, + { + "resource": "Air Monitoring Recon Unit", + "resource_identifier": "AIR-MON-08", + "date_time_ordered": "05/18/2026 14:10", + "eta": "05/18/2026 15:45", + "arrived": false, + "notes": "En route with photoionization detectors and benzene gas sensors." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_8.json b/benchmark/datasets/ground_truth/ics201_8.json new file mode 100644 index 00000000..50fed48e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_8.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Delaware Bay Container Vessel Collision & Fuel Oil Spill", + "2_incident_number": "USCG-D5-2026-0629", + "3_date_time_initiated": { + "date": "06/29/2026", + "time": "22:40" + }, + "4_map_sketch": { + "total_area_of_operations": "6.0-mile radius centered at Delaware Bay Shipping Channel Buoy 14 near Lewes, DE.", + "incident_site_area": "Delaware Bay Channel Buoy 14 (38.85°N, 75.12°W), where container vessel M/V Atlantic Voyager collided with a bulk carrier, breaching fuel tank #3 port side.", + "impacted_areas": "3.5-mile long heavy oil sheen extending East-Southeast across the bay channel towards Cape Henlopen State Park.", + "threatened_areas": "Prime Hook National Wildlife Refuge (4.0 miles Northwest) and Cape Henlopen Tidal Salt Marshes (2.5 miles South).", + "overflight_results": "USCG HC-144 Ocean Sentry aircraft confirmed a 300-yard wide slick of Intermediate Fuel Oil (IFO 380) drifting with the flood tide.", + "trajectories": "Waterborne oil slick migrating East-Southeast at 2.2 knots toward Cape Henlopen under coastal winds of 14 knots from West-Northwest.", + "impacted_shorelines": "2.0 miles of outer sandy beach and dune lines along Cape Henlopen State Park.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Echo at Lewes Ferry Terminal, Incident Command Post at USCG Sector Delaware Bay Headquarters, 1,000-yard Maritime Safety Zone, Boom Sites 1, 2, and 3, and wildlife protection zones." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 22:15 EDT on 06/29/2026, container vessel M/V Atlantic Voyager collided with anchored bulk carrier M/V Pacific Trader near Buoy 14. Port fuel tank #3 breached, discharging approximately 25,000 gallons of heavy Intermediate Fuel Oil (IFO 380) before internal fuel transfer halted the release.", + "health_and_safety_hazards": "Hydrocarbon vapor exposure (benzene/H2S), direct dermal toxicity, severe slip hazards on oily vessel hulls and shorelines, nighttime over-water operations, water drowning risk, and wildlife exposure risks.", + "necessary_measures": "Establish 1,000-yard Maritime Safety Zone; mandatory Level C PPE with PFDs and half-mask organic vapor respirators for maritime responders; continuous H2S and PID air monitoring on response vessels; compulsory work-rest hydration cycles; mobile vessel decontamination station established at Lewes Pier." + }, + "6_prepared_by": { + "name": "Commander James Vance", + "position_title": "Planning Section Chief", + "signature": "James Vance", + "date_time": "06/30/2026 01:30" + }, + "7_current_and_planned_objectives": [ + "Secure maritime safety perimeter and stabilize damaged vessel by 01:00 hours.", + "Deploy protective booming across Cape Henlopen salt marsh inlets by 04:00 hours.", + "Initiate offshore skimming operations using MSRC oil recovery vessels by 05:30 hours.", + "Activate wildlife rescue and rehab operations with Tri-State Bird Rescue by 06:00 hours.", + "Establish Unified Command (USCG, EPA, Delaware DNREC, M/V Atlantic Voyager RP) by 02:00 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "22:40", + "actions": "Initial alarm dispatch; USCG Station Lewes response boats 45601 and 45602 respond to establish maritime safety zone." + }, + { + "time": "23:15", + "actions": "Local Command established by Captain P. Hayes; Sector Delaware Bay requests MSRC oil spill response vessel mobilization." + }, + { + "time": "23:50", + "actions": "M/V Atlantic Voyager crew completes internal fuel transfer, halting active discharge from fuel tank #3." + }, + { + "time": "00:30", + "actions": "USCG HC-144 overflight completes IR sensor oil slick mapping." + }, + { + "time": "01:15", + "actions": "Delaware DNREC response team arrives at Lewes Command Post; Unified Command established." + }, + { + "time": "03:00", + "actions": "Planned: MSRC Response Vessel Delaware Responder deploys 2,500 feet of ocean containment boom at Buoy 14." + }, + { + "time": "04:30", + "actions": "Planned: Deploy sorbent diversion boom at Cape Henlopen Inlet (Boom Site 2)." + }, + { + "time": "06:00", + "actions": "Planned: Tri-State Bird Rescue team commences shoreline wildlife search along Cape Henlopen beaches." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Captain P. Hayes (USCG Unified Command IC)", + "OSC T. Gallagher (EPA R3)", + "Secretary A. Lawson (Delaware DNREC)", + "H. Lindemann (Atlantic Shipping RP IC)" + ], + "safety_officer": "Lt. Commander Mark Reynolds", + "public_information_officer": "Chief Petty Officer Kelly Adams", + "liaison_officer": "Inspector R. Thorne (DNREC)", + "operations_section_chief": "Commander Frank Miller", + "planning_section_chief": "Commander James Vance", + "logistics_section_chief": "Laura Bennett", + "finance_administration_section_chief": "Charles Foster", + "additional_positions": [ + { + "position": "Scientific Support Coordinator", + "name": "Dr. E. Sullivan (NOAA)" + }, + { + "position": "Wildlife Branch Director", + "name": "Dr. C. Jenkins (Tri-State)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "USCG Response Boat Medium", + "resource_identifier": "RBM-45601", + "date_time_ordered": "06/29/2026 22:40", + "eta": "N/A", + "arrived": true, + "notes": "Maintaining 1,000-yard safety zone around damaged vessel." + }, + { + "resource": "USCG Station Lewes RBM", + "resource_identifier": "RBM-45602", + "date_time_ordered": "06/29/2026 22:40", + "eta": "N/A", + "arrived": true, + "notes": "Conducting water sampling and perimeter safety watch." + }, + { + "resource": "MSRC Spill Vessel Delaware Responder", + "resource_identifier": "RV-MSRC-01", + "date_time_ordered": "06/29/2026 23:00", + "eta": "N/A", + "arrived": true, + "notes": "Deployed 2,500 ft ocean boom and preparing offshore skimmer." + }, + { + "resource": "USCG Ocean Sentry Aircraft", + "resource_identifier": "USCG-HC144", + "date_time_ordered": "06/29/2026 23:10", + "eta": "N/A", + "arrived": true, + "notes": "Completed IR slick mapping and overflight tracking." + }, + { + "resource": "Mobile Wildlife Rehabilitation Trailer", + "resource_identifier": "WILD-REHAB-1", + "date_time_ordered": "06/30/2026 00:15", + "eta": "06/30/2026 05:45", + "arrived": false, + "notes": "En route to establish bird cleaning station at Lewes Pier." + }, + { + "resource": "Shoreline Cleanup Strike Team", + "resource_identifier": "SCAT-TEAM-1", + "date_time_ordered": "06/30/2026 00:45", + "eta": "06/30/2026 06:30", + "arrived": false, + "notes": "Mobilizing for Cape Henlopen beach oil assessment." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics201_9.json b/benchmark/datasets/ground_truth/ics201_9.json new file mode 100644 index 00000000..c182deff --- /dev/null +++ b/benchmark/datasets/ground_truth/ics201_9.json @@ -0,0 +1,145 @@ +{ + "1_incident_name": "Oakridge Industrial Park Anhydrous Ammonia Release", + "2_incident_number": "EPA-R2-2026-0905", + "3_date_time_initiated": { + "date": "09/05/2026", + "time": "08:15" + }, + "4_map_sketch": { + "total_area_of_operations": "2.0-mile radius centered at Cold Storage Logistics Building 7, Edison, NJ.", + "incident_site_area": "Mechanical Room 3 on roof deck of Cold Storage Logistics Building 7 (100 Industrial Parkway), where an 8-inch high-pressure ammonia chiller line fractured.", + "impacted_areas": "400-yard vapor dispersion plume encompassing Building 7 loading dock and northern section of Industrial Parkway.", + "threatened_areas": "Meadowlands Residential Community (0.9 miles East downwind) and Edison Transit Center (1.2 miles Southeast).", + "overflight_results": "Edison Police Drone Recon Unit DRONE-PD-1 confirmed a dense white fog cloud hovering over Roof Mechanical Room 3 and drifting East-Northeast at 5 mph.", + "trajectories": "Airborne toxic anhydrous ammonia cloud migrating East-Northeast at 5 mph; zero liquid chemical product entering storm drainage systems.", + "impacted_shorelines": "None; contamination restricted to localized industrial ground surfaces and rooftop structures.", + "graphics_and_symbology": "North oriented vertically, depicting Staging Area Foxtrot at Edison High School parking lot, Command Post at Edison Fire Station 4, 1,000-foot Exclusion Zone boundary, and Industrial Parkway traffic control points." + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "At 07:45 EDT on 09/05/2026, a mechanical vibration failure severed an 8-inch liquid refrigeration line inside Mechanical Room 3 at Cold Storage Logistics, releasing approximately 3,000 lbs of anhydrous ammonia gas. Building 7 was evacuated immediately (85 employees), and a 1,000-foot perimeter isolation zone was established.", + "health_and_safety_hazards": "Severe chemical asphyxiation, acute pulmonary edema, permanent ocular injury, cryogenic chemical freeze burns, flammability risk in enclosed spaces (LEL 15-28%), and physical fall hazards from roof decking.", + "necessary_measures": "Mandatory 1,000-foot Exclusion Zone; Level A encapsulated vapor suits with SCBA for all roof entry personnel; Level B suits for ground backup teams; continuous electrochemical ammonia air monitoring at perimeter boundaries; high-volume water curtain deployment for vapor knockdown; mandatory dual-basin chemical wash decontamination corridor at Building 7 main entrance." + }, + "6_prepared_by": { + "name": "Captain Michael Vance", + "position_title": "Planning Section Chief", + "signature": "Michael Vance", + "date_time": "09/05/2026 10:30" + }, + "7_current_and_planned_objectives": [ + "Enforce 1,000-foot Exclusion Zone and isolate Building 7 HVAC intake by 08:30 hours.", + "Perform roof entry under Level A PPE to manually isolate main receiver valve by 11:00 hours.", + "Deploy high-volume water curtain monitoring nozzles to suppress cloud drift by 09:30 hours.", + "Perform continuous downwind air monitoring along Meadowlands border by 09:00 hours.", + "Establish Unified Command (Edison Fire, NJ DEP, EPA R2, Facility RP) by 08:45 hours." + ], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "08:15", + "actions": "Initial alarm dispatch; Edison Fire Engines 4 and 6 respond and assist Building 7 evacuation." + }, + { + "time": "08:35", + "actions": "Local Command established by Chief E. Sullivan; Industrial Parkway closed at Kilmer Road." + }, + { + "time": "09:00", + "actions": "Middlesex County Hazmat Team 2 arrived to set up perimeter air monitoring grid and water fog curtain monitors." + }, + { + "time": "09:40", + "actions": "NJ DEP and EPA Region 2 OSC arrived on scene; Unified Command established at Station 4." + }, + { + "time": "10:15", + "actions": "Drone recon flight confirmed water curtains reduced downwind cloud density by 60%." + }, + { + "time": "11:00", + "actions": "Planned: Entry Team 1 under Level A PPE executes emergency roof entry to isolate main refrigeration manifold." + }, + { + "time": "12:00", + "actions": "Planned: Initiate mechanical ventilation of Building 7 interior through charcoal scrubber array." + }, + { + "time": "13:30", + "actions": "Planned: Conduct clearance air sampling inside Building 7 prior to facility re-entry." + } + ], + "9_current_organization": { + "incident_commanders": [ + "Chief E. Sullivan (Edison Fire IC)", + "OSC H. Miller (EPA R2)", + "Inspector G. Ross (NJ DEP)", + "T. Jenkins (Cold Storage RP IC)" + ], + "safety_officer": "Captain Robert Hayes", + "public_information_officer": "Amanda Walsh (Edison OEM)", + "liaison_officer": "Lt. D. Kincaid (Middlesex PD)", + "operations_section_chief": "Battalion Chief Brian O'Connor", + "planning_section_chief": "Captain Michael Vance", + "logistics_section_chief": "Karen Miller", + "finance_administration_section_chief": "Steven Zhang", + "additional_positions": [ + { + "position": "Refrigeration Systems Specialist", + "name": "C. Bauer (Cold Storage RP)" + }, + { + "position": "Air Monitoring Leader", + "name": "Lt. V. Patel (County Hazmat)" + } + ] + }, + "10_resource_summary": [ + { + "resource": "Middlesex County Hazmat 2", + "resource_identifier": "HZM-MC-02", + "date_time_ordered": "09/05/2026 08:20", + "eta": "N/A", + "arrived": true, + "notes": "Operating water curtains and preparing Level A roof entry." + }, + { + "resource": "Edison Fire Engine 4", + "resource_identifier": "ENG-04", + "date_time_ordered": "09/05/2026 08:15", + "eta": "N/A", + "arrived": true, + "notes": "Supplying high-pressure water to fog monitors for vapor suppression." + }, + { + "resource": "Edison Ladder Truck 2", + "resource_identifier": "LADDER-02", + "date_time_ordered": "09/05/2026 08:15", + "eta": "N/A", + "arrived": true, + "notes": "Positioned for roof access and aerial water stream deployment." + }, + { + "resource": "Police Drone Recon Unit", + "resource_identifier": "DRONE-PD-1", + "date_time_ordered": "09/05/2026 08:30", + "eta": "N/A", + "arrived": true, + "notes": "Conducting continuous thermal and aerial monitoring of plume drift." + }, + { + "resource": "Mobile Mechanical Scrubber Unit", + "resource_identifier": "VENT-SCRUB-3", + "date_time_ordered": "09/05/2026 09:15", + "eta": "09/05/2026 11:45", + "arrived": false, + "notes": "En route to perform positive pressure building scrubbing." + }, + { + "resource": "High-Sensitivity Air Monitoring Truck", + "resource_identifier": "AIR-TRK-07", + "date_time_ordered": "09/05/2026 09:00", + "eta": "09/05/2026 10:15", + "arrived": false, + "notes": "En route to monitor downwind Meadowlands residential perimeter." + } + ] +} diff --git a/benchmark/datasets/ground_truth/ics202_1.json b/benchmark/datasets/ground_truth/ics202_1.json new file mode 100644 index 00000000..f6fc17d9 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_1.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift.", + "Deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen.", + "Complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading.", + "Establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety.", + "Finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity." + ], + "4_operational_period_command_emphasis": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery.", + "general_situational_awareness": "The local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "EPA Plume Trajectory Analysis Sheet", + "CSX Tank Car Cargo Manifest" + ] + }, + "7_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief J. Thomas", + "signature": "Chief J. Thomas", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "1" + } +} \ No newline at end of file diff --git a/benchmark/datasets/ground_truth/ics202_2.json b/benchmark/datasets/ground_truth/ics202_2.json new file mode 100644 index 00000000..c0af4580 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_2.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain 100% responder compliance with SCBA Level B PPE within the 500-foot Exclusion Zone throughout the night shift.", + "Complete installation of 1,000 feet of sorbent containment boom array across Blackwood River at Boom Site 2 by 22:00 hours to protect downstream municipal water intake.", + "Complete hot-tap product evacuation of damaged pipeline section by 02:00 hours to stop active anhydrous ammonia leakage.", + "Maintain continuous automated PID perimeter air monitoring downwind along Route 104 with telemetry reporting to CP every 30 minutes.", + "Finalize Operational Period 2 IAP shift plan and resource request documentation by 04:30 hours." + ], + "4_operational_period_command_emphasis": "Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when ammonia vapors hug low ground.", + "general_situational_awareness": "Clear night skies, temperatures dropping to 58°F, and wind shifting from East-Northeast to North at 4 mph. Low-lying fog expected near riverbanks between 02:00 and 06:00 hours. High vigilance required for slick riverbank terrain and reduced nighttime visibility.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Mobile Command Post Briefing Trailer at Station 12", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Ammonia Air Dispersion Model Map", + "Pipeline Shutoff Schematic" + ] + }, + "7_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief R. Henderson", + "signature": "Chief R. Henderson", + "date_time": "08/13/2026 17:15" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_3.json b/benchmark/datasets/ground_truth/ics202_3.json new file mode 100644 index 00000000..786d9948 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_3.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_objectives": [ + "Ensure zero safety incidents by enforcing mandatory Level A chemical suit entry procedures for all vessel deck operations.", + "Apply 10 tons of dry sodium bicarbonate slurry to neutralize pooled acid on Berth 42 deck by 12:00 hours.", + "Maintain 2,000 feet of chemical sorbent boom around M/T Stolt Synergy and conduct hourly river pH sampling to ensure zero downstream acid migration.", + "Complete vessel hull structural integrity audit and offloading manifold pressure test by 15:00 hours.", + "Conduct continuous public air monitoring along Channelview community boundary and issue real-time safety updates by 17:00 hours." + ], + "4_operational_period_command_emphasis": "Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Personnel safety during high-temperature Level A suit operations takes absolute priority over operational speed.", + "general_situational_awareness": "Daytime temperatures reaching 88°F with high humidity (78%), creating severe heat stress conditions for suit technicians. Winds from Southeast at 9 mph. Work-rest cycles of 20 minutes active entry followed by 40 minutes hydration/cooling are strictly mandated.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "USCG Sector Houston Command Center Safety Office", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": true, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "TCEQ Water Quality Monitoring Plan", + "Vessel Cargo Stowage Plan" + ] + }, + "7_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "8_approved_by_incident_commander": { + "name": "Captain H. Vance", + "signature": "Captain H. Vance", + "date_time": "10/14/2026 06:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_4.json b/benchmark/datasets/ground_truth/ics202_4.json new file mode 100644 index 00000000..3848dd01 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_4.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Enforce strict Level A PPE compliance and buddy system protocols during all night operations around derailment site.", + "Complete installation and torque testing of Emergency B-Kit capping assembly on chlorine tank car BNSF-77402 by 22:00 hours.", + "Maintain perimeter chlorine air monitoring at 15-minute intervals along State Route 2 evacuation boundary.", + "Operate heated decontamination trailer and warm hydration station continuously at Staging Area Charlie.", + "Formulate environmental sampling plan and morning shift briefing package by 04:00 hours." + ], + "4_operational_period_command_emphasis": "Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions. Ensure continuous warm water decon availability to prevent suit icing.", + "general_situational_awareness": "Overcast skies with mountain temperatures dropping to 28°F overnight; snow flurries expected after 01:00 hours. Light winds from West at 3 mph drifting toward ravine floor. Icing hazards on rail ballast and steep access slopes. High hypothermia hazard for standing security personnel.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Skykomish School Gym Operations Briefing Room", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": true, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Railcar Capping Procedure Manual", + "Chlorine Gas Toxicity Chart" + ] + }, + "7_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Captain E. Miller", + "signature": "Captain E. Miller", + "date_time": "11/02/2026 17:15" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_5.json b/benchmark/datasets/ground_truth/ics202_5.json new file mode 100644 index 00000000..92a48fcc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_5.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_objectives": [ + "Maintain 100% safety record with zero heat injuries or toxic vapor exposures during night shift operations.", + "Secure 2,000 feet of containment boom across Sawpit Canyon inlet and maintain skimmer vessel recovery in South arm until 23:00 hours.", + "Complete pipeline mechanical clamp repair on 12-inch main line at MM 87.3 by 02:00 hours.", + "Perform hourly water sampling upstream of Mojave Water Intake facility to ensure non-detectable hydrocarbon levels.", + "Prepare Day 2 Operational Period Incident Action Plan and resource requests by 04:30 hours." + ], + "4_operational_period_command_emphasis": "Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and life-vest compliance at all times.", + "general_situational_awareness": "Evening temperature cooling to 70°F with calm winds under 5 mph. Mountain lions and nocturnal wildlife reported near Sawpit Canyon inlet. Flashlight illumination required along all shoreline walking paths due to steep, oily riprap.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Incident Command Post at Hesperia Fire Station 30", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": false, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Silverwood Lake Water Sampling Map", + "Pipeline Repair Safety Plan" + ] + }, + "7_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "8_approved_by_incident_commander": { + "name": "Chief M. Thorne", + "signature": "Chief M. Thorne", + "date_time": "05/18/2026 17:00" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics202_6.json b/benchmark/datasets/ground_truth/ics202_6.json new file mode 100644 index 00000000..3ba25549 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics202_6.json @@ -0,0 +1,49 @@ +{ + "ics_202_ground_truth": { + "1_incident_name": "Oakridge Industrial Park Anhydrous Ammonia Release", + "2_operational_period": { + "date_from": "09/05/2026", + "date_to": "09/06/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_objectives": [ + "Enforce strict Level A/B PPE protocols and 1,000-foot Exclusion Zone control throughout daytime mechanical ventilation operations.", + "Complete charcoal scrubber building ventilation of Building 7 interior to reduce interior ammonia concentrations below 15 ppm by 13:00 hours.", + "Conduct full structural and piping stress inspection of Roof Mechanical Room 3 by 15:00 hours.", + "Perform interior clearance air sampling across all 4 building quadrants by 17:00 hours.", + "Submit final environmental decontamination report to NJ DEP and lift perimeter road closures by 18:30 hours." + ], + "4_operational_period_command_emphasis": "Focus operational efforts on safe indoor ventilation and structural validation. Ensure air monitoring teams continuously verify downwind residential air quality before discharging building exhaust.", + "general_situational_awareness": "Mostly sunny with daytime high of 82°F. Winds from West-Southwest at 7 mph. Thermal updrafts on Building 7 roof deck require secure tie-offs for all entry personnel.", + "5_site_safety_plan_required": true, + "approved_site_safety_plans_located_at": "Edison Fire Station 4 Command Post Conference Room", + "6_incident_action_plan_attachments": { + "ics_203": true, + "ics_204": true, + "ics_205": true, + "ics_205a": true, + "ics_206": true, + "ics_207": false, + "ics_208": true, + "map_chart": true, + "weather_forecast_tides_currents": true, + "other_attachments": [ + "Building 7 Ventilation Engineering Plan", + "NJ DEP Air Quality Standard Sheet" + ] + }, + "7_prepared_by": { + "name": "Captain Michael Vance", + "position_title": "Planning Section Chief", + "signature": "Michael Vance", + "date_time": "09/05/2026 06:00" + }, + "8_approved_by_incident_commander": { + "name": "Chief E. Sullivan", + "signature": "Chief E. Sullivan", + "date_time": "09/05/2026 06:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_1.json b/benchmark/datasets/ground_truth/ics203_1.json new file mode 100644 index 00000000..f049792d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_1.json @@ -0,0 +1,125 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "deputy": "Assistant Chief M. Reynolds", + "safety_officer": "Captain R. Mendez", + "public_info_officer": "A. Cho (County OEM)", + "liaison_officer": "Inspector G. Sims (SO)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US EPA Region 4", + "name": "OSC K. Lawson" + }, + { + "agency_organization": "CSX Railroad", + "name": "V. Patel" + }, + { + "agency_organization": "Whispering Pines Police Dept", + "name": "Commander B. Davis" + } + ], + "5_planning_section": { + "chief": "Marcus Vance", + "deputy": "L. Sullivan", + "resources_unit": "T. Jenkins", + "situation_unit": "E. Brooks", + "documentation_unit": "H. Martinez", + "demobilization_unit": "R. Sterling", + "technical_specialists": [ + { + "specialty": "Rail Tank Car Specialist", + "name": "D. Kowalski" + }, + { + "specialty": "Air Dispersion Modeler", + "name": "Dr. A. Chen" + } + ] + }, + "6_logistics_section": { + "chief": "K. Dunavan", + "deputy": "P. Wright", + "support_branch": { + "director": "J. Myers", + "supply_unit": "S. Taylor", + "facilities_unit": "C. Adams", + "ground_support_unit": "M. Evans" + }, + "service_branch": { + "director": "B. Foster", + "communications_unit": "R. Lee", + "medical_unit": "Dr. N. Howard", + "food_unit": "L. King" + } + }, + "7_operations_section": { + "chief": "B. Reynolds", + "deputy": "Lt. Commander D. Miller", + "staging_area": "Staging Area Alpha (County Fairgrounds)", + "branches": [ + { + "branch_name": "Hazardous Materials Branch", + "branch_director": "Lt. T. Kincaid", + "deputy": "Sgt. R. Hall", + "divisions_groups": [ + { + "identifier": "Hazmat Entry Group", + "supervisor": "Capt. P. Gomez" + }, + { + "identifier": "Decontamination Group", + "supervisor": "Lt. S. Baker" + } + ] + }, + { + "branch_name": "Fire Suppression Branch", + "branch_director": "Batt. Chief E. Walters", + "deputy": "Capt. J. Miller", + "divisions_groups": [ + { + "identifier": "Division A (Rail Right-of-Way)", + "supervisor": "Capt. D. Ross" + }, + { + "identifier": "Water Supply Group", + "supervisor": "Lt. M. Turner" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. H. Nelson" + } + }, + "8_finance_administration_section": { + "chief": "Robert Sterling", + "deputy": "A. Patel", + "time_unit": "C. White", + "procurement_unit": "G. Scott", + "comp_claims_unit": "E. Green", + "cost_unit": "W. Harris" + }, + "9_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_2.json b/benchmark/datasets/ground_truth/ics203_2.json new file mode 100644 index 00000000..238a218f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_2.json @@ -0,0 +1,125 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Chief R. Vance (Blackwood Fire Dept)", + "Officer M. Ross (State EPA)", + "J. Vance (Blackwood Pipeline Co. RP)" + ], + "deputy": "Deputy Chief L. Morgan", + "safety_officer": "Captain L. Hayes", + "public_info_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US EPA Region 5", + "name": "OSC R. Brooks" + }, + { + "agency_organization": "Blackwood County Sheriff", + "name": "Sheriff T. Higgins" + }, + { + "agency_organization": "Valley Water Authority", + "name": "Director P. Simmons" + } + ], + "5_planning_section": { + "chief": "Sarah L. Jenkins", + "deputy": "T. Bradley", + "resources_unit": "A. Patel", + "situation_unit": "C. Webb", + "documentation_unit": "Dr. H. Thorne", + "demobilization_unit": "M. Brody", + "technical_specialists": [ + { + "specialty": "Pipeline Integrity Specialist", + "name": "E. Vance" + }, + { + "specialty": "Chemical Toxicology Specialist", + "name": "Dr. S. Ray" + } + ] + }, + "6_logistics_section": { + "chief": "T. Bradley", + "deputy": "W. Miller", + "support_branch": { + "director": "G. Kross", + "supply_unit": "H. Lin", + "facilities_unit": "R. Sterling", + "ground_support_unit": "D. Miller" + }, + "service_branch": { + "director": "S. Norris", + "communications_unit": "K. Albright", + "medical_unit": "Dr. T. Vance", + "food_unit": "A. Cho" + } + }, + "7_operations_section": { + "chief": "D. Kowalski", + "deputy": "Lt. Timothy Vance", + "staging_area": "Staging Area Bravo (Westside High School)", + "branches": [ + { + "branch_name": "Pipeline Isolation Branch", + "branch_director": "Capt. Donald Kross", + "deputy": "Lt. C. Webb", + "divisions_groups": [ + { + "identifier": "Hot Zone Entry Group", + "supervisor": "Lt. R. Mendez" + }, + { + "identifier": "Vapor Suppression Group", + "supervisor": "Capt. A. Ross" + } + ] + }, + { + "branch_name": "River Spill Containment Branch", + "branch_director": "Commander Thomas Blake", + "deputy": "Inspector G. Sims", + "divisions_groups": [ + { + "identifier": "Booming Division 1", + "supervisor": "Lt. J. Thorne" + }, + { + "identifier": "Water Intake Protection Group", + "supervisor": "Capt. B. Walsh" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "A. Patel", + "deputy": "Robert Sterling", + "time_unit": "J. Mercer", + "procurement_unit": "Laura Martinez", + "comp_claims_unit": "Inspector R. Sterling", + "cost_unit": "Sandra Keller" + }, + "9_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_3.json b/benchmark/datasets/ground_truth/ics203_3.json new file mode 100644 index 00000000..e23d680f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_3.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "deputy": "Commander T. Blake", + "safety_officer": "Commander Thomas Blake", + "public_info_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "Texas Commission on Environmental Quality", + "name": "Inspector R. Sterling" + }, + { + "agency_organization": "Stolt Tankers Shipping", + "name": "K. Lindqvist" + }, + { + "agency_organization": "Port of Houston Pilots", + "name": "Captain M. Brody" + } + ], + "5_planning_section": { + "chief": "Commander Richard Croft", + "deputy": "Lt. J. Thorne", + "resources_unit": "Dr. V. Patel", + "situation_unit": "Sandra Keller", + "documentation_unit": "Wayne Miller", + "demobilization_unit": "Battalion Chief A. Ross", + "technical_specialists": [ + { + "specialty": "Chemical Hazard Specialist", + "name": "Dr. V. Patel" + }, + { + "specialty": "Marine Salvage Engineer", + "name": "E. Miller" + } + ] + }, + "6_logistics_section": { + "chief": "Sandra Keller", + "deputy": "Wayne Miller", + "support_branch": { + "director": "Capt. H. Nelson", + "supply_unit": "C. White", + "facilities_unit": "G. Scott", + "ground_support_unit": "E. Green" + }, + "service_branch": { + "director": "W. Harris", + "communications_unit": "J. Myers", + "medical_unit": "Dr. A. Chen", + "food_unit": "S. Taylor" + } + }, + "7_operations_section": { + "chief": "Battalion Chief A. Ross", + "deputy": "Lt. J. Thorne", + "staging_area": "Staging Area Bravo (Jacintoport Terminal)", + "branches": [ + { + "branch_name": "Vessel Salvage & Entry Branch", + "branch_director": "Lt. J. Thorne", + "deputy": "Capt. P. Gomez", + "divisions_groups": [ + { + "identifier": "Deck Neutralization Group", + "supervisor": "Lt. S. Baker" + }, + { + "identifier": "Manifold Isolation Team", + "supervisor": "Capt. D. Ross" + } + ] + }, + { + "branch_name": "Ship Channel Protection Branch", + "branch_director": "Capt. Donald Kross", + "deputy": "Lt. M. Turner", + "divisions_groups": [ + { + "identifier": "Water Quality Sampling Division", + "supervisor": "Dr. S. Ray" + }, + { + "identifier": "Booming Operations Group", + "supervisor": "Capt. B. Walsh" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Commander T. Blake" + } + }, + "8_finance_administration_section": { + "chief": "Wayne Miller", + "deputy": "Sandra Keller", + "time_unit": "C. Adams", + "procurement_unit": "M. Evans", + "comp_claims_unit": "B. Foster", + "cost_unit": "R. Lee" + }, + "9_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_4.json b/benchmark/datasets/ground_truth/ics203_4.json new file mode 100644 index 00000000..ec6d638e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_4.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "deputy": "Inspector G. Peterson", + "safety_officer": "Captain Marcus Vance", + "public_info_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "Washington State Department of Transportation", + "name": "Jennifer Hayes" + }, + { + "agency_organization": "BNSF Railway", + "name": "G. Peterson" + }, + { + "agency_organization": "King County Sheriff's Office", + "name": "Sgt. H. Lin" + } + ], + "5_planning_section": { + "chief": "Lt. Colonel Alan Vance", + "deputy": "David Sterling", + "resources_unit": "Patricia Ross", + "situation_unit": "Battalion Chief T. Higgins", + "documentation_unit": "Sgt. H. Lin", + "demobilization_unit": "G. Peterson", + "technical_specialists": [ + { + "specialty": "Rail Hazmat Specialist", + "name": "G. Peterson" + }, + { + "specialty": "Toxic Gas Modeler", + "name": "Dr. H. Thorne" + } + ] + }, + "6_logistics_section": { + "chief": "David Sterling", + "deputy": "Patricia Ross", + "support_branch": { + "director": "L. King", + "supply_unit": "J. Myers", + "facilities_unit": "S. Taylor", + "ground_support_unit": "C. Adams" + }, + "service_branch": { + "director": "M. Evans", + "communications_unit": "B. Foster", + "medical_unit": "Dr. N. Howard", + "food_unit": "R. Lee" + } + }, + "7_operations_section": { + "chief": "Battalion Chief T. Higgins", + "deputy": "Sgt. H. Lin", + "staging_area": "Staging Area Charlie (Stevens Pass Yard)", + "branches": [ + { + "branch_name": "Hazmat Capping Branch", + "branch_director": "Capt. P. Gomez", + "deputy": "Lt. S. Baker", + "divisions_groups": [ + { + "identifier": "Railcar Entry Group 1", + "supervisor": "Capt. D. Ross" + }, + { + "identifier": "Decontamination Group", + "supervisor": "Lt. M. Turner" + } + ] + }, + { + "branch_name": "Evacuation & Security Branch", + "branch_director": "Sgt. H. Lin", + "deputy": "Deputy S. Kowalski", + "divisions_groups": [ + { + "identifier": "Highway 2 Control Division", + "supervisor": "Capt. H. Nelson" + }, + { + "identifier": "Skykomish Evacuation Group", + "supervisor": "Sgt. R. Hall" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "Patricia Ross", + "deputy": "David Sterling", + "time_unit": "W. Harris", + "procurement_unit": "C. White", + "comp_claims_unit": "G. Scott", + "cost_unit": "E. Green" + }, + "9_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics203_5.json b/benchmark/datasets/ground_truth/ics203_5.json new file mode 100644 index 00000000..b807bacd --- /dev/null +++ b/benchmark/datasets/ground_truth/ics203_5.json @@ -0,0 +1,126 @@ +{ + "ics_203_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "deputy": "Commander F. Miller", + "safety_officer": "Captain Gregory Hall", + "public_info_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "US Forest Service", + "name": "Dr. L. Arispe" + }, + { + "agency_organization": "Mojave Water Agency", + "name": "Engineer K. Ross" + }, + { + "agency_organization": "Clean Harbors Environmental", + "name": "Captain B. Walsh" + } + ], + "5_planning_section": { + "chief": "Captain Rachel Brooks", + "deputy": "Megan Taylor", + "resources_unit": "Jason Wu", + "situation_unit": "Battalion Chief Kevin Ross", + "documentation_unit": "Dr. L. Arispe", + "demobilization_unit": "Captain B. Walsh", + "technical_specialists": [ + { + "specialty": "Environmental Unit Leader", + "name": "Dr. L. Arispe" + }, + { + "specialty": "Hydrological Flow Specialist", + "name": "Dr. S. Ray" + } + ] + }, + "6_logistics_section": { + "chief": "Megan Taylor", + "deputy": "Jason Wu", + "support_branch": { + "director": "Capt. P. Gomez", + "supply_unit": "Lt. S. Baker", + "facilities_unit": "Capt. D. Ross", + "ground_support_unit": "Lt. M. Turner" + }, + "service_branch": { + "director": "Capt. H. Nelson", + "communications_unit": "Sgt. R. Hall", + "medical_unit": "Dr. N. Howard", + "food_unit": "L. King" + } + }, + "7_operations_section": { + "chief": "Battalion Chief Kevin Ross", + "deputy": "Captain B. Walsh", + "staging_area": "Staging Area Delta (Silverwood Lake Marina)", + "branches": [ + { + "branch_name": "Canyon Pipeline Repair Branch", + "branch_director": "Capt. Gregory Hall", + "deputy": "Lt. R. Mendez", + "divisions_groups": [ + { + "identifier": "Clamp Repair Group", + "supervisor": "Capt. A. Ross" + }, + { + "identifier": "Sawpit Containment Group", + "supervisor": "Lt. C. Webb" + } + ] + }, + { + "branch_name": "Reservoir Skimming & Booming Branch", + "branch_director": "Captain B. Walsh", + "deputy": "Lt. J. Thorne", + "divisions_groups": [ + { + "identifier": "South Arm Skimmer Division", + "supervisor": "Capt. B. Walsh" + }, + { + "identifier": "Water Intake Protection Group", + "supervisor": "Dr. L. Arispe" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "Capt. E. Walters" + } + }, + "8_finance_administration_section": { + "chief": "Jason Wu", + "deputy": "Megan Taylor", + "time_unit": "J. Myers", + "procurement_unit": "S. Taylor", + "comp_claims_unit": "C. Adams", + "cost_unit": "M. Evans" + }, + "9_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "1" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_1.json b/benchmark/datasets/ground_truth/ics204_1.json new file mode 100644 index 00000000..6fb91f7d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_1.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Hazardous Materials Branch", + "division": "Division A", + "group": "Hazmat Entry Group", + "staging_area": "Staging Area Alpha (County Fairgrounds)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "B. Reynolds", + "contact_number": "555-0192 / Ch 1" + }, + "branch_director": { + "name": "Lt. T. Kincaid", + "contact_number": "555-0144 / Ch 3" + }, + "division_group_supervisor": { + "name": "Capt. P. Gomez", + "contact_number": "555-0188 / Ch 4" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-01", + "leader": "Lt. T. Kincaid", + "number_of_persons": "6", + "contact": "Radio Ch 4 (462.550 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Hot Zone Gate 1 by 18:30. Level B SCBA suits, PID air monitors, non-sparking aluminum tools." + }, + { + "resource_identifier": "DECON-02", + "leader": "Lt. S. Baker", + "number_of_persons": "4", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Establish wet-decon station at Warm Zone boundary Gate 2 by 18:45. Decon shower trailer with lime wash solution." + }, + { + "resource_identifier": "ENG-41", + "leader": "Capt. D. Ross", + "number_of_persons": "3", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Position at Warm Zone boundary for continuous AFFF foam blanket standby during tank car grounding." + } + ], + "6_work_assignments": "Execute Hot Zone entry into rail right-of-way to secure structural stabilization straps on derailed Car #14. Attach grounding cables to prevent static spark ignition. Perform real-time PID air monitoring around breached Benzene manifold and report VOC concentrations to Branch Director every 15 minutes.", + "7_special_instructions": "All entry personnel must wear Level B PPE with SCBA. Continuous air monitoring required; evacuate Hot Zone immediately if PID readings exceed 5 ppm Benzene or 10% LEL. Ensure formal decontamination shower prior to exiting Warm Zone.", + "8_communications": [ + { + "name_function": "Command Channel / Ops Chief", + "primary_contact": "Radio Ch 1 (154.280 MHz)" + }, + { + "name_function": "Hazmat Tactical Channel", + "primary_contact": "Radio Ch 4 (462.550 MHz)" + }, + { + "name_function": "Safety Officer Direct Line", + "primary_contact": "Cell: 555-0177" + }, + { + "name_function": "Medical Emergency Call", + "primary_contact": "Radio Ch 5 / Cell: 555-0199" + } + ], + "9_prepared_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_2.json b/benchmark/datasets/ground_truth/ics204_2.json new file mode 100644 index 00000000..e1cc69c0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_2.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Pipeline Isolation Branch", + "division": "Division B", + "group": "Hot Zone Entry Group", + "staging_area": "Staging Area Bravo (Westside High School)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "D. Kowalski", + "contact_number": "555-0210 / Ch 1" + }, + "branch_director": { + "name": "Capt. Donald Kross", + "contact_number": "555-0233 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. R. Mendez", + "contact_number": "555-0255 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-05", + "leader": "Lt. C. Webb", + "number_of_persons": "5", + "contact": "Radio Ch 3 (467.775 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Gate 2 by 18:15. Level A encapsulated SCBA suits, pneumatic pipe clamp, thermal imaging camera." + }, + { + "resource_identifier": "ENG-12", + "leader": "Capt. A. Ross", + "number_of_persons": "4", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Deploy unmanned fog nozzles at 500 ft perimeter to maintain water curtains over airborne ammonia plume." + }, + { + "resource_identifier": "WT-03", + "leader": "Lt. M. Turner", + "number_of_persons": "2", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Supply continuous water feed to Engine 12 monitors." + } + ], + "6_work_assignments": "Enter Hot Zone at Valve Station 14-B under Level A protection to execute manual hot-tap isolation on the 8-inch pressurized line. Secure bypass manifold to stop active liquid ammonia discharge. Maintain water curtains downwind during valve mechanical manipulation.", + "7_special_instructions": "Level A PPE mandatory for entry team. Maintain 2-person backup entry team in Level A at Warm Zone line. If ammonia concentration exceeds 300 ppm IDLH at Warm Zone baseline, sound emergency withdrawal horn (3 long blasts).", + "8_communications": [ + { + "name_function": "Command / Operations", + "primary_contact": "Radio Ch 1 (153.830 MHz)" + }, + { + "name_function": "Pipeline Tactical", + "primary_contact": "Radio Ch 3 (467.775 MHz)" + }, + { + "name_function": "Safety Officer Line", + "primary_contact": "Cell: 555-0288" + }, + { + "name_function": "Decon Line", + "primary_contact": "Radio Ch 6 (462.625 MHz)" + } + ], + "9_prepared_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_3.json b/benchmark/datasets/ground_truth/ics204_3.json new file mode 100644 index 00000000..9d4cb76e --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_3.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Vessel Salvage & Entry Branch", + "division": "Division C (Berth 42)", + "group": "Deck Neutralization Group", + "staging_area": "Staging Area Bravo (Jacintoport Terminal)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief A. Ross", + "contact_number": "555-0312 / Ch 1" + }, + "branch_director": { + "name": "Lt. J. Thorne", + "contact_number": "555-0344 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. S. Baker", + "contact_number": "555-0366 / Ch 4" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-PORT-1", + "leader": "Capt. P. Gomez", + "number_of_persons": "6", + "contact": "Radio Ch 4 (453.225 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Berth 42 Gangway by 07:30. Level A chemical suits, dry lime blower, pH test strips." + }, + { + "resource_identifier": "NEUT-TRK-05", + "leader": "Driver E. Green", + "number_of_persons": "2", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Position at Berth 42 apron. Supply dry sodium bicarbonate powder to deck entry team." + }, + { + "resource_identifier": "RV-AC-01", + "leader": "Capt. B. Walsh", + "number_of_persons": "4", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Conduct continuous water sampling and maintain acid sorbent boom around vessel hull." + } + ], + "6_work_assignments": "Board M/T Stolt Synergy under Level A protection. Apply dry sodium bicarbonate slurry over 4,200 gallons of pooled sulfuric acid on vessel deck until deck runoff pH stabilizes between 6.5 and 8.5. Inspect offloading manifold for structural stress.", + "7_special_instructions": "Strict 20-minute entry limit per technician due to heat stress (88°F/78% RH). Mandatory lime wash decon before unsuiting. Do not apply raw water directly to concentrated acid pool to prevent violent exothermic splattering.", + "8_communications": [ + { + "name_function": "Operations Command", + "primary_contact": "Radio Ch 1 (156.800 MHz / VHF 16)" + }, + { + "name_function": "Vessel Tactical", + "primary_contact": "Radio Ch 4 (453.225 MHz)" + }, + { + "name_function": "Port Safety Line", + "primary_contact": "Cell: 555-0399" + }, + { + "name_function": "Medical Standby", + "primary_contact": "Radio Ch 5 (462.600 MHz)" + } + ], + "9_prepared_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_4.json b/benchmark/datasets/ground_truth/ics204_4.json new file mode 100644 index 00000000..875ee3bc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_4.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Hazmat Capping Branch", + "division": "Division Ravine", + "group": "Railcar Entry Group 1", + "staging_area": "Staging Area Charlie (Stevens Pass Yard)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief T. Higgins", + "contact_number": "555-0411 / Ch 1" + }, + "branch_director": { + "name": "Capt. P. Gomez", + "contact_number": "555-0433 / Ch 2" + }, + "division_group_supervisor": { + "name": "Capt. D. Ross", + "contact_number": "555-0455 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "HZM-33", + "leader": "Lt. S. Baker", + "number_of_persons": "5", + "contact": "Radio Ch 3 (462.700 MHz)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Ravine Checkpoint by 18:30. Level A encapsulated suits, Emergency B-Kit capping assembly, pneumatic torque wrenches." + }, + { + "resource_identifier": "BNSF-ER-01", + "leader": "Spec. G. Peterson", + "number_of_persons": "3", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Provide railcar dome technical oversight and heavy rigging hardware." + }, + { + "resource_identifier": "DECON-04", + "leader": "Tech M. Turner", + "number_of_persons": "4", + "contact": "Radio Ch 2", + "reporting_location_special_equipment_remarks_notes_information": "Operate heated water decontamination trailer at Staging Area Charlie." + } + ], + "6_work_assignments": "Descend ravine to derailed railcar BNSF-77402 under Level A protection. Mount and torque Emergency B-Kit hood over damaged chlorine angle valve dome. Verify seal integrity using ammonia vapor swab test until zero leakage is detected.", + "7_special_instructions": "Freezing temperature hazard (28°F) and icy slopes. Suit entry teams limited to 30-minute rotations. Heated decon wash mandatory to prevent suit freeze-up. Continuous electrochemical chlorine monitoring required.", + "8_communications": [ + { + "name_function": "Command Channel", + "primary_contact": "Radio Ch 1 (154.400 MHz)" + }, + { + "name_function": "Capping Tactical", + "primary_contact": "Radio Ch 3 (462.700 MHz)" + }, + { + "name_function": "Safety Officer Line", + "primary_contact": "Cell: 555-0488" + }, + { + "name_function": "Evac Control Line", + "primary_contact": "Radio Ch 4 (467.550 MHz)" + } + ], + "9_prepared_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics204_5.json b/benchmark/datasets/ground_truth/ics204_5.json new file mode 100644 index 00000000..ccea5412 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics204_5.json @@ -0,0 +1,81 @@ +{ + "ics_204_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_branch_division_group_staging_area": { + "branch": "Reservoir Skimming & Booming Branch", + "division": "Division Reservoir South", + "group": "South Arm Skimmer Division", + "staging_area": "Staging Area Delta (Silverwood Lake Marina)" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "Battalion Chief Kevin Ross", + "contact_number": "555-0515 / Ch 1" + }, + "branch_director": { + "name": "Captain B. Walsh", + "contact_number": "555-0535 / Ch 2" + }, + "division_group_supervisor": { + "name": "Lt. J. Thorne", + "contact_number": "555-0560 / Ch 3" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "RV-LC-01", + "leader": "Capt. B. Walsh", + "number_of_persons": "4", + "contact": "Radio Ch 3 (156.550 MHz / VHF 11)", + "reporting_location_special_equipment_remarks_notes_information": "Report to Marina Slip 4 by 18:15. 1,200 ft hard boom, drum skimmer, high-intensity LED deck searchlights." + }, + { + "resource_identifier": "SKIM-02", + "leader": "Operator R. Mendez", + "number_of_persons": "3", + "contact": "Radio Ch 3", + "reporting_location_special_equipment_remarks_notes_information": "Operate heavy weir skimmer vessel in South arm pool. Pump oil to 5,000 gal floating bladder." + }, + { + "resource_identifier": "AIR-MON-08", + "leader": "Tech A. Ross", + "number_of_persons": "2", + "contact": "Radio Ch 4", + "reporting_location_special_equipment_remarks_notes_information": "Conduct continuous PID benzene air monitoring on boat deck and shoreline baseline." + } + ], + "6_work_assignments": "Deploy and anchor 1,200 feet of hard containment boom across Sawpit Canyon inlet to isolate reservoir. Operate drum skimmers and weir skimmer vessel SKIM-02 continuously throughout night shift to recover floating crude oil slick in South arm.", + "7_special_instructions": "Mandatory USCG-approved PFDs for all over-water personnel. Maintain vessel deck lighting during night operations. If PID readings exceed 1 ppm benzene, mandate half-mask APR organic vapor respirators.", + "8_communications": [ + { + "name_function": "Command Channel", + "primary_contact": "Radio Ch 1 (154.150 MHz)" + }, + { + "name_function": "Marine Tactical Channel", + "primary_contact": "Radio Ch 3 (156.550 MHz / VHF 11)" + }, + { + "name_function": "Safety Officer Direct", + "primary_contact": "Cell: 555-0588" + }, + { + "name_function": "Water Intake Guard", + "primary_contact": "Radio Ch 5 (462.575 MHz)" + } + ], + "9_prepared_by": { + "name": "Captain Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_1.json b/benchmark/datasets/ground_truth/ics205_1.json new file mode 100644 index 00000000..1053aaef --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_1.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_date_time_prepared": { + "date": "07/07/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone 1", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "TAC-1", + "assignment": "Unified Command / Section Chiefs", + "rx_frequency_n_or_w": "154.2800 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "159.4800 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "A", + "remarks": "Repeater 1 on Ridge Top" + }, + { + "zone_grp": "Zone 1", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "TAC-2", + "assignment": "Hazmat Entry Group", + "rx_frequency_n_or_w": "462.5500 N", + "rx_tone_nac": "114.8", + "tx_frequency_n_or_w": "467.5500 N", + "tx_tone_nac": "114.8", + "mode_a_d_or_m": "D", + "remarks": "Encrypted digital talkgroup" + }, + { + "zone_grp": "Zone 1", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "TAC-3", + "assignment": "Fire Suppression Branch", + "rx_frequency_n_or_w": "153.8300 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "153.8300 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "A", + "remarks": "Direct tactical simplex" + }, + { + "zone_grp": "Zone 1", + "channel_number": "4", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "SUPP-1", + "assignment": "Logistics & Decon", + "rx_frequency_n_or_w": "462.6250 N", + "rx_tone_nac": "123.0", + "tx_frequency_n_or_w": "462.6250 N", + "tx_tone_nac": "123.0", + "mode_a_d_or_m": "A", + "remarks": "Staging & decon trailer" + } + ], + "5_special_instructions": "All emergency traffic must use 'MAYDAY' call sign on Channel 1. Channel 2 encryption key loaded at Staging Area Alpha.", + "6_prepared_by": { + "name": "R. Lee", + "signature": "R. Lee", + "date_time": "07/07/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_2.json b/benchmark/datasets/ground_truth/ics205_2.json new file mode 100644 index 00000000..59c9b066 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_2.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_date_time_prepared": { + "date": "08/13/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone A", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "STATE-CMD", + "assignment": "Unified Command / Operations", + "rx_frequency_n_or_w": "153.8300 N", + "rx_tone_nac": "131.8", + "tx_frequency_n_or_w": "158.9700 N", + "tx_tone_nac": "131.8", + "mode_a_d_or_m": "A", + "remarks": "County Repeater 4" + }, + { + "zone_grp": "Zone A", + "channel_number": "2", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "HAZ-TAC-1", + "assignment": "Hot Zone Entry Group", + "rx_frequency_n_or_w": "467.7750 N", + "rx_tone_nac": "D023", + "tx_frequency_n_or_w": "467.7750 N", + "tx_tone_nac": "D023", + "mode_a_d_or_m": "D", + "remarks": "Intrinsically safe radios only" + }, + { + "zone_grp": "Zone A", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "BOOM-TAC", + "assignment": "River Booming Division", + "rx_frequency_n_or_w": "154.2800 N", + "rx_tone_nac": "131.8", + "tx_frequency_n_or_w": "154.2800 N", + "tx_tone_nac": "131.8", + "mode_a_d_or_m": "A", + "remarks": "Simplex marine/land link" + }, + { + "zone_grp": "Zone A", + "channel_number": "4", + "function": "Dispatch", + "channel_name_trunked_radio_system_talkgroup": "LAW-DISP", + "assignment": "Perimeter Evacuation / SO", + "rx_frequency_n_or_w": "460.1250 N", + "rx_tone_nac": "156.7", + "tx_frequency_n_or_w": "465.1250 N", + "tx_tone_nac": "156.7", + "mode_a_d_or_m": "D", + "remarks": "County Sheriff Dispatch" + } + ], + "5_special_instructions": "Only intrinsically safe portable radios (Class I, Div 1) permitted inside the 500-foot Exclusion Zone. Maintain 30-minute radio check-ins.", + "6_prepared_by": { + "name": "K. Albright", + "signature": "K. Albright", + "date_time": "08/13/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_3.json b/benchmark/datasets/ground_truth/ics205_3.json new file mode 100644 index 00000000..4844bd5f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_3.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_date_time_prepared": { + "date": "10/14/2026", + "time": "05:30" + }, + "3_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Port", + "channel_number": "16", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "VHF 16 / CMD", + "assignment": "USCG / Unified Command", + "rx_frequency_n_or_w": "156.8000 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.8000 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "International Maritime Distress & Command" + }, + { + "zone_grp": "Zone Port", + "channel_number": "11", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "VHF 11 / OPS", + "assignment": "Vessel Deck Entry Group", + "rx_frequency_n_or_w": "156.5500 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.5500 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "Ship-to-shore deck entry" + }, + { + "zone_grp": "Zone Port", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "PORT-HAZ", + "assignment": "Chemical Neutralization Team", + "rx_frequency_n_or_w": "453.2250 N", + "rx_tone_nac": "141.3", + "tx_frequency_n_or_w": "458.2250 N", + "tx_tone_nac": "141.3", + "mode_a_d_or_m": "D", + "remarks": "Port Fire Hazmat digital" + }, + { + "zone_grp": "Zone Port", + "channel_number": "5", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "MED-NET", + "assignment": "Medical & Decon Operations", + "rx_frequency_n_or_w": "462.6000 N", + "rx_tone_nac": "100.0", + "tx_frequency_n_or_w": "462.6000 N", + "tx_tone_nac": "100.0", + "mode_a_d_or_m": "A", + "remarks": "Decon trailer baseline" + } + ], + "5_special_instructions": "VHF Channel 16 reserved for Command & emergency traffic only. Deck entry team must maintain dual-watch on VHF 11 and Port-Haz Ch 4.", + "6_prepared_by": { + "name": "J. Myers", + "signature": "J. Myers", + "date_time": "10/14/2026 05:30" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_4.json b/benchmark/datasets/ground_truth/ics205_4.json new file mode 100644 index 00000000..5efc9bae --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_4.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_date_time_prepared": { + "date": "11/02/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Pass", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "WSP-CMD", + "assignment": "Unified Command / Operations", + "rx_frequency_n_or_w": "154.4000 N", + "rx_tone_nac": "103.5", + "tx_frequency_n_or_w": "159.0300 N", + "tx_tone_nac": "103.5", + "mode_a_d_or_m": "A", + "remarks": "Stevens Pass Mountain Repeater" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "CAPP-TAC", + "assignment": "Railcar Entry & Capping Team", + "rx_frequency_n_or_w": "462.7000 N", + "rx_tone_nac": "D074", + "tx_frequency_n_or_w": "467.7000 N", + "tx_tone_nac": "D074", + "mode_a_d_or_m": "D", + "remarks": "Digital cross-band repeater in ravine" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "EVAC-TAC", + "assignment": "Highway 2 & Evacuation Security", + "rx_frequency_n_or_w": "467.5500 N", + "rx_tone_nac": "114.8", + "tx_frequency_n_or_w": "467.5500 N", + "tx_tone_nac": "114.8", + "mode_a_d_or_m": "A", + "remarks": "Sheriff patrol direct" + }, + { + "zone_grp": "Zone Pass", + "channel_number": "6", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "LOG-SUPP", + "assignment": "Decon & Staging Charlie", + "rx_frequency_n_or_w": "151.6250 N", + "rx_tone_nac": "67.0", + "tx_frequency_n_or_w": "151.6250 N", + "tx_tone_nac": "67.0", + "mode_a_d_or_m": "A", + "remarks": "Staging Area Charlie link" + } + ], + "5_special_instructions": "Portable cross-band repeater deployed at ravine rim to ensure coverage inside mountain shadow. Cold-weather battery packs required for all handheld portables.", + "6_prepared_by": { + "name": "B. Foster", + "signature": "B. Foster", + "date_time": "11/02/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205_5.json b/benchmark/datasets/ground_truth/ics205_5.json new file mode 100644 index 00000000..2429db4f --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205_5.json @@ -0,0 +1,76 @@ +{ + "ics_205_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_date_time_prepared": { + "date": "05/18/2026", + "time": "16:00" + }, + "3_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "Zone Lake", + "channel_number": "1", + "function": "Command", + "channel_name_trunked_radio_system_talkgroup": "FIRE-CMD", + "assignment": "Unified Command / Station 30", + "rx_frequency_n_or_w": "154.1500 N", + "rx_tone_nac": "146.2", + "tx_frequency_n_or_w": "158.8500 N", + "tx_tone_nac": "146.2", + "mode_a_d_or_m": "A", + "remarks": "County Station 30 Repeater" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "3", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "MARINE-11", + "assignment": "Reservoir Skimmer Fleet", + "rx_frequency_n_or_w": "156.5500 W", + "rx_tone_nac": "None", + "tx_frequency_n_or_w": "156.5500 W", + "tx_tone_nac": "None", + "mode_a_d_or_m": "A", + "remarks": "VHF Channel 11 Marine Simplex" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "4", + "function": "Tactical", + "channel_name_trunked_radio_system_talkgroup": "PIPE-TAC", + "assignment": "Canyon Pipeline Repair Group", + "rx_frequency_n_or_w": "462.5750 N", + "rx_tone_nac": "D115", + "tx_frequency_n_or_w": "467.5750 N", + "tx_tone_nac": "D115", + "mode_a_d_or_m": "D", + "remarks": "Sawpit Canyon tactical link" + }, + { + "zone_grp": "Zone Lake", + "channel_number": "5", + "function": "Support", + "channel_name_trunked_radio_system_talkgroup": "PARK-SUPP", + "assignment": "State Parks & Intake Guard", + "rx_frequency_n_or_w": "151.4000 N", + "rx_tone_nac": "146.2", + "tx_frequency_n_or_w": "151.4000 N", + "tx_tone_nac": "146.2", + "mode_a_d_or_m": "A", + "remarks": "Water Agency & Park Rangers" + } + ], + "5_special_instructions": "All marine vessels must maintain continuous watch on VHF Channel 11. Emergency Mayday protocol monitored by Command on Ch 1.", + "6_prepared_by": { + "name": "Sgt. R. Hall", + "signature": "R. Hall", + "date_time": "05/18/2026 16:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_1.json b/benchmark/datasets/ground_truth/ics205a_1.json new file mode 100644 index 00000000..0fb968ff --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_1.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "Incident Commander (County Fire)", + "name": "Chief J. Thomas", + "methods_of_contact": "Cell: 555-0101 / Radio Ch 1 / Vehicle: FIRE-CMD-1" + }, + { + "incident_assigned_position": "Co-Incident Commander (State DEQ)", + "name": "S. Albright", + "methods_of_contact": "Cell: 555-0102 / Radio Ch 1" + }, + { + "incident_assigned_position": "Co-Incident Commander (CSX RP)", + "name": "D. Miller", + "methods_of_contact": "Cell: 555-0103 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain R. Mendez", + "methods_of_contact": "Cell: 555-0177 / Radio Ch 1 & 4" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "B. Reynolds", + "methods_of_contact": "Cell: 555-0192 / Radio Ch 1" + }, + { + "incident_assigned_position": "Hazmat Branch Director", + "name": "Lt. T. Kincaid", + "methods_of_contact": "Cell: 555-0144 / Radio Ch 3 & 4 / Vehicle: HZM-01" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Marcus Vance", + "methods_of_contact": "Cell: 555-0150 / CP Desk Ext: 104" + } + ], + "4_prepared_by": { + "name": "R. Lee", + "position_title": "Communications Unit Leader", + "signature": "R. Lee", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_2.json b/benchmark/datasets/ground_truth/ics205a_2.json new file mode 100644 index 00000000..0a876ffc --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_2.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "Incident Commander (Fire)", + "name": "Chief R. Vance", + "methods_of_contact": "Cell: 555-0201 / Radio Ch 1" + }, + { + "incident_assigned_position": "Incident Commander (State EPA)", + "name": "Officer M. Ross", + "methods_of_contact": "Cell: 555-0202 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain L. Hayes", + "methods_of_contact": "Cell: 555-0288 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "D. Kowalski", + "methods_of_contact": "Cell: 555-0210 / Radio Ch 1" + }, + { + "incident_assigned_position": "Pipeline Branch Director", + "name": "Capt. Donald Kross", + "methods_of_contact": "Cell: 555-0233 / Radio Ch 2 / Vehicle: HAZ-1" + }, + { + "incident_assigned_position": "Entry Group Supervisor", + "name": "Lt. R. Mendez", + "methods_of_contact": "Cell: 555-0255 / Radio Ch 3 / Vehicle: HZM-05" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Sarah L. Jenkins", + "methods_of_contact": "Cell: 555-0250 / CP Ext: 201" + } + ], + "4_prepared_by": { + "name": "K. Albright", + "position_title": "Communications Unit Leader", + "signature": "K. Albright", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_3.json b/benchmark/datasets/ground_truth/ics205a_3.json new file mode 100644 index 00000000..1be12fe0 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_3.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "USCG Unified Commander", + "name": "Captain H. Vance", + "methods_of_contact": "Cell: 555-0301 / VHF Ch 16 / Vessel: CG-45601" + }, + { + "incident_assigned_position": "EPA Co-Incident Commander", + "name": "OSC M. Reynolds", + "methods_of_contact": "Cell: 555-0302 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Commander Thomas Blake", + "methods_of_contact": "Cell: 555-0399 / VHF Ch 16" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief A. Ross", + "methods_of_contact": "Cell: 555-0312 / VHF Ch 16" + }, + { + "incident_assigned_position": "Vessel Salvage Branch Director", + "name": "Lt. J. Thorne", + "methods_of_contact": "Cell: 555-0344 / VHF Ch 11" + }, + { + "incident_assigned_position": "Deck Entry Supervisor", + "name": "Lt. S. Baker", + "methods_of_contact": "Cell: 555-0366 / Radio Ch 4 / Vehicle: HZM-PORT-1" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Commander Richard Croft", + "methods_of_contact": "Cell: 555-0350 / Sector Ext: 305" + } + ], + "4_prepared_by": { + "name": "J. Myers", + "position_title": "Communications Unit Leader", + "signature": "J. Myers", + "date_time": "10/14/2026 06:15" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_4.json b/benchmark/datasets/ground_truth/ics205a_4.json new file mode 100644 index 00000000..b1107d23 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_4.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "WSP Incident Commander", + "name": "Captain E. Miller", + "methods_of_contact": "Cell: 555-0401 / Radio Ch 1 / Unit: WSP-100" + }, + { + "incident_assigned_position": "EPA R10 Co-Commander", + "name": "OSC R. Brooks", + "methods_of_contact": "Cell: 555-0402 / Radio Ch 1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain Marcus Vance", + "methods_of_contact": "Cell: 555-0488 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief T. Higgins", + "methods_of_contact": "Cell: 555-0411 / Radio Ch 1" + }, + { + "incident_assigned_position": "Hazmat Capping Branch Director", + "name": "Capt. P. Gomez", + "methods_of_contact": "Cell: 555-0433 / Radio Ch 2 & 3" + }, + { + "incident_assigned_position": "Railcar Entry Supervisor", + "name": "Capt. D. Ross", + "methods_of_contact": "Cell: 555-0455 / Radio Ch 3 / Vehicle: HZM-33" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Lt. Colonel Alan Vance", + "methods_of_contact": "Cell: 555-0450 / CP Desk: Gym-1" + } + ], + "4_prepared_by": { + "name": "B. Foster", + "position_title": "Communications Unit Leader", + "signature": "B. Foster", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics205a_5.json b/benchmark/datasets/ground_truth/ics205a_5.json new file mode 100644 index 00000000..afaec8e3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics205a_5.json @@ -0,0 +1,55 @@ +{ + "ics_205a_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "EPA R9 Incident Commander", + "name": "OSC C. Martinez", + "methods_of_contact": "Cell: 555-0501 / Radio Ch 1" + }, + { + "incident_assigned_position": "SBCo Fire Incident Commander", + "name": "Chief M. Thorne", + "methods_of_contact": "Cell: 555-0502 / Radio Ch 1 / Vehicle: FIRE-SB-1" + }, + { + "incident_assigned_position": "Safety Officer", + "name": "Captain Gregory Hall", + "methods_of_contact": "Cell: 555-0588 / Radio Ch 1" + }, + { + "incident_assigned_position": "Operations Section Chief", + "name": "Battalion Chief Kevin Ross", + "methods_of_contact": "Cell: 555-0515 / Radio Ch 1" + }, + { + "incident_assigned_position": "Marine Skimming Branch Director", + "name": "Captain B. Walsh", + "methods_of_contact": "Cell: 555-0535 / Radio Ch 3 (VHF 11) / Vessel: RV-LC-01" + }, + { + "incident_assigned_position": "Canyon Repair Group Supervisor", + "name": "Lt. J. Thorne", + "methods_of_contact": "Cell: 555-0560 / Radio Ch 4 / Vehicle: CREW-04" + }, + { + "incident_assigned_position": "Planning Section Chief", + "name": "Captain Rachel Brooks", + "methods_of_contact": "Cell: 555-0550 / CP Desk Ext: 403" + } + ], + "4_prepared_by": { + "name": "Sgt. R. Hall", + "position_title": "Communications Unit Leader", + "signature": "R. Hall", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_1.json b/benchmark/datasets/ground_truth/ics206_1.json new file mode 100644 index 00000000..9b8566b6 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_1.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Staging Area Alpha First Aid Station", + "location": "Whispering Pines Creek Staging Area", + "contact_number_frequency": "462.5500 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Forward Decon Medical Post", + "location": "Derailment Site Perimeter West", + "contact_number_frequency": "467.7750 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "County Emergency Medical Services Unit 41", + "location": "Whispering Pines Staging Area", + "contact_number_frequency": "(555) 234-8901 / Ch 1", + "paramedic_service": true + }, + { + "name": "Pine Valley Volunteer Fire & Rescue Ambulance 12", + "location": "Station 12 Base", + "contact_number_frequency": "(555) 234-8902 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "LifeFlight Air Medical Helicopter", + "location": "Regional Trauma Center Helipad", + "contact_number_frequency": "(555) 999-4321 / VHF 155.340 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Whispering Pines Regional Medical Center", + "address_latitude_longitude": "100 Medical Center Drive, Whispering Pines, 42.1234 N, 73.5678 W", + "contact_number_frequency": "(555) 789-0100 / Med Channel 3", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "State University Burn & Trauma Center", + "address_latitude_longitude": "500 University Ave, Capital City, 42.3500 N, 73.8000 W", + "contact_number_frequency": "(555) 789-0900 / Med Channel 5", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "In the event of a medical emergency or chemical exposure during night operations, responders must immediately notify the Incident Commander and Medical Unit Leader over Command Channel 1. All entry personnel exposed to Benzene must undergo full gross and technical decontamination at the Forward Decon Medical Post before transport. For critical life safety, LifeFlight Air Medical is on standby for immediate evacuation from Landing Zone Alpha located adjacent to Staging Area Alpha.", + "7_prepared_by": { + "name": "Dr. Evelyn Reed", + "position_title": "Medical Unit Leader", + "signature": "Evelyn Reed", + "date_time": "07/07/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Captain Thomas Wright", + "signature": "Thomas Wright", + "date_time": "07/07/2026 17:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_2.json b/benchmark/datasets/ground_truth/ics206_2.json new file mode 100644 index 00000000..b216b947 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_2.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Blackwood Command Post Medical Station", + "location": "Station 12 Briefing Trailer", + "contact_number_frequency": "453.2125 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Hot Zone Emergency Decon Aid Station", + "location": "Pipeline Gate 4 Access Point", + "contact_number_frequency": "458.2125 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Blackwood County EMS Unit 10", + "location": "Station 12 Staging Area", + "contact_number_frequency": "(555) 345-6789 / Ch 1", + "paramedic_service": true + }, + { + "name": "Metro West Medic 5", + "location": "Route 104 Checkpoint", + "contact_number_frequency": "(555) 345-6790 / Ch 1", + "paramedic_service": true + } + ], + "air_ambulance_services": [ + { + "name": "AirEvac Response Helicopter 3", + "location": "Blackwood Airport Helipad", + "contact_number_frequency": "(555) 888-1234 / VHF 155.400 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Blackwood Community Hospital", + "address_latitude_longitude": "45 River Road, Blackwood, 39.8765 N, 84.1234 W", + "contact_number_frequency": "(555) 678-1100 / Med Net 1", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Valley Regional Toxicology & Trauma Center", + "address_latitude_longitude": "1200 Health Park Way, Dayton, 39.7500 N, 84.2000 W", + "contact_number_frequency": "(555) 678-9900 / Med Net 4", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "In the event of an acute respiratory exposure to Anhydrous Ammonia, responders must immediately evacuate the patient upwind to the Hot Zone Emergency Decon Aid Station at Gate 4. Continuous high-flow oxygen administration and eye/skin water irrigation must commence immediately during technical decon. AirEvac Response Helicopter 3 is available at LZ Bravo near Gate 4 for rapid transfer to Valley Regional Toxicology Center.", + "7_prepared_by": { + "name": "Dr. Aris Thorne", + "position_title": "Medical Unit Leader", + "signature": "Aris Thorne", + "date_time": "08/13/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Mark Davis", + "signature": "Mark Davis", + "date_time": "08/13/2026 17:15" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_3.json b/benchmark/datasets/ground_truth/ics206_3.json new file mode 100644 index 00000000..37f29f5d --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_3.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_medical_aid_stations": [ + { + "name": "Berth 42 Decon & Medical Station", + "location": "Berth 42 Dockside Command Post", + "contact_number_frequency": "156.800 MHz / Ch 16", + "paramedic_service": true + }, + { + "name": "M/T Stolt Synergy Deck Medical Post", + "location": "Vessel Starboard Main Deck", + "contact_number_frequency": "467.525 MHz / Ch 3", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Houston Fire Department Medic 42", + "location": "Port Gate 5 Security Staging", + "contact_number_frequency": "(555) 456-7890 / Ch 1", + "paramedic_service": true + }, + { + "name": "Harris County Emergency Corps Unit 18", + "location": "Channelview Staging Area", + "contact_number_frequency": "(555) 456-7891 / Ch 1", + "paramedic_service": true + } + ], + "air_ambulance_services": [ + { + "name": "Memorial Hermann Life Flight", + "location": "Houston Medical Center Helipad", + "contact_number_frequency": "(555) 777-9111 / VHF 155.280 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Houston Methodist Hospital Baytown", + "address_latitude_longitude": "4401 Garth Road, Baytown, 29.7355 N, 94.9774 W", + "contact_number_frequency": "(555) 890-2200 / Marine Channel 22", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Memorial Hermann Texas Medical Center", + "address_latitude_longitude": "6411 Fannin St, Houston, 29.7108 N, 95.3986 W", + "contact_number_frequency": "(555) 890-9900 / Marine Channel 24", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Given high ambient heat and humidity during Level A chemical suit entry, responder entry times are strictly capped at 20 minutes followed by 40 minutes active cooling and hydration. In the event of chemical acid splashes or suit breaches, perform instant water deluge at the Berth 42 Decon Station for a minimum of 15 minutes. Memorial Hermann Life Flight is designated for critical chemical burn transport from Berth 42 Pier Helipad.", + "7_prepared_by": { + "name": "LT Commander James Miller", + "position_title": "Medical Unit Leader", + "signature": "James Miller", + "date_time": "10/14/2026 06:00" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Sarah Jenkins", + "signature": "Sarah Jenkins", + "date_time": "10/14/2026 06:30" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_4.json b/benchmark/datasets/ground_truth/ics206_4.json new file mode 100644 index 00000000..06b4103c --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_4.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "Staging Area Charlie Medical Station", + "location": "Skykomish School Gym Briefing Room", + "contact_number_frequency": "461.2250 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Warm Decon Hydration & Medical Aid Station", + "location": "State Route 2 Derailment Overlook", + "contact_number_frequency": "466.2250 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "Cascade Regional EMS Medic 3", + "location": "Staging Area Charlie", + "contact_number_frequency": "(555) 567-8901 / Ch 1", + "paramedic_service": true + }, + { + "name": "Skykomish Volunteer Fire Department Ambulance 8", + "location": "Skykomish Fire Station", + "contact_number_frequency": "(555) 567-8902 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "Airlift Northwest Helicopter 1", + "location": "Arlington Regional Helipad", + "contact_number_frequency": "(555) 666-5432 / VHF 155.355 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Everett General Hospital", + "address_latitude_longitude": "1330 Colby Ave, Everett, 47.9789 N, 122.2012 W", + "contact_number_frequency": "(555) 901-3300 / State EMS Ch 4", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Harborview Medical Center", + "address_latitude_longitude": "325 9th Ave, Seattle, 47.6042 N, 122.3242 W", + "contact_number_frequency": "(555) 901-9900 / State EMS Ch 8", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Under freezing night conditions (28°F overnight) and toxic Chlorine threat, heated water decontamination trailers and active warming blankets must be operated continuously at the Warm Decon Station to prevent suit icing and hypothermia. Any exposure to Chlorine vapor requires immediate inhalation treatment with humidified oxygen and urgent transport to Harborview Medical Center. Airlift Northwest Helicopter 1 is positioned at Skykomish High School Football Field LZ.", + "7_prepared_by": { + "name": "Captain David Miller", + "position_title": "Medical Unit Leader", + "signature": "David Miller", + "date_time": "11/02/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Carl Stevens", + "signature": "Carl Stevens", + "date_time": "11/02/2026 17:15" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics206_5.json b/benchmark/datasets/ground_truth/ics206_5.json new file mode 100644 index 00000000..94c6c702 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics206_5.json @@ -0,0 +1,80 @@ +{ + "ics_206_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_medical_aid_stations": [ + { + "name": "ICP Main Medical Station", + "location": "Hesperia Fire Station 30", + "contact_number_frequency": "460.1250 MHz / Ch 1", + "paramedic_service": true + }, + { + "name": "Sawpit Canyon Water Rescue & Medical Aid Station", + "location": "Sawpit Canyon Boat Launch", + "contact_number_frequency": "465.1250 MHz / Ch 2", + "paramedic_service": true + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "San Bernardino County Fire Medic 30", + "location": "Hesperia Fire Station 30", + "contact_number_frequency": "(555) 678-9012 / Ch 1", + "paramedic_service": true + }, + { + "name": "Desert Ambulance Unit 14", + "location": "Silverwood Lake Main Entrance", + "contact_number_frequency": "(555) 678-9013 / Ch 1", + "paramedic_service": false + } + ], + "air_ambulance_services": [ + { + "name": "Mercy Air Helicopter 2", + "location": "Victorville Regional Airport Helipad", + "contact_number_frequency": "(555) 444-8765 / VHF 155.340 MHz", + "paramedic_service": true + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "Desert Valley Hospital", + "address_latitude_longitude": "16850 Bear Valley Rd, Victorville, 34.4712 N, 117.2954 W", + "contact_number_frequency": "(555) 012-4400 / County Med Net 2", + "air_ambulance_helipad": true, + "burn_center": false, + "trauma_center": true + }, + { + "hospital_name": "Loma Linda University Medical Center", + "address_latitude_longitude": "11234 Anderson St, Loma Linda, 34.0489 N, 117.2641 W", + "contact_number_frequency": "(555) 012-9900 / County Med Net 9", + "air_ambulance_helipad": true, + "burn_center": true, + "trauma_center": true + } + ], + "6_special_medical_emergency_procedures": "Night operations on water present dual risks of hydrocarbon skin absorption/ingestion and water submersion. All personnel operating on skimmer vessels or boom lines must wear USCG-approved PFDs and safety tether lines. In case of accidental water entry or oil ingestion, immediately transport the patient to Sawpit Canyon Water Rescue Medical Aid Station for emergency decon and airway management. Mercy Air Helicopter 2 is on standby at Hesperia Fire Station 30 Helipad.", + "7_prepared_by": { + "name": "Dr. Lisa Martinez", + "position_title": "Medical Unit Leader", + "signature": "Lisa Martinez", + "date_time": "05/18/2026 16:30" + }, + "8_approved_by_safety_officer": { + "name": "Safety Officer Frank Owens", + "signature": "Frank Owens", + "date_time": "05/18/2026 17:00" + }, + "iap_page_number": "5" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_1.json b/benchmark/datasets/ground_truth/ics207_1.json new file mode 100644 index 00000000..2fbba43b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_1.json @@ -0,0 +1,81 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Chief J. Thomas (County Fire)", + "S. Albright (State DEQ)", + "D. Miller (CSX Railroad RP)" + ], + "command_staff": { + "safety_officer": "Captain R. Mendez", + "public_information_officer": "A. Cho (County OEM)", + "liaison_officer": "Inspector G. Sims (SO)" + }, + "operations_section": { + "chief": "B. Reynolds", + "staging_area_manager": "Staging Area Alpha Manager", + "branches_divisions_groups": [ + { + "title": "Hazardous Materials Branch Director", + "name": "Lt. T. Kincaid" + }, + { + "title": "Hazmat Entry Group Supervisor", + "name": "Capt. P. Gomez" + }, + { + "title": "Decontamination Group Supervisor", + "name": "Lt. S. Baker" + }, + { + "title": "Fire Suppression Branch Director", + "name": "Batt. Chief E. Walters" + } + ] + }, + "planning_section": { + "chief": "Marcus Vance", + "resources_unit_ldr": "T. Jenkins", + "situation_unit_ldr": "E. Brooks", + "documentation_unit_ldr": "H. Martinez", + "demobilization_unit_ldr": "R. Sterling" + }, + "logistics_section": { + "chief": "K. Dunavan", + "support_branch": { + "director": "J. Myers", + "supply_unit_ldr": "S. Taylor", + "facilities_unit_ldr": "C. Adams", + "ground_spt_unit_ldr": "M. Evans" + }, + "service_branch": { + "director": "B. Foster", + "comms_unit_ldr": "R. Lee", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "L. King" + } + }, + "finance_administration_section": { + "chief": "Robert Sterling", + "time_unit_ldr": "C. White", + "procurement_unit_ldr": "G. Scott", + "comp_claims_unit_ldr": "E. Green", + "cost_unit_ldr": "W. Harris" + } + }, + "4_prepared_by": { + "name": "T. Jenkins", + "position_title": "Resources Unit Leader", + "signature": "T. Jenkins", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_2.json b/benchmark/datasets/ground_truth/ics207_2.json new file mode 100644 index 00000000..3542b22b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_2.json @@ -0,0 +1,81 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Chief R. Vance (Blackwood Fire Dept)", + "Officer M. Ross (State EPA)", + "J. Vance (Blackwood Pipeline Co. RP)" + ], + "command_staff": { + "safety_officer": "Captain L. Hayes", + "public_information_officer": "E. Wright", + "liaison_officer": "Deputy K. Miller" + }, + "operations_section": { + "chief": "D. Kowalski", + "staging_area_manager": "Staging Area Bravo Manager", + "branches_divisions_groups": [ + { + "title": "Pipeline Isolation Branch Director", + "name": "Capt. Donald Kross" + }, + { + "title": "Hot Zone Entry Group Supervisor", + "name": "Lt. R. Mendez" + }, + { + "title": "Vapor Suppression Group Supervisor", + "name": "Capt. A. Ross" + }, + { + "title": "River Spill Containment Branch Director", + "name": "Commander Thomas Blake" + } + ] + }, + "planning_section": { + "chief": "Sarah L. Jenkins", + "resources_unit_ldr": "A. Patel", + "situation_unit_ldr": "C. Webb", + "documentation_unit_ldr": "Dr. H. Thorne", + "demobilization_unit_ldr": "M. Brody" + }, + "logistics_section": { + "chief": "T. Bradley", + "support_branch": { + "director": "G. Kross", + "supply_unit_ldr": "H. Lin", + "facilities_unit_ldr": "R. Sterling", + "ground_spt_unit_ldr": "D. Miller" + }, + "service_branch": { + "director": "S. Norris", + "comms_unit_ldr": "K. Albright", + "medical_unit_ldr": "Dr. T. Vance", + "food_unit_ldr": "A. Cho" + } + }, + "finance_administration_section": { + "chief": "A. Patel", + "time_unit_ldr": "J. Mercer", + "procurement_unit_ldr": "Laura Martinez", + "comp_claims_unit_ldr": "Inspector R. Sterling", + "cost_unit_ldr": "Sandra Keller" + } + }, + "4_prepared_by": { + "name": "A. Patel", + "position_title": "Resources Unit Leader", + "signature": "A. Patel", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_3.json b/benchmark/datasets/ground_truth/ics207_3.json new file mode 100644 index 00000000..8f6442da --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_3.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Captain H. Vance (USCG Unified Command IC)", + "OSC M. Reynolds (EPA Co-IC)", + "Chief D. Garcia (Port Authority Fire IC)", + "K. Lindqvist (Stolt Tankers RP IC)" + ], + "command_staff": { + "safety_officer": "Commander Thomas Blake", + "public_information_officer": "Laura Martinez (Port Authority)", + "liaison_officer": "Inspector R. Sterling (TCEQ)" + }, + "operations_section": { + "chief": "Battalion Chief A. Ross", + "staging_area_manager": "Staging Area Bravo Manager", + "branches_divisions_groups": [ + { + "title": "Vessel Salvage & Entry Branch Director", + "name": "Lt. J. Thorne" + }, + { + "title": "Deck Neutralization Group Supervisor", + "name": "Lt. S. Baker" + }, + { + "title": "Manifold Isolation Team Supervisor", + "name": "Capt. D. Ross" + }, + { + "title": "Ship Channel Protection Branch Director", + "name": "Capt. Donald Kross" + } + ] + }, + "planning_section": { + "chief": "Commander Richard Croft", + "resources_unit_ldr": "Dr. V. Patel", + "situation_unit_ldr": "Sandra Keller", + "documentation_unit_ldr": "Wayne Miller", + "demobilization_unit_ldr": "Battalion Chief A. Ross" + }, + "logistics_section": { + "chief": "Sandra Keller", + "support_branch": { + "director": "Capt. H. Nelson", + "supply_unit_ldr": "C. White", + "facilities_unit_ldr": "G. Scott", + "ground_spt_unit_ldr": "E. Green" + }, + "service_branch": { + "director": "W. Harris", + "comms_unit_ldr": "J. Myers", + "medical_unit_ldr": "Dr. A. Chen", + "food_unit_ldr": "S. Taylor" + } + }, + "finance_administration_section": { + "chief": "Wayne Miller", + "time_unit_ldr": "C. Adams", + "procurement_unit_ldr": "M. Evans", + "comp_claims_unit_ldr": "B. Foster", + "cost_unit_ldr": "R. Lee" + } + }, + "4_prepared_by": { + "name": "Dr. V. Patel", + "position_title": "Resources Unit Leader", + "signature": "Dr. V. Patel", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_4.json b/benchmark/datasets/ground_truth/ics207_4.json new file mode 100644 index 00000000..89685ecf --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_4.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "Captain E. Miller (WSP Unified Command IC)", + "OSC R. Brooks (EPA R10 Co-IC)", + "Chief D. Olson (Skykomish Fire IC)", + "M. Campbell (BNSF Railway RP IC)" + ], + "command_staff": { + "safety_officer": "Captain Marcus Vance", + "public_information_officer": "Jennifer Hayes (WSDOT)", + "liaison_officer": "Deputy S. Kowalski (KCSO)" + }, + "operations_section": { + "chief": "Battalion Chief T. Higgins", + "staging_area_manager": "Staging Area Charlie Manager", + "branches_divisions_groups": [ + { + "title": "Hazmat Capping Branch Director", + "name": "Capt. P. Gomez" + }, + { + "title": "Railcar Entry Group 1 Supervisor", + "name": "Capt. D. Ross" + }, + { + "title": "Decontamination Group Supervisor", + "name": "Lt. M. Turner" + }, + { + "title": "Evacuation & Security Branch Director", + "name": "Sgt. H. Lin" + } + ] + }, + "planning_section": { + "chief": "Lt. Colonel Alan Vance", + "resources_unit_ldr": "Patricia Ross", + "situation_unit_ldr": "Battalion Chief T. Higgins", + "documentation_unit_ldr": "Sgt. H. Lin", + "demobilization_unit_ldr": "G. Peterson" + }, + "logistics_section": { + "chief": "David Sterling", + "support_branch": { + "director": "L. King", + "supply_unit_ldr": "J. Myers", + "facilities_unit_ldr": "S. Taylor", + "ground_spt_unit_ldr": "C. Adams" + }, + "service_branch": { + "director": "M. Evans", + "comms_unit_ldr": "B. Foster", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "R. Lee" + } + }, + "finance_administration_section": { + "chief": "Patricia Ross", + "time_unit_ldr": "W. Harris", + "procurement_unit_ldr": "C. White", + "comp_claims_unit_ldr": "G. Scott", + "cost_unit_ldr": "E. Green" + } + }, + "4_prepared_by": { + "name": "Patricia Ross", + "position_title": "Resources Unit Leader", + "signature": "Patricia Ross", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics207_5.json b/benchmark/datasets/ground_truth/ics207_5.json new file mode 100644 index 00000000..6c0b7119 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics207_5.json @@ -0,0 +1,82 @@ +{ + "ics_207_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_organization_chart": { + "incident_commanders": [ + "OSC C. Martinez (EPA R9 Co-IC)", + "Chief M. Thorne (SBCo Fire IC)", + "Chief Inspector A. Kim (Cal OES)", + "W. Vance (Pipeline RP IC)" + ], + "command_staff": { + "safety_officer": "Captain Gregory Hall", + "public_information_officer": "Samantha Norris (Cal OES)", + "liaison_officer": "Ranger D. Stevens (State Parks)" + }, + "operations_section": { + "chief": "Battalion Chief Kevin Ross", + "staging_area_manager": "Staging Area Delta Manager", + "branches_divisions_groups": [ + { + "title": "Canyon Pipeline Repair Branch Director", + "name": "Capt. Gregory Hall" + }, + { + "title": "Clamp Repair Group Supervisor", + "name": "Capt. A. Ross" + }, + { + "title": "Reservoir Skimming & Booming Branch Director", + "name": "Captain B. Walsh" + }, + { + "title": "South Arm Skimmer Division Supervisor", + "name": "Capt. B. Walsh" + } + ] + }, + "planning_section": { + "chief": "Captain Rachel Brooks", + "resources_unit_ldr": "Jason Wu", + "situation_unit_ldr": "Battalion Chief Kevin Ross", + "documentation_unit_ldr": "Dr. L. Arispe", + "demobilization_unit_ldr": "Captain B. Walsh" + }, + "logistics_section": { + "chief": "Megan Taylor", + "support_branch": { + "director": "Capt. P. Gomez", + "supply_unit_ldr": "Lt. S. Baker", + "facilities_unit_ldr": "Capt. D. Ross", + "ground_spt_unit_ldr": "Lt. M. Turner" + }, + "service_branch": { + "director": "Capt. H. Nelson", + "comms_unit_ldr": "Sgt. R. Hall", + "medical_unit_ldr": "Dr. N. Howard", + "food_unit_ldr": "L. King" + } + }, + "finance_administration_section": { + "chief": "Jason Wu", + "time_unit_ldr": "J. Myers", + "procurement_unit_ldr": "S. Taylor", + "comp_claims_unit_ldr": "C. Adams", + "cost_unit_ldr": "M. Evans" + } + }, + "4_prepared_by": { + "name": "Jason Wu", + "position_title": "Resources Unit Leader", + "signature": "Jason Wu", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "4" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_1.json b/benchmark/datasets/ground_truth/ics208_1.json new file mode 100644 index 00000000..8ad9a4ca --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_1.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_operational_period": { + "date_from": "07/07/2026", + "date_to": "07/08/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap Benzene vapors closer to the ground. Mandate Level B SCBA PPE within the 300-foot exclusion zone. Mandatory buddy system and continuous photoionization detector air monitoring required for all entry crews along creek banks.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Mobile Command Post inside the Forward Operations Briefing Trailer", + "5_prepared_by": { + "name": "Captain R. Mendez", + "position_title": "Safety Officer", + "signature": "R. Mendez", + "date_time": "07/07/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_2.json b/benchmark/datasets/ground_truth/ics208_2.json new file mode 100644 index 00000000..82b4e974 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_2.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_operational_period": { + "date_from": "08/13/2026", + "date_to": "08/14/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when Anhydrous Ammonia vapors hug low ground. SCBA Level B PPE is mandatory within the 500-foot exclusion zone. Vigilance required for reduced visibility, riverbank slip hazards, and toxic vapor pockets.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Mobile Command Post Briefing Trailer at Station 12", + "5_prepared_by": { + "name": "Captain L. Hayes", + "position_title": "Safety Officer", + "signature": "L. Hayes", + "date_time": "08/13/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_3.json b/benchmark/datasets/ground_truth/ics208_3.json new file mode 100644 index 00000000..41aa8307 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_3.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_operational_period": { + "date_from": "10/14/2026", + "date_to": "10/15/2026", + "time_from": "07:00", + "time_to": "19:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Mandatory Level A chemical suit entry procedures enforced for all vessel deck operations. Due to extreme daytime heat (88°F, 78% humidity), entry work cycles are capped at 20 minutes active entry followed by 40 minutes active hydration and cooling.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "USCG Sector Houston Command Center Safety Office", + "5_prepared_by": { + "name": "Commander Thomas Blake", + "position_title": "Safety Officer", + "signature": "Thomas Blake", + "date_time": "10/14/2026 06:00" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_4.json b/benchmark/datasets/ground_truth/ics208_4.json new file mode 100644 index 00000000..c8d4f68b --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_4.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_operational_period": { + "date_from": "11/02/2026", + "date_to": "11/03/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions (28°F overnight). Enforce mandatory Level A PPE compliance and buddy system protocols. Heated decontamination water and warm hydration must be maintained continuously to prevent suit icing and hypothermia.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Skykomish School Gym Operations Briefing Room", + "5_prepared_by": { + "name": "Captain Marcus Vance", + "position_title": "Safety Officer", + "signature": "Marcus Vance", + "date_time": "11/02/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics208_5.json b/benchmark/datasets/ground_truth/ics208_5.json new file mode 100644 index 00000000..90bb9bad --- /dev/null +++ b/benchmark/datasets/ground_truth/ics208_5.json @@ -0,0 +1,21 @@ +{ + "ics_208_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_operational_period": { + "date_from": "05/18/2026", + "date_to": "05/19/2026", + "time_from": "18:00", + "time_to": "06:00" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and 100% life-vest compliance at all times. High vigilance required for steep, oil-covered riprap banks and wildlife hazards.", + "4_site_safety_plan_required": true, + "approved_site_safety_plan_located_at": "Incident Command Post at Hesperia Fire Station 30", + "5_prepared_by": { + "name": "Captain Gregory Hall", + "position_title": "Safety Officer", + "signature": "Gregory Hall", + "date_time": "05/18/2026 16:30" + }, + "iap_page_number": "6" + } +} diff --git a/benchmark/datasets/ground_truth/ics213_1.json b/benchmark/datasets/ground_truth/ics213_1.json new file mode 100644 index 00000000..0a0836f1 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_1.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Whispering Pines Derailment", + "2_to": { + "name": "Marcus Vance", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Commander Eric Sterling", + "position": "Operations Section Chief" + }, + "4_subject": "Request for Additional Vapor Suppression Foam and Boom Resources", + "5_date": "07/07/2026", + "6_time": "19:15", + "7_message": "Due to increased ambient vapor concentrations of Benzene detected downwind along Whispering Pines Creek, Operations urgently requests five additional totes (1,250 gallons) of fluoroprotein vapor suppression foam and 500 feet of sorbent boom to reinforce Checkpoint Bravo before 22:00 hours.", + "8_approved_by": { + "name": "Chief J. Thomas", + "position_title": "Incident Commander", + "signature": "Chief J. Thomas" + }, + "9_reply": "Supply Unit has authorized immediate dispatch of five totes of foam and 500 feet of sorbent boom from Regional Logistics Depot. ETA to Staging Area Alpha is 21:00 hours.", + "10_replied_by": { + "name": "Marcus Vance", + "position_title": "Planning Section Chief", + "signature": "Marcus Vance", + "date_time": "07/07/2026 19:45" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_2.json b/benchmark/datasets/ground_truth/ics213_2.json new file mode 100644 index 00000000..f8d59ec3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_2.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Blackwood Chemical Pipeline Breach", + "2_to": { + "name": "Sarah L. Jenkins", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Division Supervisor Alan Cole", + "position": "Division A Supervisor" + }, + "4_subject": "Downstream Municipal Water Intake Precautionary Shutoff Notice", + "5_date": "08/13/2026", + "6_time": "19:30", + "7_message": "Field water monitoring team at Boom Site 2 reports low-level dissolved ammonia readings reaching 0.05 ppm near Blackwood River mile 14. Recommend immediately notifying Blackwood Water Authority to initiate precautionary intake gate closure.", + "8_approved_by": { + "name": "Chief R. Henderson", + "position_title": "Incident Commander", + "signature": "Chief R. Henderson" + }, + "9_reply": "Liaison Officer contacted Blackwood Water Authority at 19:50 hours. Water intake gates closed at 20:00 hours. Alternate reservoir supply activated.", + "10_replied_by": { + "name": "Sarah L. Jenkins", + "position_title": "Planning Section Chief", + "signature": "Sarah L. Jenkins", + "date_time": "08/13/2026 20:10" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_3.json b/benchmark/datasets/ground_truth/ics213_3.json new file mode 100644 index 00000000..27df43b8 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_3.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Port of Houston Sulfuric Acid Tanker Leak", + "2_to": { + "name": "Commander Richard Croft", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Hazmat Group Supervisor Mark Ross", + "position": "Hazmat Group Supervisor" + }, + "4_subject": "Authorization for Sodium Bicarbonate Neutralization Slurry Application", + "5_date": "10/14/2026", + "6_time": "08:45", + "7_message": "Hazmat entry team has contained deck pooling at Berth 42. Request formal authorization from UC to apply 10 tons of dry sodium bicarbonate slurry to deck pooling to neutralize concentrated sulfuric acid prior to washdown.", + "8_approved_by": { + "name": "Captain H. Vance", + "position_title": "Incident Commander", + "signature": "Captain H. Vance" + }, + "9_reply": "Unified Command approves application of 10 tons sodium bicarbonate slurry. Technical Specialists from TCEQ are monitoring runoff pH at Berth 42 outfall.", + "10_replied_by": { + "name": "Commander Richard Croft", + "position_title": "Planning Section Chief", + "signature": "Richard Croft", + "date_time": "10/14/2026 09:15" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_4.json b/benchmark/datasets/ground_truth/ics213_4.json new file mode 100644 index 00000000..829659e5 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_4.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Cascade Pass Chlorine Railcar Derailment", + "2_to": { + "name": "Lt. Colonel Alan Vance", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Rail Tactical Specialist George Miller", + "position": "Technical Specialist" + }, + "4_subject": "Urgent Transport Request for Emergency B-Kit Capping Assembly", + "5_date": "11/02/2026", + "6_time": "19:00", + "7_message": "Capping team at car BNSF-77402 requires specialized torque wrench set and secondary seal gaskets for Emergency B-Kit capping assembly. Request immediate dispatch from Staging Area Charlie via all-terrain vehicle.", + "8_approved_by": { + "name": "Captain E. Miller", + "position_title": "Incident Commander", + "signature": "Captain E. Miller" + }, + "9_reply": "Ground Support Unit dispatched ATV-02 with requested torque wrench set and B-Kit gaskets at 19:20 hours. ETA to railcar site is 19:45 hours.", + "10_replied_by": { + "name": "Lt. Colonel Alan Vance", + "position_title": "Planning Section Chief", + "signature": "Alan Vance", + "date_time": "11/02/2026 19:30" + } + } +} diff --git a/benchmark/datasets/ground_truth/ics213_5.json b/benchmark/datasets/ground_truth/ics213_5.json new file mode 100644 index 00000000..fe5be9a3 --- /dev/null +++ b/benchmark/datasets/ground_truth/ics213_5.json @@ -0,0 +1,29 @@ +{ + "ics_213_ground_truth": { + "1_incident_name": "Silverwood Reservoir Crude Oil Pipeline Rupture", + "2_to": { + "name": "Rachel Brooks", + "position": "Planning Section Chief" + }, + "3_from": { + "name": "Skimmer Operations Leader Frank Gomez", + "position": "Operations Group Supervisor" + }, + "4_subject": "Skimmer Vessel Relocation and Sorbent Boom Realignment", + "5_date": "05/18/2026", + "6_time": "20:00", + "7_message": "Due to shifts in surface currents toward Sawpit Canyon inlet, requesting authorization to reposition Skimmer Vessel RV-LC-01 to South Arm and deploy an additional 500 feet of hard containment boom by 22:00 hours.", + "8_approved_by": { + "name": "Chief M. Thorne", + "position_title": "Incident Commander", + "signature": "Chief M. Thorne" + }, + "9_reply": "Operations Chief approves relocation of RV-LC-01 and deployment of 500 feet hard boom. Work Boat 3 assigned to assist boom rigging.", + "10_replied_by": { + "name": "Rachel Brooks", + "position_title": "Planning Section Chief", + "signature": "Rachel Brooks", + "date_time": "05/18/2026 20:30" + } + } +} diff --git a/benchmark/datasets/narratives/ics201_1.txt b/benchmark/datasets/narratives/ics201_1.txt new file mode 100644 index 00000000..a3d83038 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_1.txt @@ -0,0 +1,13 @@ +### Whispering Pines Derailment Mockup Incident Briefing (ICS 201) + +The Whispering Pines Derailment incident, designated under incident number US-EPA-R4-2026-0707, was formally initiated on July 07, 2026, at 06:30 hours. + +The operational landscape, mapped with North oriented at the top of the page, encompasses a 1.5-mile radius centered around the intersection of CSX Rail Milepost 142.5 and Highway 411. Within this sector, County Line Road is completely closed to public traffic to secure the Staging Area established at the County Fairgrounds. The primary incident site is located at Rail Milepost 142.5, where seven freight cars (#12 through #18) have jackknifed. The actively impacted area includes Car #14, a breached tank car releasing a Class 3 flammable and toxic liquid (Benzene) directly into Whispering Pines Creek at an estimated rate of 20 gallons per minute, causing visible surface sheening extending 500 meters downstream. Local air monitoring indicates elevated Volatile Organic Compounds within a 300-foot downwind plume tracking East-Southeast at 5 miles per hour. The critical threatened areas downstream include the Whispering Pines Nature Reserve Wetlands one mile away and the Municipal Water Intake located two miles downstream. Trajectory modeling shows waterborne containment issues moving East at 1.5 knots, which commands immediate defensive booming actions. + +The situation summary reveals that the derailment occurred at 05:45 hours due to a mechanical rail car failure, sparking a secondary 0.5-acre brush fire in the right-of-way that directly threatens an adjacent, intact liquefied petroleum gas tank car. In response, a mandatory 0.5-mile residential evacuation is being executed by local law enforcement. The comprehensive health and safety briefing identifies primary hazards as toxic Benzene inhalation, dermal exposure risks, flash fires from pooling product, heavy machinery movement around compromised rail lines, responder heat stress in the ambient 88°F weather, and slip-and-trip hazards along the steep creek banks. To mitigate these risks, an immediate 300-foot Exclusion Zone has been established around the rail cars. Responders entering the Hot Zone for containment are strictly required to wear Level B personal protective equipment, including self-contained breathing apparatus and chemical-resistant clothing. Level C protection is authorized for warm zone booming teams only if continuous photoionization detector monitoring confirms volatile organic compound levels remain below 1 part per million. Firefighters must wear full structural turnout gear with self-contained breathing apparatus, and all personnel must pass through a formal wet decontamination corridor set up at the Warm-to-Cold zone interface before leaving the operational area. This section was officially prepared by Marcus Vance, serving as the Initial Planning Section Chief, on July 07, 2026, at 08:00 hours. + +The current and planned objectives focus heavily on safety and containment, aiming to ensure the life safety of all emergency responders and the public within the perimeter by 09:00 hours, complete the 0.5-mile downwind residential evacuation by 09:30 hours, suppress the brush fire near the liquefied petroleum gas car to eliminate thermal risks by 10:00 hours, deploy effective deflection and underflow containment booming across the creek to protect the water intake, and formally establish a Unified Command structure by 10:30 hours. The chronological strategy and tactics began at 06:00 hours with the initial dispatch of County Fire and Sheriff units and the setup of a 500-foot isolation perimeter. At 06:15 hours, local Fire Chief Thomas established command and notified the State Department of Environmental Quality, the Environmental Protection Agency, and CSX Railroad. By 06:45 hours, the Sheriff’s Department initiated door-to-door evacuations of 45 homes. At 07:15 hours, Regional Hazmat Team 1 arrived to deploy an air-monitoring grid and inspect the damaged tanks, followed at 07:30 hours by Engine 41 and Tanker 5 initiating defensive foam applications on the brush fire. By 07:45 hours, advance teams from the state environmental agency and the railroad arrived to finalize the water-containment strategy. Planned actions include deploying a specialized spill contractor to drop 500 feet of deflection boom at Boom Site 1 at 08:15 hours, launching a drone overflight at 08:45 hours to map downstream tracking, and completing the transition to a fully unified command at the Mobile Command Post at 09:30 hours. This programmatic operational strategy was compiled and signed by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. + +The current command and general staff organization is structured as a Unified Command consisting of Chief J. Thomas from County Fire, S. Albright from the State Department of Environmental Quality, and D. Miller representing CSX Railroad as the Responsible Party. This command core is directly supported by Safety Officer Captain R. Mendez, Public Information Officer A. Cho representing the county, and Liaison Officer Inspector G. Sims from the Sheriff's Office. The General Staff is led by Operations Section Chief B. Reynolds from the state agency, Planning Section Chief Marcus Vance, and Logistics Section Chief K. Dunavan. Under the Operations Section, field execution is split into the Hazardous Materials Group, supervised by Lieutenant T. Kincaid of Regional Hazmat Team 1, and the Fire Suppression Division, commanded by Battalion Chief E. Walters. This organizational layout was logged and validated by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. + +The resource summary details the specific assets allocated to stabilize the incident. Regional Hazmat Team 1, a Type 1 Hazardous Materials unit, was ordered at 06:00 hours, arrived on scene at 07:00 hours, and is currently positioned at the Cold Zone boundary conducting perimeter monitoring and plugging operations. County Engine 41 and County Engine 45 were both ordered at 05:48 hours, arriving at 06:00 hours and 06:05 hours respectively; Engine 41 is actively suppressing the brush fire while Engine 45 manages critical water supply lines. County Tanker 5, a water tender ordered at 06:02 hours, arrived at 06:20 hours and is providing continuous flow for foam application. Four law enforcement units from the County Sheriff arrived at 05:55 hours following a 05:48 hours dispatch and are running traffic checkpoints and roadblocks on Creek Road alongside evacuation duties. County EMS Unit 12 arrived at 06:12 hours after being ordered at 06:00 hours, standing by at the Staging Area for responder rehabilitation and medical backup. Several critical resources are currently en route: HEPACO Spill Team A was ordered at 06:45 hours with an estimated arrival of 08:15 hours to deploy 1,000 feet of containment boom and vacuum trucks; State DEQ Drone Unit 1 was ordered at 07:10 hours with an estimated arrival of 08:30 hours to provide aerial thermal imagery; and the CSX contractor vacuum truck TechRad was ordered at 07:15 hours with an estimated arrival of 09:30 hours to begin product offloading from the breached tank car once the scene stabilizes. This complete resource ledger was verified and signed by Planning Section Chief Marcus Vance on July 07, 2026, at 08:00 hours. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_2.txt b/benchmark/datasets/narratives/ics201_2.txt new file mode 100644 index 00000000..1ff0ef5c --- /dev/null +++ b/benchmark/datasets/narratives/ics201_2.txt @@ -0,0 +1,24 @@ +Blackwood Chemical Pipeline Breach Mockup Incident Briefing (ICS 201) +The Blackwood Chemical Pipeline Breach incident, designated under incident number US-EPA-R5-2026-0813, was formally initiated on August 13, 2026, at 07:15 hours. + +The operational landscape, mapped with standard graphics and GIS symbology, encompasses a total area of operations spanning a 3.0-mile radius around the Blackwood Industrial Corridor. Within this zone, the primary incident site area is centered at Pipeline Valve Station 14-B along Blackwood River Road. The actively impacted areas include a 150-foot radius surrounding the breached 8-inch pressurized line releasing anhydrous ammonia gas, along with an impacted shoreline extending 1,000 yards down the adjacent Blackwood River. Threatened areas downstream include the Oakridge Residential Subdivision 0.75 miles downwind to the East, as well as the Valley Water Authority Municipal Intake Facility located 1.5 miles downstream. Overflight results from state police aviation confirm a dense, low-hanging vapor cloud hugging the ground and moving steadily downwind, alongside a localized surface sheen on the riverbank. Dispersion trajectories model a toxic vapor plume moving East-Northeast at 6 miles per hour, with waterborne chemical runoff traveling downstream at approximately 2.0 knots. + +The situation summary indicates that a third-party excavation crew accidentally punctured the main transfer pipeline at 06:50 hours, causing an uncontrolled high-pressure release of hazardous material. The comprehensive health and safety briefing identifies primary hazards as severe atmospheric exposure to anhydrous ammonia vapor, cryogenic freeze burns upon direct contact with liquid leakage, respiratory injury, structural eye damage, heat stress among response personnel wearing heavy gear in 85°F temperatures, and slip-and-fall risks near steep river embankment slopes. Necessary measures to protect responders and the public include the immediate establishment of a 500-foot Exclusion Zone, mandatory Level A vapor-tight personal protective equipment with self-contained breathing apparatus for all entries into the Hot Zone, continuous photoionization detector air monitoring at all control perimeter boundaries, water curtain deployment to knock down vapor clouds, and a compulsory full-body chemical decontamination procedure at the Warm-to-Cold zone boundary prior to exiting the site. This section was formally prepared by Sarah Jenkins, serving as the Initial Planning Section Chief, who applied her signature on August 13, 2026, at 08:30 hours. + +Current and planned objectives center on life safety, hazard isolation, and environmental mitigation: achieve complete isolation of the pipeline segment by closing upstream valves by 09:30 hours; finish the sheltering-in-place order and partial evacuation of 120 homes in the downwind Oakridge subdivision by 10:00 hours; deploy absorbent and containment booming across the Blackwood River above the municipal water intake by 10:30 hours; and fully establish a Multi-Agency Unified Command at the Regional Emergency Operations Center by 11:00 hours. The chronological sequence of current and planned actions, strategies, and tactics began at 07:00 hours, when initial dispatch sent Blackwood Fire Department and Local Sheriff units to establish an outer safety perimeter. At 07:20 hours, Fire Chief R. Vance established Incident Command and ordered an immediate downwind shelter-in-place alert. At 07:45 hours, Hazmat Team 5 arrived on scene to conduct initial perimeter air monitoring and assist in establishing control zones. At 08:15 hours, pipeline technicians confirmed valve isolation procedures were initiated, while fire crews set up unmanned monitor nozzles to disperse airborne vapors. Planned actions include deploying the Regional Spill Response Team at 09:00 hours to launch containment boom at River Boom Site Alpha, conducting a secondary drone air sampling flight at 09:30 hours, and finalizing the shift to a Unified Command structure at 10:00 hours. + +The current command and general staff organization operates under a Unified Command structure led by Incident Commanders Chief R. Vance (Blackwood Fire Department), Officer M. Ross (State Environmental Protection Agency), and J. Vance (Blackwood Pipeline Co., Responsible Party). The Command Staff features Safety Officer Captain L. Hayes, Public Information Officer E. Wright, and Liaison Officer Deputy K. Miller. The General Staff comprises Operations Section Chief D. Kowalski, Planning Section Chief Sarah Jenkins, Logistics Section Chief T. Bradley, and Finance/Administration Section Chief A. Patel. Additional tactical positions established within the operational structure include Hazmat Group Supervisor Lieutenant C. Webb and Air Monitoring Specialist Dr. H. Thorne. + +The resource summary details the operational status and tracking of critical response assets: + +Hazmat Team 5 (Resource ID: HZM-05), ordered on August 13 at 07:00 hours, arrived on scene at 07:40 hours (Arrived: True) and is currently executing air monitoring and entry ops at the Hot Zone perimeter. + +Blackwood Engine 12 (Resource ID: ENG-12), ordered at 06:55 hours, arrived at 07:05 hours (Arrived: True) and is actively operating water curtains for vapor suppression. + +County Water Tender 3 (Resource ID: WT-03), ordered at 07:10 hours, arrived at 07:25 hours (Arrived: True) and is supplying continuous water flow to Engine 12. + +Sheriff Patrol Unit Group Alpha (Resource ID: SO-ALPHA), ordered at 06:55 hours, arrived at 07:10 hours (Arrived: True) and is maintaining road closures and executing downwind evacuation notices. + +CleanHarbor Spill Response Team (Resource ID: CH-SRT-1), ordered at 07:30 hours with an estimated time of arrival at 09:00 hours (Arrived: False), is currently en route with 1,500 feet of river containment boom and vacuum recovery equipment. + +State EPA Air Monitoring Drone Unit (Resource ID: EPA-DRONE-2), ordered at 07:40 hours with an estimated time of arrival at 08:45 hours (Arrived: False), is en route to provide real-time thermal and chemical plume mapping. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_3.txt b/benchmark/datasets/narratives/ics201_3.txt new file mode 100644 index 00000000..a1c04a37 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_3.txt @@ -0,0 +1,29 @@ +Cypress Bend Tanker Collision Mockup Incident Briefing (ICS 201) +The Cypress Bend Tanker Collision incident, designated under incident number US-CG-D8-2026-0921, was formally initiated on September 21, +2026, at 14: 15 hours. + +The operational landscape, mapped with standard navigational charts and GIS symbology, encompasses a total area of operations spanning a 4.0-mile radius centered at Intracoastal Waterway Mile Marker 245. Within this zone, the primary incident site area is located at the confluence of the Intracoastal Waterway and Cypress Bayou. The actively impacted areas include a 300-yard containment zone surrounding a punctured double-hulled barge discharging Heavy Fuel Oil (HFO 380) at approximately 30 gallons per minute, alongside an impacted shoreline extending 1.5 miles south along the eastern bank of Cypress Bayou. Threatened areas downstream include the Grand Cypress Mangrove Sanctuary located 2.0 miles south-southeast and the Delta Commercial Oyster Leases 3.5 miles down-river. Overflight results from US Coast Guard Aviation Unit 65 confirm a surface slick measuring 2.0 miles long by 100 yards wide moving with the ebb tide, with visible heavy sheen accumulating along the shoreline. Dispersion trajectories model the waterborne oil plume moving South-Southeast at 1.8 knots with local coastal wind vectors blowing from the Northwest at 12 knots. + +The situation summary indicates that a tugboat towing two loaded asphalt barges collided with an anchored commercial tanker at 13: 50 hours, rupturing Cargo Tank #2 starboard. The comprehensive health and safety briefing identifies primary hazards as hydrogen sulfide gas accumulation from the fuel oil cargo, inhalation of petroleum hydrocarbons, direct dermal irritation, slip-and-fall hazards on oily vessel decks and muddy riverbanks, responder heat exhaustion in humid 91°F conditions, and water safety/drowning risks during vessel-to-vessel transfers. Necessary measures to protect responders include establishing a 1, +000-foot Maritime Safety Zone, requiring Level C personal protective equipment with half-mask organic vapor respirators and personal flotation devices for all over-water personnel, continuous multi-gas air monitoring near the breached tank, mandatory work-rest cycles with hydration stations in the staging area, and operating a vessel-based decontamination station anchored at the cold zone boundary. This section was formally prepared by Commander Elena Rostova, serving as the Initial Planning Section Chief, who applied her signature on September 21, +2026, at 15: 30 hours. + +Current and planned objectives prioritize environmental protection and spill isolation: complete primary deflection boom deployment across the mouth of Cypress Bayou by 16: 30 hours; secure mechanical patching or product transfer (lightering) of Cargo Tank #2 by 18: 00 hours; deploy skimming vessels to recover free-floating surface product before nightfall at 19: 30 hours; and establish a full Joint Information Center to handle media inquiries by 20: 00 hours. The chronological sequence of current and planned actions, strategies, and tactics began at 14: 00 hours, when Coast Guard Station Cypress Bend dispatched Sector Patrol Craft and issued a Notice to Mariners closing the waterway. At 14: 30 hours, Captain M. Thorne established Incident Command and deployed first-responder skimmer boats. At 15: 00 hours, Port Authority Hazmat Teams completed initial safety sweeps and confirmed zero structural fire threats. At 15: 30 hours, marine salvage engineers boarded the barge to inspect damage and prep transfer pumps. Planned actions include dropping 2, +000 feet of hard boom across the mangrove inlet at 16: 00 hours, initiating lightering operations to pump fuel out of the damaged tank at 17: 00 hours, and deploying an evening drone overflight at 18: 30 hours to track thermal slick drift patterns. + +The current command and general staff organization operates under a Unified Command structure led by Incident Commanders Captain M. Thorne (US Coast Guard), Director D. Sterling (State Department of Environmental Quality), and H. Vance (Cypress Towing Co., Responsible Party). The Command Staff features Safety Officer Lieutenant Commander J. Ruiz, Public Information Officer C. Alvarez, and Liaison Officer Agent P. Brooks. The General Staff comprises Operations Section Chief G. Morales, Planning Section Chief Commander Elena Rostova, Logistics Section Chief R. O'Connor, and Finance/Administration Section Chief M. Lin. Additional tactical positions established within the operational structure include Salvage & Engineering Group Supervisor Chief Specialist K. Vance and Wildlife Rescue Leader Dr. A. Mercer. + +The resource summary details the operational status and tracking of critical response assets: + +USCG Marine Safety Detachment 1 (Resource ID: USCG-MSD-01), ordered on September 21 at 14: 00 hours, arrived on scene at 14: 25 hours (Arrived: True) and is currently enforcing the maritime safety zone and conducting gas monitoring. + +Cypress Port Skimmer Vessel 4 (Resource ID: SKIM-04), ordered at 14: 10 hours, arrived at 14: 35 hours (Arrived: True) and is actively skimming free surface product around the barge stern. + +Delta Salvage Tug 'Warrior' (Resource ID: TUG-WARRIOR), ordered at 14: 30 hours, arrived at 15: 15 hours (Arrived: True) and is providing stabilizing push-support and powering transfer pumps. + +State DEQ Water Sampling Craft (Resource ID: DEQ-BOAT-2), ordered at 14: 20 hours, arrived at 15: 00 hours (Arrived: True) and is collecting downstream water samples and turbidity readings. + +National Spill Response Team Barge 8 (Resource ID: NSRT-B-08), ordered at 14: 45 hours with an estimated time of arrival at 16: 30 hours (Arrived: False), is currently en route with 3, +000 feet of ocean containment boom and heavy skimmers. + +Gulf Coast Wildlife Rescue Unit (Resource ID: GC-WILD-1), ordered at 15: 10 hours with an estimated time of arrival at 17: 15 hours (Arrived: False), is en route to set up a oily bird stabilization and staging facility. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_4.txt b/benchmark/datasets/narratives/ics201_4.txt new file mode 100644 index 00000000..d3f129a4 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_4.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Blackwood River Chemical Spill + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Blackwood River Chemical Spill (Incident Number: USCG-EPA-R4-2026-0813) was formally initiated on 08/13/2026 at 07:15 EDT. The designated total area of operations encompasses a 3.5-mile radius including the Blackwood Industrial Park, Mile Marker 42 of the Blackwood River, and the adjacent State Route 104 transportation corridor. The primary incident site is localized at Storage Tank #4 within the Apex Chemical Tank Farm (MM 42.1, East Bank), where a structural shell fracture is leaking concentrated Styrene Monomer. Currently impacted areas include 1,200 meters of the river surface displaying heavy chemical sheening and a volatile organic compound (VOC) vapor plume extending 800 yards downwind (East-Northeast) across State Route 104. Primary threatened locations include the Blackwood Municipal Drinking Water Intake situated 3.0 miles downstream at MM 39.1 and the Blackwood Marsh Ecological Reserve located 1.5 miles downstream. Overflight reconnaissance conducted via USCG Aux Drone Flight #1 confirmed a 200-yard wide chemical slick migrating South-Southwest at 1.8 knots with moderate shore oiling along the east bank. Dispersion trajectories indicate the airborne VOC plume is tracking East-Northeast at 6 mph towards unpopulated forestry, while the aquatic slick moves downstream toward the municipal water intake. Impacted shorelines are currently restricted to the east bank riprap and adjacent low-lying mudflats from MM 42.1 to MM 41.3. Tactical graphics and maps oriented North vertically depict Staging Area A at Westside High School, Boom Site 1 (Deflection), Boom Site 2 (Containment), the Mobile Command Post at County Fire Station 12, a 500-foot Exclusion Zone perimeter, and downstream drinking water intake protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 06:45 EDT on 08/13/2026, a catastrophic seal failure occurred on Tank #4 at the Apex Chemical Facility, releasing approximately 8,500 gallons of liquid Styrene Monomer into secondary containment, with overflow entering the Blackwood River via storm drain outfall #3 at an estimated 30 gpm. A secondary vapor cloud formed across State Route 104, triggering immediate road closures and precautionary evacuations of 60 surrounding industrial properties. Primary health and safety hazards include acute inhalation toxicity from styrene vapors (causing narcotic effects, central nervous system depression, and respiratory irritation), flammability risk (flash point 88°F), dermal chemical burns, slip/trip/fall hazards along steep river banks, and ambient heat stress (current temperature 91°F). Necessary protective measures require establishing a strict 500-foot Exclusion Zone; mandating Level B PPE (supplied air SCBA with continuous PID/LEL/O2 monitoring) for all Hot Zone operations; Level C PPE (full-face APR with organic vapor cartridges) within the Warm Zone; continuous perimeter air monitoring at CP and downwind baselines; and operating a formal wet-decontamination corridor at Facility Gate 2. This briefing was formally prepared by Sarah L. Jenkins, Planning Section Chief, signed Sarah L. Jenkins, on 08/13/2026 at 09:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 (08/13/2026) are: (1) Maintain life safety and enforce the 500-foot exclusion perimeter by 08:00 hours; (2) Complete containment booming at Boom Site 1 (MM 41.5) by 10:30 hours to protect downstream drinking water intake; (3) Secure the leak source on Apex Tank #4 and complete product transfer by 12:00 hours; (4) Perform continuous air monitoring downwind along Route 104 and publish public safety advisories by 11:00 hours; and (5) Transition to full Unified Command (USCG, EPA, State DEQ, County Hazmat) by 10:00 hours. The chronological timeline of tactical actions proceeds as follows: At 07:15, initial alarm dispatch occurred, sending County Fire Engine 12 and Hazmat 1 to establish the 500-foot perimeter. At 07:30, Local Command was established by Chief Henderson, initiating facility emergency shutdown and closing Route 104. At 07:55, Regional Hazmat Team 4 arrived, initiating perimeter PID air monitoring and setting up the decon corridor at Gate 2. At 08:20, USCG Sector Strike Team and EPA On-Scene Coordinator arrived on scene to establish Unified Command at Fire Station 12. At 08:45, Spill Response Vessel River Guardian deployed 1,000 feet of hard containment boom at MM 41.5 (Boom Site 1). Planned tactical actions include: At 09:15, Entry Team 1 enters the Hot Zone under Level B PPE to secure Tank #4 bottom manifold valve; at 10:00, deploy secondary sorbent deflection boom array at Boom Site 2 (MM 40.2) upstream of the drinking water intake; and at 11:30, commence vacuum truck skimmer recovery of pooled styrene at storm drain outfall #3. + +Paragraph 4: Command & General Staff Organization Structure The Incident Organization operates under a Unified Command structure comprising Unified Incident Commanders Captain M. Ross (USCG Unified Command IC), OSC E. Vance (EPA Co-IC), Chief R. Henderson (County Fire IC), and J. Mercer (Apex Facility RP IC). Command Staff includes Lt. Commander David Miller as Safety Officer, Elena Gomez (County OEM) as Public Information Officer, and Captain Arthur Pendelton (State Police) as Liaison Officer. General Staff operations are directed by Battalion Chief Marcus Brody as Operations Section Chief, Sarah L. Jenkins as Planning Section Chief, Karen Albright as Logistics Section Chief, and Robert Sterling as Finance/Administration Section Chief. Specialized operational units are led by Captain Donald Kross (Hazmat 1) serving as Hazmat Group Supervisor and Lt. Timothy Vance (USCG) serving as Waterborne Operations Branch Director. + +Paragraph 5: Resource Summary & Tactical Assignments Assigned operational resources are tracked as follows: (1) Hazmat Unit (Identifier: HM-01), ordered 08/13/2026 07:20, Status: Arrived (ETA N/A), assigned to conduct initial air monitoring and execute Hot Zone valve isolation; (2) Fire Engine (Identifier: ENG-12), ordered 08/13/2026 07:15, Status: Arrived (ETA N/A), assigned to secure outer perimeter and maintain foam fire standby; (3) USCG Strike Team (Identifier: USCG-ST-04), ordered 08/13/2026 07:40, Status: Arrived (ETA N/A), assigned to assist Unified Command and oversee waterborne operations; (4) Spill Response Vessel (Identifier: RV-RG-02), ordered 08/13/2026 08:00, Status: Arrived (ETA N/A), assigned to deploy 1,000 ft containment boom at Boom Site 1; (5) Vacuum Truck Unit (Identifier: VAC-99), ordered 08/13/2026 08:30, Status: En Route (ETA 08/13/2026 10:45), assigned to perform liquid product recovery at outfall #3 upon arrival; and (6) Air Monitoring Unit (Identifier: AMR-03), ordered 08/13/2026 08:15, Status: En Route (ETA 08/13/2026 09:45), assigned to transport high-sensitivity PID and Jerome mercury/VOC analyzers to establish downwind grid monitoring. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics201_5.txt b/benchmark/datasets/narratives/ics201_5.txt new file mode 100644 index 00000000..22ec49c2 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_5.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Port of Houston Sulfuric Acid Tanker Leak + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Port of Houston Sulfuric Acid Tanker Leak (Incident Number: USCG-EPA-R6-2026-1014) was formally initiated on 10/14/2026 at 11:20 CDT. The designated total area of operations covers a 2.5-mile radius centered around Berth 42 of the Houston Ship Channel Tank Terminal. The primary incident site is localized at Tanker Berth 42 (MM 51.2, West Bank), where a manifold failure on the chemical parcel tanker M/T Stolt Synergy released concentrated 98% sulfuric acid onto the vessel deck and into secondary containment, with runoff entering the waterway. Currently impacted areas include a 500-yard perimeter along Berth 42 exhibiting localized water discoloration and acidic vapor mist, extending 400 yards downwind across adjacent industrial docks. Primary threatened locations include the San Jacinto Battleground Historic Site 1.2 miles down-river and the Channelview Residential Neighborhood 2.0 miles downwind to the North-Northwest. Overflight reconnaissance conducted via USCG Air Station Houston Helicopter 6512 confirmed a localized 150-yard plume dissipating in the ship channel with continuous water sampling underway. Dispersion trajectories model the acidic vapor mist tracking North-Northwest at 8 mph, while waterborne runoff moves with the ebb tide East-Southeast at 1.2 knots. Impacted shorelines are confined to 400 yards of concrete bulkhead and wooden fender piling at Berth 42. Tactical graphics and maps oriented North vertically depict Staging Area Bravo at Jacintoport Terminal, Spill Boom Site 1, Command Post at USCG Sector Houston-Galveston, Exclusion Zone boundaries (1,000 ft radius), and ship channel navigation safety zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 10:50 CDT on 10/14/2026, a high-pressure discharge hose burst during offloading operations at Berth 42, spilling approximately 4,200 gallons of 98% sulfuric acid. Heat generated by exothermic reaction with bilge water created a dense corrosive acid mist cloud over the berth. Primary health and safety hazards include severe skin necrosis upon contact, permanent ocular damage, pulmonary edema from acid mist inhalation, extreme heat reaction when mixed with water, and slip hazards on degraded berth decking. Protective measures enforce a 1,000-foot Exclusion Zone; Level A chemical suit protection with SCBA for Hot Zone entries; Level B protective suits with SCBA for Warm Zone support; continuous pH water sampling and real-time acid mist sensor monitoring; and a mandatory dual-stage lime neutralization decontamination wash at Gate 4. This briefing was formally prepared by Commander Richard Croft, Planning Section Chief, signed Richard Croft, on 10/14/2026 at 13:00 CDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce a 1,000-foot Exclusion Zone around Berth 42 and secure vessel offloading by 12:00 hours; (2) Complete neutralization of deck spill using sodium bicarbonate slurry by 14:00 hours; (3) Deploy chemical neutralization boom array at Berth 42 slip by 13:30 hours; (4) Monitor atmospheric acid mist levels along Channelview perimeter to ensure public safety by 15:00 hours; and (5) Transition to Unified Command (USCG, EPA, TCEQ, RP) by 12:30 hours. Chronological tactical timeline: At 11:20, initial emergency response initiated; Port Authority Fire Engines 81 and 82 respond; 1,000-foot safety perimeter set up. At 11:45, Sector Command established by Captain H. Vance; Houston Ship Channel traffic restricted to one-way slow speed. At 12:15, Port Hazmat Team 1 arrives on scene to establish pH monitoring grid and set up neutralization decon corridor. At 12:50, USCG Strike Team arrives; initiates shipboard inspection and containment audit. At 13:15, Stolt Tankers RP response vessel deploys chemical sorbent boom around M/T Stolt Synergy. Planned actions: At 14:00, Entry Team Alpha under Level A PPE executes emergency valve shutdown on shipboard manifold; at 14:30, begin high-volume sodium bicarbonate dry-powder application on vessel deck; at 16:00, perform secondary overflight to confirm complete plume dissipation. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Captain H. Vance (USCG Unified Command IC), OSC M. Reynolds (EPA Co-IC), Chief D. Garcia (Port Authority Fire IC), and K. Lindqvist (Stolt Tankers RP IC). Command Staff includes Commander Thomas Blake as Safety Officer, Laura Martinez (Port Authority) as Public Information Officer, and Inspector R. Sterling (TCEQ) as Liaison Officer. General Staff consists of Battalion Chief A. Ross as Operations Section Chief, Commander Richard Croft as Planning Section Chief, Sandra Keller as Logistics Section Chief, and Wayne Miller as Finance/Admin Section Chief. Specialized positions include Chemical Hazards Specialist Dr. V. Patel and Vessel Boarding Group Supervisor Lt. J. Thorne. + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational units: (1) Port Authority Hazmat Team (Identifier: HZM-PORT-1), ordered 10/14/2026 11:25, Status: Arrived (ETA N/A), executing pH sampling grid and Hot Zone entry prep; (2) Port Fire Engine 81 (Identifier: ENG-81), ordered 10/14/2026 11:20, Status: Arrived (ETA N/A), securing 1,000 ft landward perimeter and foam standby; (3) USCG Gulf Strike Team (Identifier: USCG-GST-02), ordered 10/14/2026 11:50, Status: Arrived (ETA N/A), supervising vessel entry protocol and safety verification; (4) Chemical Response Vessel Acid Contain 1 (Identifier: RV-AC-01), ordered 10/14/2026 12:00, Status: Arrived (ETA N/A), deployed chemical sorbent boom around vessel berth; (5) Lime Neutralization Truck Unit (Identifier: NEUT-TRK-05), ordered 10/14/2026 12:30, Status: En Route (ETA 10/14/2026 14:15), delivering 10 tons of dry sodium bicarbonate slurry; and (6) Mobile Air Sampling Van (Identifier: AIR-SAM-02), ordered 10/14/2026 12:10, Status: En Route (ETA 10/14/2026 13:45), transporting real-time SO3/acid mist photoionization sensors. diff --git a/benchmark/datasets/narratives/ics201_6.txt b/benchmark/datasets/narratives/ics201_6.txt new file mode 100644 index 00000000..d36e9cc2 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_6.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Cascade Pass Chlorine Railcar Derailment + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Cascade Pass Chlorine Railcar Derailment (Incident Number: NTSB-EPA-R10-2026-1102) was formally initiated on 11/02/2026 at 04:45 PST. The total area of operations encompasses a 5.0-mile radius centered around BNSF Railway Milepost 218.4 near Skykomish, WA. The primary incident site is located at Rail Milepost 218.4, where five freight cars derailed, including pressurized tank car DOT-105J500W (BNSF-77402) containing liquefied chlorine gas with damaged protective valve housing. Currently impacted areas include a 1,500-foot vapor dispersion zone along the rail right-of-way and adjacent State Route 2. Threatened areas include the Town of Skykomish situated 1.8 miles West downwind and the South Fork Skykomish River salmon spawning habitat located 300 feet South. Overflight reconnaissance conducted via Washington State Patrol FLIR Helicopter WSP-AIR-2 confirmed a localized green-yellow chlorine cloud hugging the ravine floor and drifting West-Northwest at 4 mph. Dispersion trajectories model the airborne toxic plume tracking West-Northwest along the State Route 2 corridor at 4 mph, with zero liquid chemical runoff entering waterways. Impacted shorelines are unimpacted, with ground contamination restricted to northern ravine embankment slopes. Tactical graphics and maps oriented North vertically depict Staging Area Charlie at Stevens Pass Maintenance Yard, Mobile Command Post at Skykomish School District Gym, a 1.5-mile Exclusion Zone boundary, and State Route 2 traffic closure checkpoints. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 04:15 PST on 11/02/2026, a mountain derailment caused five freight cars to leave the tracks. Tank car BNSF-77402 sustained severe dome housing damage, resulting in a low-rate pressurized vapor release of toxic chlorine at approximately 15 lbs/min. State Route 2 was closed immediately and a mandatory evacuation was ordered for 250 residents of Skykomish. Primary health and safety hazards include severe pulmonary edema risk from chlorine gas inhalation, ocular and skin chemical burns, acute respiratory collapse, mountain hypothermia (ambient temp 34°F), and steep terrain slip hazards. Protective measures enforce a 1.5-mile Exclusion Zone; mandatory Level A vapor-tight encapsulated suits with SCBA for all Hot Zone entry personnel; Level B suits for Warm Zone support; continuous electrochemical chlorine sensor monitoring at CP baselines; mandatory indoor shelter-in-place for outer perimeters; and a heated wet-decontamination trailer wash at Staging Area Charlie. This briefing was formally prepared by Lt. Colonel Alan Vance, Planning Section Chief, signed Alan Vance, on 11/02/2026 at 07:15 PST. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Complete mandatory evacuation of Skykomish township within 1.5-mile radius by 07:30 hours; (2) Perform Hot Zone entry to apply Emergency B-Kit capping assembly on chlorine railcar dome by 09:30 hours; (3) Establish continuous multi-point perimeter air monitoring along State Route 2 by 08:00 hours; and (4) Secure rail right-of-way and establish Unified Command by 07:00 hours. Chronological tactical timeline: At 04:45, initial alarm dispatch occurred; Skykomish Fire and King County Sheriff units respond, closing State Route 2 at Milepost 215. At 05:15, Local Command was established by Chief D. Olson; mandatory evacuation sirens activated across Skykomish. At 05:50, Regional Hazmat Team 3 and BNSF Response Team arrived on scene to initiate perimeter air testing. At 06:30, WSP FLIR helicopter completed thermal overflight mapping plume dispersion. At 07:00, Unified Command was established at Skykomish School Gym with EPA Region 10 and BNSF RP. Planned actions: At 08:15, Entry Team 1 under Level A PPE initiates B-Kit capping tool installation on railcar valve dome; at 09:30, perform pressure test and seal verification of capping assembly; at 11:00, complete environmental health review to evaluate lifting outer zone evacuation orders. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Captain E. Miller (WSP Unified Command IC), OSC R. Brooks (EPA R10 Co-IC), Chief D. Olson (Skykomish Fire IC), and M. Campbell (BNSF Railway RP IC). Command Staff includes Captain Marcus Vance as Safety Officer, Jennifer Hayes (WSDOT) as Public Information Officer, and Deputy S. Kowalski (KCSO) as Liaison Officer. General Staff operations are managed by Battalion Chief T. Higgins as Operations Section Chief, Lt. Colonel Alan Vance as Planning Section Chief, David Sterling as Logistics Section Chief, and Patricia Ross as Finance/Admin Section Chief. Specialized tactical roles include Rail Hazmat Specialist G. Peterson (BNSF) and Evacuation Group Supervisor Sgt. H. Lin (KCSO). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) King County Hazmat 3 (Identifier: HZM-33), ordered 11/02/2026 05:00, Status: Arrived (ETA N/A), preparing Level A Entry Team 1 and Emergency B-Kit capping assembly; (2) Skykomish Engine 11 (Identifier: ENG-11), ordered 11/02/2026 04:45, Status: Arrived (ETA N/A), securing State Route 2 East closure checkpoint; (3) BNSF Emergency Response Unit (Identifier: BNSF-ER-01), ordered 11/02/2026 05:15, Status: Arrived (ETA N/A), on scene with railcar specialized capping kits; (4) WSP Aviation FLIR Helicopter (Identifier: WSP-AIR-2), ordered 11/02/2026 05:30, Status: Arrived (ETA N/A), completed thermal imagery mapping of plume drift; (5) Mobile Decontamination Trailer (Identifier: DECON-04), ordered 11/02/2026 05:45, Status: En Route (ETA 11/02/2026 08:30), en route to establish warm water decon at Staging Area Charlie; and (6) Hazmat Air Monitoring Recon (Identifier: AIR-MON-10), ordered 11/02/2026 06:00, Status: En Route (ETA 11/02/2026 07:45), en route with multi-gas chlorine sensors. diff --git a/benchmark/datasets/narratives/ics201_7.txt b/benchmark/datasets/narratives/ics201_7.txt new file mode 100644 index 00000000..b80073b6 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_7.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Silverwood Reservoir Crude Oil Pipeline Rupture + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Silverwood Reservoir Crude Oil Pipeline Rupture (Incident Number: CalOES-EPA-R9-2026-0518) was formally initiated on 05/18/2026 at 13:10 PDT. The total area of operations encompasses a 4.0-mile radius including Trans-California Pipeline MM 87.3, Sawpit Canyon, and the Silverwood Reservoir basin. The primary incident site is located at Valve Station 12 along Trans-California Pipeline MM 87.3 in Sawpit Canyon, where a 12-inch pressurized crude line ruptured. Currently impacted areas include 800 yards of Sawpit Canyon creek bed carrying crude oil towards Silverwood Lake, with a 30-acre surface oil sheen in the reservoir South arm. Primary threatened locations include the Mojave Water Agency Municipal Pumping Plant situated 1.5 miles North and Silverwood Lake State Recreation Area campgrounds located 0.8 miles West. Overflight reconnaissance conducted via Cal FIRE Recon Aircraft 240 confirmed heavy black crude pooling in Sawpit Canyon and a 400-yard wide oil ribbon entering Silverwood Lake. Dispersion trajectories model the waterborne crude oil slick moving North at 1.0 knot towards the main reservoir basin, while the airborne volatile organic cloud tracks Southeast at 7 mph into canyon slopes. Impacted shorelines encompass 1.2 miles of rocky reservoir shoreline and marsh vegetation along the Sawpit Canyon inlet. Tactical graphics and maps oriented North vertically depict Staging Area Delta at Silverwood Lake Marina, Incident Command Post at Hesperia Fire Station 30, a 1,000-foot Exclusion Zone perimeter, Deflection Boom Sites A and B, and municipal water intake protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 12:40 PDT on 05/18/2026, a hillside land movement ruptured a 12-inch crude oil pipeline at MM 87.3, discharging approximately 12,000 gallons of heavy crude oil into Sawpit Canyon creek before automated valves shut off flow. The oil flowed downstream into Silverwood Reservoir, threatening southern California drinking water supplies. Primary health and safety hazards include toxic benzene vapor inhalation, volatile organic compound exposure, acute flammability (flash point 65°F), wildfire ignition risk in dry brush, slip-and-fall hazards on oily terrain, and severe heat exhaustion (ambient temp 94°F). Protective measures enforce a mandatory 1,000-foot Exclusion Zone; Level B PPE with SCBA for initial Hot Zone creek entry; Level C PPE with organic vapor respirators for shoreline booming teams; continuous PID air monitoring downwind; mandatory wildland fire standby team with AFFF foam; and establishment of dual-basin wash decontamination at Marina Ramp 2. This briefing was formally prepared by Captain Rachel Brooks, Planning Section Chief, signed Rachel Brooks, on 05/18/2026 at 15:30 PDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce 1,000-foot Exclusion Zone and secure pipeline isolation by 14:00 hours; (2) Deploy 2,000 feet of containment boom across Sawpit Canyon inlet by 16:30 hours to prevent oil migration to water intake; (3) Evacuate Silverwood Lake State Recreation Area campgrounds by 15:00 hours; (4) Initiate skimmer boat oil recovery in South arm of reservoir by 17:00 hours; and (5) Establish Unified Command (Cal OES, EPA R9, USFS, San Bernardino County Fire, Pipeline RP) by 14:30 hours. Chronological tactical timeline: At 13:10, alarm dispatch occurred; San Bernardino County Fire Engine 30 and USFS Crew 4 respond to set up initial safety perimeter. At 13:40, Local Command was established by Chief M. Thorne; Trans-California Pipeline operators confirm remote valve isolation at MM 85 and MM 90. At 14:15, State Parks Rangers complete full evacuation of Silverwood Lake campgrounds (150 visitors evacuated). At 14:45, Cal OES and EPA OSC arrive on scene; Unified Command established at Station 30. At 15:15, Clean Harbors Spill Response Vessel deploys 1,200 feet of hard boom at Sawpit Inlet (Boom Site A). Planned actions: At 16:00, deploy secondary sorbent deflection boom array at Boom Site B upstream of Mojave Water Intake; at 17:00, vacuum skimmer vessel Lake Clean 1 initiates surface crude extraction in South arm; at 18:30, complete initial shoreline oiling assessment along Sawpit Canyon inlet. + +Paragraph 4: Command & General Staff Organization Structure Operational command operates under a Unified Command structure comprising Unified Incident Commanders OSC C. Martinez (EPA R9 Co-IC), Chief M. Thorne (SBCo Fire IC), Chief Inspector A. Kim (Cal OES), and W. Vance (Pipeline RP IC). Command Staff includes Captain Gregory Hall as Safety Officer, Samantha Norris (Cal OES) as Public Information Officer, and Ranger D. Stevens (State Parks) as Liaison Officer. General Staff operations are directed by Battalion Chief Kevin Ross as Operations Section Chief, Captain Rachel Brooks as Planning Section Chief, Megan Taylor as Logistics Section Chief, and Jason Wu as Finance/Admin Section Chief. Specialized positions include Environmental Unit Leader Dr. L. Arispe (USFS) and Waterborne Recovery Supervisor Captain B. Walsh (Clean Harbors). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) San Bernardino Hazmat Engine (Identifier: ENG-30), ordered 05/18/2026 13:10, Status: Arrived (ETA N/A), operating perimeter vapor monitoring and fire standby; (2) USFS Wildland Handcrew (Identifier: CREW-04), ordered 05/18/2026 13:15, Status: Arrived (ETA N/A), clearing brush and building containment dikes in Sawpit Canyon; (3) State Parks Ranger Unit (Identifier: PARK-12), ordered 05/18/2026 13:20, Status: Arrived (ETA N/A), completed campground evacuations and road closures; (4) Clean Harbors Spill Vessel (Identifier: RV-LC-01), ordered 05/18/2026 14:00, Status: Arrived (ETA N/A), deployed 1,200 ft hard boom at Sawpit Canyon inlet; (5) Heavy Vacuum Skimmer Boat (Identifier: SKIM-02), ordered 05/18/2026 14:30, Status: En Route (ETA 05/18/2026 16:45), en route from San Pedro for reservoir skimmer recovery; and (6) Air Monitoring Recon Unit (Identifier: AIR-MON-08), ordered 05/18/2026 14:10, Status: En Route (ETA 05/18/2026 15:45), en route with photoionization detectors and benzene gas sensors. diff --git a/benchmark/datasets/narratives/ics201_8.txt b/benchmark/datasets/narratives/ics201_8.txt new file mode 100644 index 00000000..89fb93e7 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_8.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Delaware Bay Container Vessel Collision & Fuel Oil Spill + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Delaware Bay Container Vessel Collision & Fuel Oil Spill (Incident Number: USCG-D5-2026-0629) was formally initiated on 06/29/2026 at 22:40 EDT. The designated total area of operations covers a 6.0-mile radius centered at Delaware Bay Shipping Channel Buoy 14 near Lewes, DE. The primary incident site is located at Delaware Bay Channel Buoy 14 (38.85°N, 75.12°W), where container vessel M/V Atlantic Voyager collided with a bulk carrier, breaching fuel tank #3 port side. Currently impacted areas include a 3.5-mile long heavy oil sheen extending East-Southeast across the bay channel towards Cape Henlopen State Park. Primary threatened locations include Prime Hook National Wildlife Refuge situated 4.0 miles Northwest and Cape Henlopen Tidal Salt Marshes located 2.5 miles South. Overflight reconnaissance conducted via USCG HC-144 Ocean Sentry aircraft confirmed a 300-yard wide slick of Intermediate Fuel Oil (IFO 380) drifting with the flood tide. Dispersion trajectories model the waterborne oil slick migrating East-Southeast at 2.2 knots toward Cape Henlopen under coastal winds of 14 knots from the West-Northwest. Impacted shorelines encompass 2.0 miles of outer sandy beach and dune lines along Cape Henlopen State Park. Tactical graphics and maps oriented North vertically depict Staging Area Echo at Lewes Ferry Terminal, Incident Command Post at USCG Sector Delaware Bay Headquarters, a 1,000-yard Maritime Safety Zone, Boom Sites 1, 2, and 3, and wildlife protection zones. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 22:15 EDT on 06/29/2026, container vessel M/V Atlantic Voyager collided with anchored bulk carrier M/V Pacific Trader near Buoy 14. Port fuel tank #3 breached, discharging approximately 25,000 gallons of heavy Intermediate Fuel Oil (IFO 380) before vessel operators completed internal fuel transfer to starboard tanks. Primary health and safety hazards include hydrocarbon vapor exposure (benzene/H2S), direct dermal toxicity, severe slip hazards on oily vessel hulls and shorelines, nighttime over-water operations, water drowning risk, and wildlife exposure risks. Protective measures enforce a 1,000-yard Maritime Safety Zone; mandatory Level C PPE with PFDs and half-mask organic vapor respirators for maritime responders; continuous H2S and PID air monitoring on response vessels; compulsory work-rest hydration cycles; and a mobile vessel decontamination station established at Lewes Pier. This briefing was formally prepared by Commander James Vance, Planning Section Chief, signed James Vance, on 06/30/2026 at 01:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Secure maritime safety perimeter and stabilize damaged vessel by 01:00 hours; (2) Deploy protective booming across Cape Henlopen salt marsh inlets by 04:00 hours; (3) Initiate offshore skimming operations using MSRC oil recovery vessels by 05:30 hours; (4) Activate wildlife rescue and rehab operations with Tri-State Bird Rescue by 06:00 hours; and (5) Establish Unified Command (USCG, EPA, Delaware DNREC, M/V Atlantic Voyager RP) by 02:00 hours. Chronological tactical timeline: At 22:40, alarm dispatch occurred; USCG Station Lewes response boats 45601 and 45602 respond to establish maritime safety zone. At 23:15, Local Command was established by Captain P. Hayes; Sector Delaware Bay requests MSRC oil spill response vessel mobilization. At 23:50, M/V Atlantic Voyager crew completes internal fuel transfer, halting active discharge from fuel tank #3. At 00:30, USCG HC-144 overflight completes IR sensor oil slick mapping. At 01:15, Delaware DNREC response team arrives at Lewes Command Post; Unified Command established. Planned actions: At 03:00, MSRC Response Vessel Delaware Responder deploys 2,500 feet of ocean containment boom at Buoy 14; at 04:30, deploy sorbent diversion boom at Cape Henlopen Inlet (Boom Site 2); at 06:00, Tri-State Bird Rescue team commences shoreline wildlife search along Cape Henlopen beaches. + +Paragraph 4: Command & General Staff Organization Structure Operational command operates under a Unified Command structure comprising Unified Incident Commanders Captain P. Hayes (USCG Unified Command IC), OSC T. Gallagher (EPA R3), Secretary A. Lawson (Delaware DNREC), and H. Lindemann (Atlantic Shipping RP IC). Command Staff includes Lt. Commander Mark Reynolds as Safety Officer, Chief Petty Officer Kelly Adams as Public Information Officer, and Inspector R. Thorne (DNREC) as Liaison Officer. General Staff operations are directed by Commander Frank Miller as Operations Section Chief, Commander James Vance as Planning Section Chief, Laura Bennett as Logistics Section Chief, and Charles Foster as Finance/Admin Section Chief. Specialized tactical positions include Scientific Support Coordinator Dr. E. Sullivan (NOAA) and Wildlife Branch Director Dr. C. Jenkins (Tri-State). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) USCG Response Boat Medium (Identifier: RBM-45601), ordered 06/29/2026 22:40, Status: Arrived (ETA N/A), maintaining 1,000-yard safety zone around damaged vessel; (2) USCG Station Lewes RBM (Identifier: RBM-45602), ordered 06/29/2026 22:40, Status: Arrived (ETA N/A), conducting water sampling and perimeter safety watch; (3) MSRC Spill Vessel Delaware Responder (Identifier: RV-MSRC-01), ordered 06/29/2026 23:00, Status: Arrived (ETA N/A), deployed 2,500 ft ocean boom and preparing offshore skimmer; (4) USCG Ocean Sentry Aircraft (Identifier: USCG-HC144), ordered 06/29/2026 23:10, Status: Arrived (ETA N/A), completed IR slick mapping and overflight tracking; (5) Mobile Wildlife Rehabilitation Trailer (Identifier: WILD-REHAB-1), ordered 06/30/2026 00:15, Status: En Route (ETA 06/30/2026 05:45), en route to establish bird cleaning station at Lewes Pier; and (6) Shoreline Cleanup Strike Team (Identifier: SCAT-TEAM-1), ordered 06/30/2026 00:45, Status: En Route (ETA 06/30/2026 06:30), mobilizing for Cape Henlopen beach oil assessment. diff --git a/benchmark/datasets/narratives/ics201_9.txt b/benchmark/datasets/narratives/ics201_9.txt new file mode 100644 index 00000000..4e580cb9 --- /dev/null +++ b/benchmark/datasets/narratives/ics201_9.txt @@ -0,0 +1,11 @@ +ICS 201 Incident Briefing Narrative: Oakridge Industrial Park Anhydrous Ammonia Release + +Paragraph 1: Header, Initiation & Tactical Map/Sketch Details The Oakridge Industrial Park Anhydrous Ammonia Release (Incident Number: EPA-R2-2026-0905) was formally initiated on 09/05/2026 at 08:15 EDT. The designated total area of operations covers a 2.0-mile radius centered at Cold Storage Logistics Building 7, Edison, NJ. The primary incident site is located at Mechanical Room 3 on the roof deck of Cold Storage Logistics Building 7 (100 Industrial Parkway), where an 8-inch high-pressure ammonia chiller line fractured. Currently impacted areas include a 400-yard vapor dispersion plume encompassing the Building 7 loading dock and northern section of Industrial Parkway. Primary threatened locations include the Meadowlands Residential Community situated 0.9 miles East downwind and the Edison Transit Center located 1.2 miles Southeast. Overflight reconnaissance conducted via Edison Police Drone Recon Unit DRONE-PD-1 confirmed a dense white fog cloud hovering over Roof Mechanical Room 3 and drifting East-Northeast at 5 mph. Dispersion trajectories model the airborne toxic anhydrous ammonia cloud migrating East-Northeast at 5 mph, with zero liquid chemical product entering storm drainage systems. Impacted shorelines are unimpacted, with contamination restricted to localized industrial ground surfaces and rooftop structures. Tactical graphics and maps oriented North vertically depict Staging Area Foxtrot at Edison High School parking lot, Command Post at Edison Fire Station 4, a 1,000-foot Exclusion Zone boundary, and Industrial Parkway traffic control points. + +Paragraph 2: Situation Summary & Health/Safety Briefing At 07:45 EDT on 09/05/2026, a mechanical vibration failure severed an 8-inch liquid refrigeration line inside Mechanical Room 3 at Cold Storage Logistics, releasing approximately 3,000 lbs of anhydrous ammonia gas. Building 7 was evacuated immediately (85 employees), and a 1,000-foot perimeter isolation zone was established. Primary health and safety hazards include severe chemical asphyxiation, acute pulmonary edema, permanent ocular injury, cryogenic chemical freeze burns, flammability risk in enclosed spaces (LEL 15-28%), and physical fall hazards from roof decking. Protective measures enforce a mandatory 1,000-foot Exclusion Zone; Level A encapsulated vapor suits with SCBA for all roof entry personnel; Level B suits for ground backup teams; continuous electrochemical ammonia air monitoring at perimeter boundaries; high-volume water curtain deployment for vapor knockdown; and a mandatory dual-basin chemical wash decontamination corridor at Building 7 main entrance. This briefing was formally prepared by Captain Michael Vance, Planning Section Chief, signed Michael Vance, on 09/05/2026 at 10:30 EDT. + +Paragraph 3: Objectives & Action Tactics Current and planned strategic objectives for Operational Period 1 are: (1) Enforce 1,000-foot Exclusion Zone and isolate Building 7 HVAC intake by 08:30 hours; (2) Perform roof entry under Level A PPE to manually isolate main receiver valve by 11:00 hours; (3) Deploy high-volume water curtain monitoring nozzles to suppress cloud drift by 09:30 hours; (4) Perform continuous downwind air monitoring along Meadowlands border by 09:00 hours; and (5) Establish Unified Command (Edison Fire, NJ DEP, EPA R2, Facility RP) by 08:45 hours. Chronological tactical timeline: At 08:15, initial alarm dispatch occurred; Edison Fire Engines 4 and 6 respond and assist Building 7 evacuation. At 08:35, Local Command was established by Chief E. Sullivan; Industrial Parkway closed at Kilmer Road. At 09:00, Middlesex County Hazmat Team 2 arrived to set up perimeter air monitoring grid and water fog curtain monitors. At 09:40, NJ DEP and EPA Region 2 OSC arrived on scene; Unified Command established at Station 4. At 10:15, drone recon flight confirmed water curtains reduced downwind cloud density by 60%. Planned actions: At 11:00, Entry Team 1 under Level A PPE executes emergency roof entry to isolate main refrigeration manifold; at 12:00, initiate mechanical ventilation of Building 7 interior through charcoal scrubber array; at 13:30, conduct clearance air sampling inside Building 7 prior to facility re-entry. + +Paragraph 4: Command & General Staff Organization Structure Operational command functions under a Unified Command structure comprising Unified Incident Commanders Chief E. Sullivan (Edison Fire IC), OSC H. Miller (EPA R2), Inspector G. Ross (NJ DEP), and T. Jenkins (Cold Storage RP IC). Command Staff includes Captain Robert Hayes as Safety Officer, Amanda Walsh (Edison OEM) as Public Information Officer, and Lt. D. Kincaid (Middlesex PD) as Liaison Officer. General Staff operations are directed by Battalion Chief Brian O'Connor as Operations Section Chief, Captain Michael Vance as Planning Section Chief, Karen Miller as Logistics Section Chief, and Steven Zhang as Finance/Admin Section Chief. Specialized positions include Refrigeration Systems Specialist C. Bauer (Cold Storage RP) and Air Monitoring Leader Lt. V. Patel (County Hazmat). + +Paragraph 5: Resource Summary & Tactical Assignments Tracking of operational assets: (1) Middlesex County Hazmat 2 (Identifier: HZM-MC-02), ordered 09/05/2026 08:20, Status: Arrived (ETA N/A), operating water curtains and preparing Level A roof entry; (2) Edison Fire Engine 4 (Identifier: ENG-04), ordered 09/05/2026 08:15, Status: Arrived (ETA N/A), supplying high-pressure water to fog monitors for vapor suppression; (3) Edison Ladder Truck 2 (Identifier: LADDER-02), ordered 09/05/2026 08:15, Status: Arrived (ETA N/A), positioned for roof access and aerial water stream deployment; (4) Police Drone Recon Unit (Identifier: DRONE-PD-1), ordered 09/05/2026 08:30, Status: Arrived (ETA N/A), conducting continuous thermal and aerial monitoring of plume drift; (5) Mobile Mechanical Scrubber Unit (Identifier: VENT-SCRUB-3), ordered 09/05/2026 09:15, Status: En Route (ETA 09/05/2026 11:45), en route to perform positive pressure building scrubbing; and (6) High-Sensitivity Air Monitoring Truck (Identifier: AIR-TRK-07), ordered 09/05/2026 09:00, Status: En Route (ETA 09/05/2026 10:15), en route to monitor downwind Meadowlands residential perimeter. diff --git a/benchmark/datasets/narratives/ics202_1.txt b/benchmark/datasets/narratives/ics202_1.txt new file mode 100644 index 00000000..a0af7cea --- /dev/null +++ b/benchmark/datasets/narratives/ics202_1.txt @@ -0,0 +1,9 @@ +### Whispering Pines Derailment Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from July 07, 2026, at 18:00 hours through July 08, 2026, at 06:00 hours, marking the first critical night-shift rotation of the response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain a zero-incident safety record for all response personnel by enforcing mandatory air monitoring and strict personal protective equipment compliance throughout the entirety of the night shift. The second objective dictates that containment teams must deploy an additional five hundred feet of underflow and sorbent booming at designated downstream check-points on Whispering Pines Creek by 22:00 hours to intercept any escaping Benzene sheen. The third objective requires hazardous materials entry teams to complete the structural stabilization and grounding of the breached Car #14 by 01:00 hours to prepare the vessel for safe product offloading. The fourth objective mandates that the Air Monitoring Group establish a continuous, telemetry-linked vapor monitoring perimeter around the eastern residential boundary by 20:00 hours to guarantee community safety. The fifth and final objective requires the Planning and Logistics sections to finalize the morning resource allocation and shift rotation plan by 04:00 hours to ensure seamless operational continuity. + +The operational period command emphasis directs all supervisors to place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap hazardous vapors closer to the ground. Command explicitly prioritizes the safety of the hazardous materials entry teams working in low-visibility conditions near the creek bed over speed of recovery. General situational awareness notes indicate that the local weather forecast predicts clearing skies with a significant ambient temperature drop to 62°F by midnight, accompanied by winds shifting from the Northwest at three to five miles per hour. This wind shift introduces a critical safety warning regarding the potential accumulation of heavy Benzene vapors in low-lying drainage ditches and creek pockets east of the derailment site. Field crews are issued a universal safety message to remain highly vigilant for low-visibility hazards, uneven muddy terrain along the banks, and potential wildlife hazards native to the adjacent wetlands. Due to the high-consequence nature of the chemical release, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the primary Mobile Command Post inside the Forward Operations Briefing Trailer. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for the active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the hot zone, and the localized National Weather Service Forecast. Additional attachments compiled into the final briefing packet include the specialized EPA Plume Trajectory Analysis Sheet and the CSX Tank Car Cargo Manifest. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Marcus Vance, who reviewed and signed the form on July 07, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Unified Incident Commander Chief J. Thomas of County Fire, who signed off on the complete package on July 07, 2026, at 17:00 hours. \ No newline at end of file diff --git a/benchmark/datasets/narratives/ics202_2.txt b/benchmark/datasets/narratives/ics202_2.txt new file mode 100644 index 00000000..051994e4 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_2.txt @@ -0,0 +1,9 @@ +### Blackwood Chemical Pipeline Breach Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from August 13, 2026, at 18:00 hours through August 14, 2026, at 06:00 hours, marking the critical night-shift rotation of the emergency response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain 100% responder compliance with SCBA Level B PPE within the 500-foot Exclusion Zone throughout the night shift. The second objective dictates that containment teams must complete installation of 1,000 feet of sorbent containment boom array across Blackwood River at Boom Site 2 by 22:00 hours to protect downstream municipal water intake. The third objective requires hazardous materials entry teams to complete hot-tap product evacuation of damaged pipeline section by 02:00 hours to stop active anhydrous ammonia leakage. The fourth objective mandates that the Air Monitoring Group maintain continuous automated PID perimeter air monitoring downwind along Route 104 with telemetry reporting to CP every 30 minutes. The fifth and final objective requires the Planning and Logistics sections to finalize Operational Period 2 IAP shift plan and resource request documentation by 04:30 hours. + +The operational period command emphasis directs all supervisors to focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when ammonia vapors hug low ground. Command explicitly prioritizes responder safety in low-visibility nighttime conditions over recovery speed. General situational awareness notes indicate that the local weather forecast predicts clear night skies, temperatures dropping to 58°F, and wind shifting from East-Northeast to North at 4 mph. Low-lying fog is expected near riverbanks between 02:00 and 06:00 hours. High vigilance is required for slick riverbank terrain and reduced nighttime visibility. Due to the hazardous chemical nature of the release, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Mobile Command Post Briefing Trailer at Station 12. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the hot zone, and the localized Weather Forecast. Additional attachments compiled into the final briefing packet include the Ammonia Air Dispersion Model Map and the Pipeline Shutoff Schematic. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Sarah L. Jenkins, who reviewed and signed the form on August 13, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief R. Henderson, who signed off on the complete package on August 13, 2026, at 17:15 hours. diff --git a/benchmark/datasets/narratives/ics202_3.txt b/benchmark/datasets/narratives/ics202_3.txt new file mode 100644 index 00000000..d3f12108 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_3.txt @@ -0,0 +1,9 @@ +### Port of Houston Sulfuric Acid Tanker Leak Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from October 14, 2026, at 07:00 hours through October 15, 2026, at 19:00 hours, marking the primary daytime operational shift of the chemical spill response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to ensure zero safety incidents by enforcing mandatory Level A chemical suit entry procedures for all vessel deck operations. The second objective dictates that neutralization teams must apply 10 tons of dry sodium bicarbonate slurry to neutralize pooled acid on Berth 42 deck by 12:00 hours. The third objective requires waterborne response teams to maintain 2,000 feet of chemical sorbent boom around M/T Stolt Synergy and conduct hourly river pH sampling to ensure zero downstream acid migration. The fourth objective mandates that vessel salvage specialists complete vessel hull structural integrity audit and offloading manifold pressure test by 15:00 hours. The fifth and final objective requires the Environmental Unit to conduct continuous public air monitoring along Channelview community boundary and issue real-time safety updates by 17:00 hours. + +The operational period command emphasis directs all supervisors to place primary tactical focus on safe chemical neutralization and pH stabilization in the ship channel water column. Personnel safety during high-temperature Level A suit operations takes absolute priority over operational speed. General situational awareness notes indicate daytime temperatures reaching 88°F with high humidity (78%), creating severe heat stress conditions for suit technicians. Winds are blowing from the Southeast at 9 mph. Work-rest cycles of 20 minutes active entry followed by 40 minutes hydration/cooling are strictly mandated. Due to the high hazard profile of concentrated sulfuric acid, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the USCG Sector Houston Command Center Safety Office. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 205a Communications List, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the port terminal, and localized Weather Forecast and Tides tables. Additional attachments compiled into the final briefing packet include the TCEQ Water Quality Monitoring Plan and the Vessel Cargo Stowage Plan. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Commander Richard Croft, who reviewed and signed the form on October 14, 2026, at 06:00 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Captain H. Vance, who signed off on the complete package on October 14, 2026, at 06:30 hours. diff --git a/benchmark/datasets/narratives/ics202_4.txt b/benchmark/datasets/narratives/ics202_4.txt new file mode 100644 index 00000000..0deec926 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_4.txt @@ -0,0 +1,9 @@ +### Cascade Pass Chlorine Railcar Derailment Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from November 02, 2026, at 18:00 hours through November 03, 2026, at 06:00 hours, marking the critical overnight shift of the hazardous materials response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to enforce strict Level A PPE compliance and buddy system protocols during all night operations around the derailment site. The second objective dictates that hazardous materials capping teams must complete installation and torque testing of Emergency B-Kit capping assembly on chlorine tank car BNSF-77402 by 22:00 hours. The third objective requires air monitoring units to maintain perimeter chlorine air monitoring at 15-minute intervals along State Route 2 evacuation boundary. The fourth objective mandates that logistics staff operate heated decontamination trailer and warm hydration station continuously at Staging Area Charlie. The fifth and final objective requires the Environmental Unit to formulate environmental sampling plan and morning shift briefing package by 04:00 hours. + +The operational period command emphasis directs all supervisors to emphasize absolute safety of capping entry teams working under nighttime freezing conditions. Ensure continuous warm water decon availability to prevent suit icing. General situational awareness notes indicate overcast skies with mountain temperatures dropping to 28°F overnight, with snow flurries expected after 01:00 hours. Light winds are blowing from the West at 3 mph drifting toward the ravine floor. Icing hazards exist on rail ballast and steep access slopes, alongside high hypothermia hazards for standing security personnel. Due to toxic chlorine gas hazards, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Skykomish School Gym Operations Briefing Room. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 207 Organization Chart, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the rail right-of-way, and the Mountain Weather Forecast package. Additional attachments compiled into the final briefing packet include the Railcar Capping Procedure Manual and the Chlorine Gas Toxicity Chart. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Lt. Colonel Alan Vance, who reviewed and signed the form on November 02, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Captain E. Miller, who signed off on the complete package on November 02, 2026, at 17:15 hours. diff --git a/benchmark/datasets/narratives/ics202_5.txt b/benchmark/datasets/narratives/ics202_5.txt new file mode 100644 index 00000000..52060a94 --- /dev/null +++ b/benchmark/datasets/narratives/ics202_5.txt @@ -0,0 +1,9 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from May 18, 2026, at 18:00 hours through May 19, 2026, at 06:00 hours, marking the overnight containment shift of the inland oil spill response. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to maintain 100% safety record with zero heat injuries or toxic vapor exposures during night shift operations. The second objective dictates that marine spill response crews must secure 2,000 feet of containment boom across Sawpit Canyon inlet and maintain skimmer vessel recovery in South arm until 23:00 hours. The third objective requires pipeline repair crews to complete pipeline mechanical clamp repair on 12-inch main line at MM 87.3 by 02:00 hours. The fourth objective mandates that water sampling teams perform hourly water sampling upstream of Mojave Water Intake facility to ensure non-detectable hydrocarbon levels. The fifth and final objective requires the Planning Section to prepare Day 2 Operational Period Incident Action Plan and resource requests by 04:30 hours. + +The operational period command emphasis directs all supervisors to focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and life-vest compliance at all times. General situational awareness notes indicate evening temperature cooling to 70°F with calm winds under 5 mph. Mountain lions and nocturnal wildlife have been reported near Sawpit Canyon inlet. Flashlight illumination is required along all shoreline walking paths due to steep, oily riprap. Due to potential drinking water contamination risks, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Incident Command Post at Hesperia Fire Station 30. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of Silverwood Lake, and localized Weather and Reservoir Current Forecasts. Additional attachments compiled into the final briefing packet include the Silverwood Lake Water Sampling Map and the Pipeline Repair Safety Plan. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Captain Rachel Brooks, who reviewed and signed the form on May 18, 2026, at 16:30 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief M. Thorne, who signed off on the complete package on May 18, 2026, at 17:00 hours. diff --git a/benchmark/datasets/narratives/ics202_6.txt b/benchmark/datasets/narratives/ics202_6.txt new file mode 100644 index 00000000..c44f517a --- /dev/null +++ b/benchmark/datasets/narratives/ics202_6.txt @@ -0,0 +1,9 @@ +### Oakridge Industrial Park Anhydrous Ammonia Release Incident Objectives (ICS 202) + +The Incident Objectives form, designated as ICS 202 for the Oakridge Industrial Park Anhydrous Ammonia Release incident under tracking number EPA-R2-2026-0905, officially establishes the strategic blueprint for the upcoming operational period. This specific operational period is scheduled to run from September 05, 2026, at 07:00 hours through September 06, 2026, at 19:00 hours, marking the recovery and clearance operational shift of the industrial hazmat incident. + +The core incident objectives for this twelve-hour window have been prioritized following the SMART model to ensure clear, action-oriented execution by field personnel. The primary objective is to enforce strict Level A/B PPE protocols and 1,000-foot Exclusion Zone control throughout daytime mechanical ventilation operations. The second objective dictates that mechanical ventilation teams must complete charcoal scrubber building ventilation of Building 7 interior to reduce interior ammonia concentrations below 15 ppm by 13:00 hours. The third objective requires structural engineers to conduct full structural and piping stress inspection of Roof Mechanical Room 3 by 15:00 hours. The fourth objective mandates that environmental sampling teams perform interior clearance air sampling across all 4 building quadrants by 17:00 hours. The fifth and final objective requires Liaison and Command Staff to submit final environmental decontamination report to NJ DEP and lift perimeter road closures by 18:30 hours. + +The operational period command emphasis directs all supervisors to focus operational efforts on safe indoor ventilation and structural validation. Ensure air monitoring teams continuously verify downwind residential air quality before discharging building exhaust. General situational awareness notes indicate mostly sunny conditions with a daytime high of 82°F. Winds are blowing from the West-Southwest at 7 mph. Thermal updrafts on Building 7 roof deck require secure safety tie-offs for all entry personnel. Due to high vapor pressure chemical hazards, a formal Site Safety Plan is strictly required for this incident. The approved Site Safety Plan has been finalized, signed by the Safety Officer, and is physically located at the Edison Fire Station 4 Command Post Conference Room. + +The complete Incident Action Plan for this operational period has been fully assembled, and the specific components included in the package have been validated. The final plan incorporates the ICS 203 Organization Assignment List, multiple ICS 204 Assignment Lists for active divisions, the ICS 205 Incident Radio Communications Plan, the ICS 205a Communications List, the ICS 206 Medical Plan, the ICS 208 Safety Message and Plan, a detailed geospatial Map and Chart package of the industrial park, and localized Weather Forecast updates. Additional attachments compiled into the final briefing packet include the Building 7 Ventilation Engineering Plan and the NJ DEP Air Quality Standard Sheet. This administrative document serves as Page 1 of the comprehensive Incident Action Plan package and was officially prepared by Planning Section Chief Captain Michael Vance, who reviewed and signed the form on September 05, 2026, at 06:00 hours. Final executive approval for the implementation of these objectives was granted by Incident Commander Chief E. Sullivan, who signed off on the complete package on September 05, 2026, at 06:30 hours. diff --git a/benchmark/datasets/narratives/ics203_1.txt b/benchmark/datasets/narratives/ics203_1.txt new file mode 100644 index 00000000..14d7b853 --- /dev/null +++ b/benchmark/datasets/narratives/ics203_1.txt @@ -0,0 +1,13 @@ +### Whispering Pines Derailment Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, officially documents the active organizational structure and personnel assignments for Operational Period 1 (July 07, 2026, 18:00 hours to July 08, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Chief J. Thomas representing County Fire, S. Albright representing State DEQ, and D. Miller representing CSX Railroad. Executive operational support is provided by Deputy Incident Commander Assistant Chief M. Reynolds. Safety oversight is directed by Safety Officer Captain R. Mendez, public communications are managed by Public Information Officer A. Cho of County OEM, and inter-agency coordination is handled by Liaison Officer Inspector G. Sims of the Sheriff's Office. External agency representatives active at the command post include OSC K. Lawson representing US EPA Region 4, V. Patel representing CSX Railroad, and Commander B. Davis representing the Whispering Pines Police Department. + +The Planning Section is led by Planning Section Chief Marcus Vance, supported by Deputy L. Sullivan. Unit Leaders under Planning include T. Jenkins managing the Resources Unit, E. Brooks managing the Situation Unit, H. Martinez managing the Documentation Unit, and R. Sterling managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists D. Kowalski (Rail Tank Car Specialist) and Dr. A. Chen (Air Dispersion Modeler). + +The Logistics Section is commanded by Logistics Section Chief K. Dunavan, supported by Deputy P. Wright. Logistics is organized into two primary branches: the Support Branch directed by J. Myers, overseeing Supply Unit Leader S. Taylor, Facilities Unit Leader C. Adams, and Ground Support Unit Leader M. Evans; and the Service Branch directed by B. Foster, overseeing Communications Unit Leader R. Lee, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Operations Section is directed by Operations Section Chief B. Reynolds, supported by Deputy Lt. Commander D. Miller. Tactical field units operate from Staging Area Alpha located at the County Fairgrounds. Operational branches include the Hazardous Materials Branch led by Branch Director Lt. T. Kincaid and Deputy Sgt. R. Hall, supervising the Hazmat Entry Group under Capt. P. Gomez and the Decontamination Group under Lt. S. Baker; and the Fire Suppression Branch led by Branch Director Batt. Chief E. Walters and Deputy Capt. J. Miller, supervising Division A (Rail Right-of-Way) under Capt. D. Ross and the Water Supply Group under Lt. M. Turner. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. H. Nelson. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Robert Sterling, supported by Deputy A. Patel. Financial operations are staffed by Time Unit Leader C. White, Procurement Unit Leader G. Scott, Compensation/Claims Unit Leader E. Green, and Cost Unit Leader W. Harris. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Marcus Vance on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_2.txt b/benchmark/datasets/narratives/ics203_2.txt new file mode 100644 index 00000000..7cbb7d3a --- /dev/null +++ b/benchmark/datasets/narratives/ics203_2.txt @@ -0,0 +1,13 @@ +### Blackwood Chemical Pipeline Breach Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, officially documents the active organizational structure and personnel assignments for Operational Period 1 (August 13, 2026, 18:00 hours to August 14, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Chief R. Vance representing Blackwood Fire Department, Officer M. Ross representing State EPA, and J. Vance representing Blackwood Pipeline Co. Executive operational support is provided by Deputy Incident Commander Deputy Chief L. Morgan. Safety oversight is directed by Safety Officer Captain L. Hayes, public communications are managed by Public Information Officer E. Wright, and inter-agency coordination is handled by Liaison Officer Deputy K. Miller. External agency representatives active at the command post include OSC R. Brooks representing US EPA Region 5, Sheriff T. Higgins representing Blackwood County Sheriff, and Director P. Simmons representing Valley Water Authority. + +The Planning Section is led by Planning Section Chief Sarah L. Jenkins, supported by Deputy T. Bradley. Unit Leaders under Planning include A. Patel managing the Resources Unit, C. Webb managing the Situation Unit, Dr. H. Thorne managing the Documentation Unit, and M. Brody managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists E. Vance (Pipeline Integrity Specialist) and Dr. S. Ray (Chemical Toxicology Specialist). + +The Logistics Section is commanded by Logistics Section Chief T. Bradley, supported by Deputy W. Miller. Logistics is organized into two primary branches: the Support Branch directed by G. Kross, overseeing Supply Unit Leader H. Lin, Facilities Unit Leader R. Sterling, and Ground Support Unit Leader D. Miller; and the Service Branch directed by S. Norris, overseeing Communications Unit Leader K. Albright, Medical Unit Leader Dr. T. Vance, and Food Unit Leader A. Cho. + +The Operations Section is directed by Operations Section Chief D. Kowalski, supported by Deputy Lt. Timothy Vance. Tactical field units operate from Staging Area Bravo located at Westside High School. Operational branches include the Pipeline Isolation Branch led by Branch Director Capt. Donald Kross and Deputy Lt. C. Webb, supervising the Hot Zone Entry Group under Lt. R. Mendez and the Vapor Suppression Group under Capt. A. Ross; and the River Spill Containment Branch led by Branch Director Commander Thomas Blake and Deputy Inspector G. Sims, supervising Booming Division 1 under Lt. J. Thorne and the Water Intake Protection Group under Capt. B. Walsh. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief A. Patel, supported by Deputy Robert Sterling. Financial operations are staffed by Time Unit Leader J. Mercer, Procurement Unit Leader Laura Martinez, Compensation/Claims Unit Leader Inspector R. Sterling, and Cost Unit Leader Sandra Keller. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Sarah L. Jenkins on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_3.txt b/benchmark/datasets/narratives/ics203_3.txt new file mode 100644 index 00000000..5490d3e1 --- /dev/null +++ b/benchmark/datasets/narratives/ics203_3.txt @@ -0,0 +1,13 @@ +### Port of Houston Sulfuric Acid Tanker Leak Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, officially documents the active organizational structure and personnel assignments for Operational Period 1 (October 14, 2026, 07:00 hours to October 15, 2026, 19:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Captain H. Vance representing USCG, OSC M. Reynolds representing EPA, Chief D. Garcia representing Port Authority Fire, and K. Lindqvist representing Stolt Tankers. Executive operational support is provided by Deputy Incident Commander Commander T. Blake. Safety oversight is directed by Safety Officer Commander Thomas Blake, public communications are managed by Public Information Officer Laura Martinez of Port Authority, and inter-agency coordination is handled by Liaison Officer Inspector R. Sterling of TCEQ. External agency representatives active at the command post include Inspector R. Sterling representing Texas Commission on Environmental Quality, K. Lindqvist representing Stolt Tankers Shipping, and Captain M. Brody representing Port of Houston Pilots. + +The Planning Section is led by Planning Section Chief Commander Richard Croft, supported by Deputy Lt. J. Thorne. Unit Leaders under Planning include Dr. V. Patel managing the Resources Unit, Sandra Keller managing the Situation Unit, Wayne Miller managing the Documentation Unit, and Battalion Chief A. Ross managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists Dr. V. Patel (Chemical Hazard Specialist) and E. Miller (Marine Salvage Engineer). + +The Logistics Section is commanded by Logistics Section Chief Sandra Keller, supported by Deputy Wayne Miller. Logistics is organized into two primary branches: the Support Branch directed by Capt. H. Nelson, overseeing Supply Unit Leader C. White, Facilities Unit Leader G. Scott, and Ground Support Unit Leader E. Green; and the Service Branch directed by W. Harris, overseeing Communications Unit Leader J. Myers, Medical Unit Leader Dr. A. Chen, and Food Unit Leader S. Taylor. + +The Operations Section is directed by Operations Section Chief Battalion Chief A. Ross, supported by Deputy Lt. J. Thorne. Tactical field units operate from Staging Area Bravo located at Jacintoport Terminal. Operational branches include the Vessel Salvage & Entry Branch led by Branch Director Lt. J. Thorne and Deputy Capt. P. Gomez, supervising the Deck Neutralization Group under Lt. S. Baker and the Manifold Isolation Team under Capt. D. Ross; and the Ship Channel Protection Branch led by Branch Director Capt. Donald Kross and Deputy Lt. M. Turner, supervising the Water Quality Sampling Division under Dr. S. Ray and the Booming Operations Group under Capt. B. Walsh. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Commander T. Blake. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Wayne Miller, supported by Deputy Sandra Keller. Financial operations are staffed by Time Unit Leader C. Adams, Procurement Unit Leader M. Evans, Compensation/Claims Unit Leader B. Foster, and Cost Unit Leader R. Lee. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Commander Richard Croft on October 14, 2026, at 06:00 hours. diff --git a/benchmark/datasets/narratives/ics203_4.txt b/benchmark/datasets/narratives/ics203_4.txt new file mode 100644 index 00000000..9d998f6c --- /dev/null +++ b/benchmark/datasets/narratives/ics203_4.txt @@ -0,0 +1,13 @@ +### Cascade Pass Chlorine Railcar Derailment Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, officially documents the active organizational structure and personnel assignments for Operational Period 1 (November 02, 2026, 18:00 hours to November 03, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders Captain E. Miller representing WSP, OSC R. Brooks representing EPA Region 10, Chief D. Olson representing Skykomish Fire, and M. Campbell representing BNSF Railway. Executive operational support is provided by Deputy Incident Commander Inspector G. Peterson. Safety oversight is directed by Safety Officer Captain Marcus Vance, public communications are managed by Public Information Officer Jennifer Hayes of WSDOT, and inter-agency coordination is handled by Liaison Officer Deputy S. Kowalski of King County Sheriff's Office. External agency representatives active at the command post include Jennifer Hayes representing Washington State Department of Transportation, G. Peterson representing BNSF Railway, and Sgt. H. Lin representing King County Sheriff's Office. + +The Planning Section is led by Planning Section Chief Lt. Colonel Alan Vance, supported by Deputy David Sterling. Unit Leaders under Planning include Patricia Ross managing the Resources Unit, Battalion Chief T. Higgins managing the Situation Unit, Sgt. H. Lin managing the Documentation Unit, and G. Peterson managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists G. Peterson (Rail Hazmat Specialist) and Dr. H. Thorne (Toxic Gas Modeler). + +The Logistics Section is commanded by Logistics Section Chief David Sterling, supported by Deputy Patricia Ross. Logistics is organized into two primary branches: the Support Branch directed by L. King, overseeing Supply Unit Leader J. Myers, Facilities Unit Leader S. Taylor, and Ground Support Unit Leader C. Adams; and the Service Branch directed by M. Evans, overseeing Communications Unit Leader B. Foster, Medical Unit Leader Dr. N. Howard, and Food Unit Leader R. Lee. + +The Operations Section is directed by Operations Section Chief Battalion Chief T. Higgins, supported by Deputy Sgt. H. Lin. Tactical field units operate from Staging Area Charlie located at the Stevens Pass Maintenance Yard. Operational branches include the Hazmat Capping Branch led by Branch Director Capt. P. Gomez and Deputy Lt. S. Baker, supervising Railcar Entry Group 1 under Capt. D. Ross and the Decontamination Group under Lt. M. Turner; and the Evacuation & Security Branch led by Branch Director Sgt. H. Lin and Deputy Deputy S. Kowalski, supervising the Highway 2 Control Division under Capt. H. Nelson and the Skykomish Evacuation Group under Sgt. R. Hall. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Patricia Ross, supported by Deputy David Sterling. Financial operations are staffed by Time Unit Leader W. Harris, Procurement Unit Leader C. White, Compensation/Claims Unit Leader G. Scott, and Cost Unit Leader E. Green. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Lt. Colonel Alan Vance on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics203_5.txt b/benchmark/datasets/narratives/ics203_5.txt new file mode 100644 index 00000000..8d961ebd --- /dev/null +++ b/benchmark/datasets/narratives/ics203_5.txt @@ -0,0 +1,13 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Organization Assignment List (ICS 203) + +The Organization Assignment List, designated as ICS 203 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, officially documents the active organizational structure and personnel assignments for Operational Period 1 (May 18, 2026, 18:00 hours to May 19, 2026, 06:00 hours). + +Command Staff operations are managed under a Unified Command structure consisting of Incident Commanders OSC C. Martinez representing EPA Region 9, Chief M. Thorne representing San Bernardino County Fire, Chief Inspector A. Kim representing Cal OES, and W. Vance representing Pipeline RP. Executive operational support is provided by Deputy Incident Commander Commander F. Miller. Safety oversight is directed by Safety Officer Captain Gregory Hall, public communications are managed by Public Information Officer Samantha Norris of Cal OES, and inter-agency coordination is handled by Liaison Officer Ranger D. Stevens of State Parks. External agency representatives active at the command post include Dr. L. Arispe representing US Forest Service, Engineer K. Ross representing Mojave Water Agency, and Captain B. Walsh representing Clean Harbors Environmental. + +The Planning Section is led by Planning Section Chief Captain Rachel Brooks, supported by Deputy Megan Taylor. Unit Leaders under Planning include Jason Wu managing the Resources Unit, Battalion Chief Kevin Ross managing the Situation Unit, Dr. L. Arispe managing the Documentation Unit, and Captain B. Walsh managing the Demobilization Unit. Specialized technical advice is provided by Technical Specialists Dr. L. Arispe (Environmental Unit Leader) and Dr. S. Ray (Hydrological Flow Specialist). + +The Logistics Section is commanded by Logistics Section Chief Megan Taylor, supported by Deputy Jason Wu. Logistics is organized into two primary branches: the Support Branch directed by Capt. P. Gomez, overseeing Supply Unit Leader Lt. S. Baker, Facilities Unit Leader Capt. D. Ross, and Ground Support Unit Leader Lt. M. Turner; and the Service Branch directed by Capt. H. Nelson, overseeing Communications Unit Leader Sgt. R. Hall, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Operations Section is directed by Operations Section Chief Battalion Chief Kevin Ross, supported by Deputy Captain B. Walsh. Tactical field units operate from Staging Area Delta located at the Silverwood Lake Marina. Operational branches include the Canyon Pipeline Repair Branch led by Branch Director Capt. Gregory Hall and Deputy Lt. R. Mendez, supervising the Clamp Repair Group under Capt. A. Ross and the Sawpit Containment Group under Lt. C. Webb; and the Reservoir Skimming & Booming Branch led by Branch Director Captain B. Walsh and Deputy Lt. J. Thorne, supervising the South Arm Skimmer Division under Capt. B. Walsh and the Water Intake Protection Group under Dr. L. Arispe. Aviation resources are coordinated through the Air Operations Branch under Air Operations Branch Director Capt. E. Walters. + +The Finance/Administration Section is managed by Finance/Admin Section Chief Jason Wu, supported by Deputy Megan Taylor. Financial operations are staffed by Time Unit Leader J. Myers, Procurement Unit Leader S. Taylor, Compensation/Claims Unit Leader C. Adams, and Cost Unit Leader M. Evans. This organization assignment list serves as Page 1 of the Incident Action Plan package and was prepared by Planning Section Chief Captain Rachel Brooks on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_1.txt b/benchmark/datasets/narratives/ics204_1.txt new file mode 100644 index 00000000..b803e274 --- /dev/null +++ b/benchmark/datasets/narratives/ics204_1.txt @@ -0,0 +1,14 @@ +### Whispering Pines Derailment Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, details the tactical assignments for the Hazmat Entry Group operating within Division A under the Hazardous Materials Branch during Operational Period 1 (July 07, 2026, 18:00 hours to July 08, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief B. Reynolds (Contact: 555-0192 / Radio Ch 1), Branch Director Lt. T. Kincaid (Contact: 555-0144 / Radio Ch 3), and Division/Group Supervisor Capt. P. Gomez (Contact: 555-0188 / Radio Ch 4). Tactical resources staging and deployment are managed out of Staging Area Alpha located at the County Fairgrounds. + +Resources assigned to the Hazmat Entry Group for this shift include: +1. Hazmat Unit HZM-01, led by Lt. T. Kincaid, staffing 6 personnel, contactable via Radio Ch 4 (462.550 MHz). Special instructions require reporting to Hot Zone Gate 1 by 18:30 equipped with Level B SCBA chemical suits, PID air monitors, and non-sparking aluminum tools. +2. Decontamination Trailer DECON-02, led by Lt. S. Baker, staffing 4 personnel, contactable via Radio Ch 4. Special instructions mandate establishing a wet-decontamination station at the Warm Zone boundary Gate 2 by 18:45 utilizing a decon shower trailer with lime wash solution. +3. Fire Engine ENG-41, led by Capt. D. Ross, staffing 3 personnel, contactable via Radio Ch 2. Special instructions require positioning at the Warm Zone boundary for continuous AFFF foam blanket standby during tank car grounding operations. + +The specific work assignments for the Hazmat Entry Group direct personnel to execute Hot Zone entry into the rail right-of-way to secure structural stabilization straps on derailed Car #14, attach grounding cables to prevent static spark ignition, and perform real-time PID air monitoring around the breached Benzene manifold, reporting VOC concentrations to the Branch Director every 15 minutes. + +Special safety instructions mandate that all entry personnel wear Level B PPE with SCBA. Continuous air monitoring is strictly required; personnel must evacuate the Hot Zone immediately if PID readings exceed 5 ppm Benzene or 10% LEL. Formal decontamination shower procedures must be completed prior to exiting the Warm Zone. Communications protocols specify Radio Ch 1 (154.280 MHz) for Command, Radio Ch 4 (462.550 MHz) for Hazmat Tactical, Cell 555-0177 for Safety Officer Direct, and Radio Ch 5 / Cell 555-0199 for Medical Emergency Call. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Marcus Vance on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_2.txt b/benchmark/datasets/narratives/ics204_2.txt new file mode 100644 index 00000000..1ef56c3c --- /dev/null +++ b/benchmark/datasets/narratives/ics204_2.txt @@ -0,0 +1,14 @@ +### Blackwood Chemical Pipeline Breach Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, details the tactical assignments for the Hot Zone Entry Group operating within Division B under the Pipeline Isolation Branch during Operational Period 1 (August 13, 2026, 18:00 hours to August 14, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief D. Kowalski (Contact: 555-0210 / Radio Ch 1), Branch Director Capt. Donald Kross (Contact: 555-0233 / Radio Ch 2), and Division/Group Supervisor Lt. R. Mendez (Contact: 555-0255 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Bravo located at Westside High School. + +Resources assigned to the Hot Zone Entry Group for this shift include: +1. Hazmat Unit HZM-05, led by Lt. C. Webb, staffing 5 personnel, contactable via Radio Ch 3 (467.775 MHz). Special instructions require reporting to Gate 2 by 18:15 equipped with Level A encapsulated SCBA suits, pneumatic pipe clamp, and thermal imaging camera. +2. Fire Engine ENG-12, led by Capt. A. Ross, staffing 4 personnel, contactable via Radio Ch 2. Special instructions mandate deploying unmanned fog nozzles at 500 ft perimeter to maintain water curtains over airborne ammonia plume. +3. Water Tender WT-03, led by Lt. M. Turner, staffing 2 personnel, contactable via Radio Ch 2. Special instructions require supplying continuous water feed to Engine 12 monitors. + +The specific work assignments for the Hot Zone Entry Group direct personnel to enter Hot Zone at Valve Station 14-B under Level A protection to execute manual hot-tap isolation on the 8-inch pressurized line, secure bypass manifold to stop active liquid ammonia discharge, and maintain water curtains downwind during valve mechanical manipulation. + +Special safety instructions mandate Level A PPE for entry team, maintaining a 2-person backup entry team in Level A at Warm Zone line, and sounding emergency withdrawal horn (3 long blasts) if ammonia concentration exceeds 300 ppm IDLH at Warm Zone baseline. Communications protocols specify Radio Ch 1 (153.830 MHz) for Command / Operations, Radio Ch 3 (467.775 MHz) for Pipeline Tactical, Cell 555-0288 for Safety Officer Line, and Radio Ch 6 (462.625 MHz) for Decon Line. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Sarah L. Jenkins on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_3.txt b/benchmark/datasets/narratives/ics204_3.txt new file mode 100644 index 00000000..d8d3fa1c --- /dev/null +++ b/benchmark/datasets/narratives/ics204_3.txt @@ -0,0 +1,14 @@ +### Port of Houston Sulfuric Acid Tanker Leak Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, details the tactical assignments for the Deck Neutralization Group operating within Division C (Berth 42) under the Vessel Salvage & Entry Branch during Operational Period 1 (October 14, 2026, 07:00 hours to October 15, 2026, 19:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief A. Ross (Contact: 555-0312 / Radio Ch 1), Branch Director Lt. J. Thorne (Contact: 555-0344 / Radio Ch 2), and Division/Group Supervisor Lt. S. Baker (Contact: 555-0366 / Radio Ch 4). Tactical resources staging and deployment are managed out of Staging Area Bravo located at Jacintoport Terminal. + +Resources assigned to the Deck Neutralization Group for this shift include: +1. Port Authority Hazmat Team HZM-PORT-1, led by Capt. P. Gomez, staffing 6 personnel, contactable via Radio Ch 4 (453.225 MHz). Special instructions require reporting to Berth 42 Gangway by 07:30 equipped with Level A chemical suits, dry lime blower, and pH test strips. +2. Neutralization Truck Unit NEUT-TRK-05, led by Driver E. Green, staffing 2 personnel, contactable via Radio Ch 4. Special instructions mandate positioning at Berth 42 apron to supply dry sodium bicarbonate powder to deck entry team. +3. Chemical Response Vessel RV-AC-01, led by Capt. B. Walsh, staffing 4 personnel, contactable via Radio Ch 3. Special instructions require conducting continuous water sampling and maintaining acid sorbent boom around vessel hull. + +The specific work assignments for the Deck Neutralization Group direct personnel to board M/T Stolt Synergy under Level A protection, apply dry sodium bicarbonate slurry over 4,200 gallons of pooled sulfuric acid on vessel deck until deck runoff pH stabilizes between 6.5 and 8.5, and inspect offloading manifold for structural stress. + +Special safety instructions mandate a strict 20-minute entry limit per technician due to heat stress (88°F/78% RH), compulsory lime wash decon before unsuiting, and prohibiting direct application of raw water to concentrated acid pool to prevent violent exothermic splattering. Communications protocols specify Radio Ch 1 (156.800 MHz / VHF 16) for Operations Command, Radio Ch 4 (453.225 MHz) for Vessel Tactical, Cell 555-0399 for Port Safety Line, and Radio Ch 5 (462.600 MHz) for Medical Standby. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Commander Richard Croft on October 14, 2026, at 06:00 hours. diff --git a/benchmark/datasets/narratives/ics204_4.txt b/benchmark/datasets/narratives/ics204_4.txt new file mode 100644 index 00000000..568b4f3e --- /dev/null +++ b/benchmark/datasets/narratives/ics204_4.txt @@ -0,0 +1,14 @@ +### Cascade Pass Chlorine Railcar Derailment Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, details the tactical assignments for the Railcar Entry Group 1 operating within Division Ravine under the Hazmat Capping Branch during Operational Period 1 (November 02, 2026, 18:00 hours to November 03, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief T. Higgins (Contact: 555-0411 / Radio Ch 1), Branch Director Capt. P. Gomez (Contact: 555-0433 / Radio Ch 2), and Division/Group Supervisor Capt. D. Ross (Contact: 555-0455 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Charlie located at the Stevens Pass Maintenance Yard. + +Resources assigned to Railcar Entry Group 1 for this shift include: +1. King County Hazmat 3 HZM-33, led by Lt. S. Baker, staffing 5 personnel, contactable via Radio Ch 3 (462.700 MHz). Special instructions require reporting to Ravine Checkpoint by 18:30 equipped with Level A encapsulated suits, Emergency B-Kit capping assembly, and pneumatic torque wrenches. +2. BNSF Emergency Response Unit BNSF-ER-01, led by Spec. G. Peterson, staffing 3 personnel, contactable via Radio Ch 3. Special instructions mandate providing railcar dome technical oversight and heavy rigging hardware. +3. Mobile Decontamination Trailer DECON-04, led by Tech M. Turner, staffing 4 personnel, contactable via Radio Ch 2. Special instructions require operating heated water decontamination trailer at Staging Area Charlie. + +The specific work assignments for Railcar Entry Group 1 direct personnel to descend ravine to derailed railcar BNSF-77402 under Level A protection, mount and torque Emergency B-Kit hood over damaged chlorine angle valve dome, and verify seal integrity using ammonia vapor swab test until zero leakage is detected. + +Special safety instructions mandate precautions for freezing temperature hazards (28°F) and icy slopes. Suit entry teams are limited to 30-minute rotations. Heated decon wash is mandatory to prevent suit freeze-up, and continuous electrochemical chlorine monitoring is required throughout the entry. Communications protocols specify Radio Ch 1 (154.400 MHz) for Command Channel, Radio Ch 3 (462.700 MHz) for Capping Tactical, Cell 555-0488 for Safety Officer Line, and Radio Ch 4 (467.550 MHz) for Evac Control Line. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Lt. Colonel Alan Vance on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics204_5.txt b/benchmark/datasets/narratives/ics204_5.txt new file mode 100644 index 00000000..8baf3210 --- /dev/null +++ b/benchmark/datasets/narratives/ics204_5.txt @@ -0,0 +1,14 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Assignment List (ICS 204) + +The Assignment List, designated as ICS 204 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, details the tactical assignments for the South Arm Skimmer Division operating within Division Reservoir South under the Reservoir Skimming & Booming Branch during Operational Period 1 (May 18, 2026, 18:00 hours to May 19, 2026, 06:00 hours). + +Tactical leadership for this operational unit is provided by Operations Section Chief Battalion Chief Kevin Ross (Contact: 555-0515 / Radio Ch 1), Branch Director Captain B. Walsh (Contact: 555-0535 / Radio Ch 2), and Division/Group Supervisor Lt. J. Thorne (Contact: 555-0560 / Radio Ch 3). Tactical resources staging and deployment are managed out of Staging Area Delta located at the Silverwood Lake Marina. + +Resources assigned to the South Arm Skimmer Division for this shift include: +1. Clean Harbors Spill Vessel RV-LC-01, led by Capt. B. Walsh, staffing 4 personnel, contactable via Radio Ch 3 (156.550 MHz / VHF 11). Special instructions require reporting to Marina Slip 4 by 18:15 equipped with 1,200 ft hard boom, drum skimmer, and high-intensity LED deck searchlights. +2. Heavy Vacuum Skimmer Boat SKIM-02, led by Operator R. Mendez, staffing 3 personnel, contactable via Radio Ch 3. Special instructions mandate operating heavy weir skimmer vessel in South arm pool, pumping oil into a 5,000 gal floating bladder. +3. Air Monitoring Recon Unit AIR-MON-08, led by Tech A. Ross, staffing 2 personnel, contactable via Radio Ch 4. Special instructions require conducting continuous PID benzene air monitoring on boat deck and shoreline baseline. + +The specific work assignments for the South Arm Skimmer Division direct personnel to deploy and anchor 1,200 feet of hard containment boom across Sawpit Canyon inlet to isolate the reservoir, and operate drum skimmers and weir skimmer vessel SKIM-02 continuously throughout the night shift to recover floating crude oil slick in the South arm. + +Special safety instructions mandate USCG-approved PFDs for all over-water personnel. Maintain vessel deck lighting during night operations. If PID readings exceed 1 ppm benzene, mandate half-mask APR organic vapor respirators. Communications protocols specify Radio Ch 1 (154.150 MHz) for Command Channel, Radio Ch 3 (156.550 MHz / VHF 11) for Marine Tactical Channel, Cell 555-0588 for Safety Officer Direct, and Radio Ch 5 (462.575 MHz) for Water Intake Guard. This document serves as Page 4 of the Incident Action Plan and was prepared by Planning Section Chief Captain Rachel Brooks on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205_1.txt b/benchmark/datasets/narratives/ics205_1.txt new file mode 100644 index 00000000..b32bd728 --- /dev/null +++ b/benchmark/datasets/narratives/ics205_1.txt @@ -0,0 +1,11 @@ +### Whispering Pines Derailment Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, was prepared on July 07, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (July 07, 2026, 18:00 hours through July 08, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone 1): Functioned for Command, channel name TAC-1, assigned to Unified Command and Section Chiefs. Operates on RX frequency 154.2800 N (Tone 156.7) and TX frequency 159.4800 N (Tone 156.7) in Analog mode (A), utilizing Repeater 1 located on Ridge Top. +2. Channel 2 (Zone 1): Functioned for Tactical operations, channel name TAC-2, assigned to Hazmat Entry Group. Operates on RX frequency 462.5500 N (Tone 114.8) and TX frequency 467.5500 N (Tone 114.8) in Digital mode (D), utilizing an encrypted digital talkgroup. +3. Channel 3 (Zone 1): Functioned for Tactical operations, channel name TAC-3, assigned to Fire Suppression Branch. Operates on RX frequency 153.8300 N (Tone 156.7) and TX frequency 153.8300 N (Tone 156.7) in Analog mode (A), utilizing direct tactical simplex communications. +4. Channel 4 (Zone 1): Functioned for Support operations, channel name SUPP-1, assigned to Logistics and Decon. Operates on RX frequency 462.6250 N (Tone 123.0) and TX frequency 462.6250 N (Tone 123.0) in Analog mode (A), serving Staging Area Alpha and the decon trailer. + +Special instructions dictate that all emergency radio traffic must use the 'MAYDAY' call sign prefix on Channel 1. Channel 2 encryption keys must be loaded into handheld radios at Staging Area Alpha prior to Hot Zone deployment. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader R. Lee on July 07, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_2.txt b/benchmark/datasets/narratives/ics205_2.txt new file mode 100644 index 00000000..e09c47fd --- /dev/null +++ b/benchmark/datasets/narratives/ics205_2.txt @@ -0,0 +1,11 @@ +### Blackwood Chemical Pipeline Breach Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, was prepared on August 13, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (August 13, 2026, 18:00 hours through August 14, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone A): Functioned for Command, channel name STATE-CMD, assigned to Unified Command and Operations. Operates on RX frequency 153.8300 N (Tone 131.8) and TX frequency 158.9700 N (Tone 131.8) in Analog mode (A), utilizing County Repeater 4. +2. Channel 2 (Zone A): Functioned for Tactical operations, channel name HAZ-TAC-1, assigned to Hot Zone Entry Group. Operates on RX frequency 467.7750 N (Tone D023) and TX frequency 467.7750 N (Tone D023) in Digital mode (D), restricting use strictly to intrinsically safe radios. +3. Channel 3 (Zone A): Functioned for Tactical operations, channel name BOOM-TAC, assigned to River Booming Division. Operates on RX frequency 154.2800 N (Tone 131.8) and TX frequency 154.2800 N (Tone 131.8) in Analog mode (A), serving as a simplex marine and land communications link. +4. Channel 4 (Zone A): Functioned for Dispatch, channel name LAW-DISP, assigned to Perimeter Evacuation and Sheriff's Office. Operates on RX frequency 460.1250 N (Tone 156.7) and TX frequency 465.1250 N (Tone 156.7) in Digital mode (D), linking to County Sheriff Dispatch. + +Special instructions mandate that only intrinsically safe portable radios (Class I, Div 1) are permitted inside the 500-foot Exclusion Zone. Personnel must maintain 30-minute mandatory radio check-ins. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader K. Albright on August 13, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_3.txt b/benchmark/datasets/narratives/ics205_3.txt new file mode 100644 index 00000000..7e187842 --- /dev/null +++ b/benchmark/datasets/narratives/ics205_3.txt @@ -0,0 +1,11 @@ +### Port of Houston Sulfuric Acid Tanker Leak Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, was prepared on October 14, 2026, at 05:30 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (October 14, 2026, 07:00 hours through October 15, 2026, 19:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 16 (Zone Port): Functioned for Command, channel name VHF 16 / CMD, assigned to USCG and Unified Command. Operates on RX frequency 156.8000 W (Tone None) and TX frequency 156.8000 W (Tone None) in Analog mode (A), serving as International Maritime Distress and Command channel. +2. Channel 11 (Zone Port): Functioned for Tactical operations, channel name VHF 11 / OPS, assigned to Vessel Deck Entry Group. Operates on RX frequency 156.5500 W (Tone None) and TX frequency 156.5500 W (Tone None) in Analog mode (A), serving ship-to-shore deck entry operations. +3. Channel 4 (Zone Port): Functioned for Tactical operations, channel name PORT-HAZ, assigned to Chemical Neutralization Team. Operates on RX frequency 453.2250 N (Tone 141.3) and TX frequency 458.2250 N (Tone 141.3) in Digital mode (D), utilizing Port Fire Hazmat digital system. +4. Channel 5 (Zone Port): Functioned for Support operations, channel name MED-NET, assigned to Medical & Decon Operations. Operates on RX frequency 462.6000 N (Tone 100.0) and TX frequency 462.6000 N (Tone 100.0) in Analog mode (A), maintaining communication with decon trailer baseline. + +Special instructions dictate that VHF Channel 16 is reserved strictly for Command and emergency traffic. The deck entry team must maintain dual-watch monitoring on VHF Channel 11 and Port-Haz Channel 4. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader J. Myers on October 14, 2026, at 05:30 hours. diff --git a/benchmark/datasets/narratives/ics205_4.txt b/benchmark/datasets/narratives/ics205_4.txt new file mode 100644 index 00000000..1966788d --- /dev/null +++ b/benchmark/datasets/narratives/ics205_4.txt @@ -0,0 +1,11 @@ +### Cascade Pass Chlorine Railcar Derailment Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, was prepared on November 02, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (November 02, 2026, 18:00 hours through November 03, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone Pass): Functioned for Command, channel name WSP-CMD, assigned to Unified Command and Operations. Operates on RX frequency 154.4000 N (Tone 103.5) and TX frequency 159.0300 N (Tone 103.5) in Analog mode (A), utilizing Stevens Pass Mountain Repeater. +2. Channel 3 (Zone Pass): Functioned for Tactical operations, channel name CAPP-TAC, assigned to Railcar Entry & Capping Team. Operates on RX frequency 462.7000 N (Tone D074) and TX frequency 467.7000 N (Tone D074) in Digital mode (D), utilizing a digital cross-band repeater deployed in the ravine. +3. Channel 4 (Zone Pass): Functioned for Tactical operations, channel name EVAC-TAC, assigned to Highway 2 & Evacuation Security. Operates on RX frequency 467.5500 N (Tone 114.8) and TX frequency 467.5500 N (Tone 114.8) in Analog mode (A), serving Sheriff patrol direct communications. +4. Channel 6 (Zone Pass): Functioned for Support operations, channel name LOG-SUPP, assigned to Decon & Staging Charlie. Operates on RX frequency 151.6250 N (Tone 67.0) and TX frequency 151.6250 N (Tone 67.0) in Analog mode (A), maintaining Staging Area Charlie link. + +Special instructions indicate that a portable cross-band repeater is deployed at the ravine rim to ensure coverage inside mountain signal shadows. Cold-weather battery packs are strictly required for all handheld portable radios. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader B. Foster on November 02, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205_5.txt b/benchmark/datasets/narratives/ics205_5.txt new file mode 100644 index 00000000..de01cf3f --- /dev/null +++ b/benchmark/datasets/narratives/ics205_5.txt @@ -0,0 +1,11 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Incident Radio Communications Plan (ICS 205) + +The Incident Radio Communications Plan, designated as ICS 205 for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, was prepared on May 18, 2026, at 16:00 hours. This communications blueprint governs basic radio channel assignments for Operational Period 1 (May 18, 2026, 18:00 hours through May 19, 2026, 06:00 hours). + +Basic radio channel allocations for this operational period are structured as follows: +1. Channel 1 (Zone Lake): Functioned for Command, channel name FIRE-CMD, assigned to Unified Command and Station 30. Operates on RX frequency 154.1500 N (Tone 146.2) and TX frequency 158.8500 N (Tone 146.2) in Analog mode (A), utilizing County Station 30 Repeater. +2. Channel 3 (Zone Lake): Functioned for Tactical operations, channel name MARINE-11, assigned to Reservoir Skimmer Fleet. Operates on RX frequency 156.5500 W (Tone None) and TX frequency 156.5500 W (Tone None) in Analog mode (A), using VHF Channel 11 Marine Simplex. +3. Channel 4 (Zone Lake): Functioned for Tactical operations, channel name PIPE-TAC, assigned to Canyon Pipeline Repair Group. Operates on RX frequency 462.5750 N (Tone D115) and TX frequency 467.5750 N (Tone D115) in Digital mode (D), linking Sawpit Canyon tactical operations. +4. Channel 5 (Zone Lake): Functioned for Support operations, channel name PARK-SUPP, assigned to State Parks & Water Intake Guard. Operates on RX frequency 151.4000 N (Tone 146.2) and TX frequency 151.4000 N (Tone 146.2) in Analog mode (A), maintaining communication with Mojave Water Agency and Park Rangers. + +Special instructions dictate that all marine vessels must maintain continuous watch on VHF Channel 11. Emergency Mayday calls will be monitored by Command on Channel 1. This document serves as Page 5 of the Incident Action Plan and was prepared by Communications Unit Leader Sgt. R. Hall on May 18, 2026, at 16:00 hours. diff --git a/benchmark/datasets/narratives/ics205a_1.txt b/benchmark/datasets/narratives/ics205a_1.txt new file mode 100644 index 00000000..84517d38 --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_1.txt @@ -0,0 +1,14 @@ +### Whispering Pines Derailment Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Whispering Pines Derailment incident under tracking number US-EPA-R4-2026-0707, functions as the primary incident contact directory for Operational Period 1 (July 07, 2026, 18:00 hours through July 08, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: Incident Commander (County Fire), Name: Chief J. Thomas, Contact Methods: Cell: 555-0101 / Radio Ch 1 / Vehicle: FIRE-CMD-1. +2. Position: Co-Incident Commander (State DEQ), Name: S. Albright, Contact Methods: Cell: 555-0102 / Radio Ch 1. +3. Position: Co-Incident Commander (CSX RP), Name: D. Miller, Contact Methods: Cell: 555-0103 / Radio Ch 1. +4. Position: Safety Officer, Name: Captain R. Mendez, Contact Methods: Cell: 555-0177 / Radio Ch 1 & 4. +5. Position: Operations Section Chief, Name: B. Reynolds, Contact Methods: Cell: 555-0192 / Radio Ch 1. +6. Position: Hazmat Branch Director, Name: Lt. T. Kincaid, Contact Methods: Cell: 555-0144 / Radio Ch 3 & 4 / Vehicle: HZM-01. +7. Position: Planning Section Chief, Name: Marcus Vance, Contact Methods: Cell: 555-0150 / CP Desk Ext: 104. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader R. Lee on July 07, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_2.txt b/benchmark/datasets/narratives/ics205a_2.txt new file mode 100644 index 00000000..e7e74cca --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_2.txt @@ -0,0 +1,14 @@ +### Blackwood Chemical Pipeline Breach Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Blackwood Chemical Pipeline Breach incident under tracking number US-EPA-R5-2026-0813, functions as the primary incident contact directory for Operational Period 1 (August 13, 2026, 18:00 hours through August 14, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: Incident Commander (Fire), Name: Chief R. Vance, Contact Methods: Cell: 555-0201 / Radio Ch 1. +2. Position: Incident Commander (State EPA), Name: Officer M. Ross, Contact Methods: Cell: 555-0202 / Radio Ch 1. +3. Position: Safety Officer, Name: Captain L. Hayes, Contact Methods: Cell: 555-0288 / Radio Ch 1. +4. Position: Operations Section Chief, Name: D. Kowalski, Contact Methods: Cell: 555-0210 / Radio Ch 1. +5. Position: Pipeline Branch Director, Name: Capt. Donald Kross, Contact Methods: Cell: 555-0233 / Radio Ch 2 / Vehicle: HAZ-1. +6. Position: Entry Group Supervisor, Name: Lt. R. Mendez, Contact Methods: Cell: 555-0255 / Radio Ch 3 / Vehicle: HZM-05. +7. Position: Planning Section Chief, Name: Sarah L. Jenkins, Contact Methods: Cell: 555-0250 / CP Ext: 201. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader K. Albright on August 13, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_3.txt b/benchmark/datasets/narratives/ics205a_3.txt new file mode 100644 index 00000000..e77a1467 --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_3.txt @@ -0,0 +1,14 @@ +### Port of Houston Sulfuric Acid Tanker Leak Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Port of Houston Sulfuric Acid Tanker Leak incident under tracking number USCG-EPA-R6-2026-1014, functions as the primary incident contact directory for Operational Period 1 (October 14, 2026, 07:00 hours through October 15, 2026, 19:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: USCG Unified Commander, Name: Captain H. Vance, Contact Methods: Cell: 555-0301 / VHF Ch 16 / Vessel: CG-45601. +2. Position: EPA Co-Incident Commander, Name: OSC M. Reynolds, Contact Methods: Cell: 555-0302 / Radio Ch 1. +3. Position: Safety Officer, Name: Commander Thomas Blake, Contact Methods: Cell: 555-0399 / VHF Ch 16. +4. Position: Operations Section Chief, Name: Battalion Chief A. Ross, Contact Methods: Cell: 555-0312 / VHF Ch 16. +5. Position: Vessel Salvage Branch Director, Name: Lt. J. Thorne, Contact Methods: Cell: 555-0344 / VHF Ch 11. +6. Position: Deck Entry Supervisor, Name: Lt. S. Baker, Contact Methods: Cell: 555-0366 / Radio Ch 4 / Vehicle: HZM-PORT-1. +7. Position: Planning Section Chief, Name: Commander Richard Croft, Contact Methods: Cell: 555-0350 / Sector Ext: 305. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader J. Myers on October 14, 2026, at 06:15 hours. diff --git a/benchmark/datasets/narratives/ics205a_4.txt b/benchmark/datasets/narratives/ics205a_4.txt new file mode 100644 index 00000000..fd6ef8ee --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_4.txt @@ -0,0 +1,14 @@ +### Cascade Pass Chlorine Railcar Derailment Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Cascade Pass Chlorine Railcar Derailment incident under tracking number NTSB-EPA-R10-2026-1102, functions as the primary incident contact directory for Operational Period 1 (November 02, 2026, 18:00 hours through November 03, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: WSP Incident Commander, Name: Captain E. Miller, Contact Methods: Cell: 555-0401 / Radio Ch 1 / Unit: WSP-100. +2. Position: EPA R10 Co-Commander, Name: OSC R. Brooks, Contact Methods: Cell: 555-0402 / Radio Ch 1. +3. Position: Safety Officer, Name: Captain Marcus Vance, Contact Methods: Cell: 555-0488 / Radio Ch 1. +4. Position: Operations Section Chief, Name: Battalion Chief T. Higgins, Contact Methods: Cell: 555-0411 / Radio Ch 1. +5. Position: Hazmat Capping Branch Director, Name: Capt. P. Gomez, Contact Methods: Cell: 555-0433 / Radio Ch 2 & 3. +6. Position: Railcar Entry Supervisor, Name: Capt. D. Ross, Contact Methods: Cell: 555-0455 / Radio Ch 3 / Vehicle: HZM-33. +7. Position: Planning Section Chief, Name: Lt. Colonel Alan Vance, Contact Methods: Cell: 555-0450 / CP Desk: Gym-1. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader B. Foster on November 02, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics205a_5.txt b/benchmark/datasets/narratives/ics205a_5.txt new file mode 100644 index 00000000..bb34ddac --- /dev/null +++ b/benchmark/datasets/narratives/ics205a_5.txt @@ -0,0 +1,14 @@ +### Silverwood Reservoir Crude Oil Pipeline Rupture Communications List (ICS 205A) + +The Communications List, designated as ICS 205A for the Silverwood Reservoir Crude Oil Pipeline Rupture incident under tracking number CalOES-EPA-R9-2026-0518, functions as the primary incident contact directory for Operational Period 1 (May 18, 2026, 18:00 hours through May 19, 2026, 06:00 hours). + +Basic local communications contact methods for assigned incident leadership are established as follows: +1. Position: EPA R9 Incident Commander, Name: OSC C. Martinez, Contact Methods: Cell: 555-0501 / Radio Ch 1. +2. Position: SBCo Fire Incident Commander, Name: Chief M. Thorne, Contact Methods: Cell: 555-0502 / Radio Ch 1 / Vehicle: FIRE-SB-1. +3. Position: Safety Officer, Name: Captain Gregory Hall, Contact Methods: Cell: 555-0588 / Radio Ch 1. +4. Position: Operations Section Chief, Name: Battalion Chief Kevin Ross, Contact Methods: Cell: 555-0515 / Radio Ch 1. +5. Position: Marine Skimming Branch Director, Name: Captain B. Walsh, Contact Methods: Cell: 555-0535 / Radio Ch 3 (VHF 11) / Vessel: RV-LC-01. +6. Position: Canyon Repair Group Supervisor, Name: Lt. J. Thorne, Contact Methods: Cell: 555-0560 / Radio Ch 4 / Vehicle: CREW-04. +7. Position: Planning Section Chief, Name: Captain Rachel Brooks, Contact Methods: Cell: 555-0550 / CP Desk Ext: 403. + +This administrative contact directory serves as Page 6 of the Incident Action Plan package and was prepared by Communications Unit Leader Sgt. R. Hall on May 18, 2026, at 16:30 hours. diff --git a/benchmark/datasets/narratives/ics206_1.txt b/benchmark/datasets/narratives/ics206_1.txt new file mode 100644 index 00000000..7973f942 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_1.txt @@ -0,0 +1,9 @@ +This Medical Plan applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. Medical care and responder health monitoring are established across two primary medical aid stations. The Staging Area Alpha First Aid Station is located at Whispering Pines Creek Staging Area, maintaining contact via 462.5500 MHz / Ch 1, staffed with full paramedic service. The Forward Decon Medical Post is stationed at Derailment Site Perimeter West, communicating via 467.7750 MHz / Ch 2, also providing full paramedic service. + +Ground ambulance transportation services are provided by County Emergency Medical Services Unit 41, located at Whispering Pines Staging Area, reachable at (555) 234-8901 / Ch 1 with paramedic service available. Additionally, Pine Valley Volunteer Fire & Rescue Ambulance 12 is stationed at Station 12 Base, reachable at (555) 234-8902 / Ch 1, providing basic transport without paramedic service. Air ambulance transportation is supported by LifeFlight Air Medical Helicopter, stationed at Regional Trauma Center Helipad, reachable at (555) 999-4321 / VHF 155.340 MHz, with full paramedic service on board. + +Two regional hospital facilities are designated for medical receiving. Whispering Pines Regional Medical Center, located at 100 Medical Center Drive, Whispering Pines, 42.1234 N, 73.5678 W, can be contacted at (555) 789-0100 / Med Channel 3; this facility features an air ambulance helipad, is not a burn center, but is a certified trauma center. State University Burn & Trauma Center, located at 500 University Ave, Capital City, 42.3500 N, 73.8000 W, can be contacted at (555) 789-0900 / Med Channel 5; this facility features an air ambulance helipad, is a designated burn center, and is a certified trauma center. + +Special medical emergency procedures dictate that in the event of a medical emergency or chemical exposure during night operations, responders must immediately notify the Incident Commander and Medical Unit Leader over Command Channel 1. All entry personnel exposed to Benzene must undergo full gross and technical decontamination at the Forward Decon Medical Post before transport. For critical life safety, LifeFlight Air Medical is on standby for immediate evacuation from Landing Zone Alpha located adjacent to Staging Area Alpha. + +This document was prepared by Dr. Evelyn Reed, Medical Unit Leader, signed Evelyn Reed on 07/07/2026 16:30, and approved by Safety Officer Captain Thomas Wright, signed Thomas Wright on 07/07/2026 17:00. This form is assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_2.txt b/benchmark/datasets/narratives/ics206_2.txt new file mode 100644 index 00000000..c26d63f2 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_2.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and ending 08/14/2026 at 06:00. Emergency health oversight and responder medical monitoring are supported by two designated medical aid stations. The Blackwood Command Post Medical Station is located inside Station 12 Briefing Trailer, communicating over 453.2125 MHz / Ch 1, fully equipped with paramedic service. The Hot Zone Emergency Decon Aid Station is located at Pipeline Gate 4 Access Point, communicating over 458.2125 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation services are operated by Blackwood County EMS Unit 10, stationed at Station 12 Staging Area, accessible via (555) 345-6789 / Ch 1 with paramedic service. Metro West Medic 5 is located at Route 104 Checkpoint, accessible via (555) 345-6790 / Ch 1, providing paramedic service. Air ambulance support is provided by AirEvac Response Helicopter 3, located at Blackwood Airport Helipad, reachable at (555) 888-1234 / VHF 155.400 MHz, staffed with paramedic service. + +Medical receiving facilities include two regional medical centers. Blackwood Community Hospital, located at 45 River Road, Blackwood, 39.8765 N, 84.1234 W, can be reached at (555) 678-1100 / Med Net 1; it features an air ambulance helipad, is not a burn center, but operates as a certified trauma center. Valley Regional Toxicology & Trauma Center, located at 1200 Health Park Way, Dayton, 39.7500 N, 84.2000 W, can be reached at (555) 678-9900 / Med Net 4; it features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that in the event of an acute respiratory exposure to Anhydrous Ammonia, responders must immediately evacuate the patient upwind to the Hot Zone Emergency Decon Aid Station at Gate 4. Continuous high-flow oxygen administration and eye/skin water irrigation must commence immediately during technical decon. AirEvac Response Helicopter 3 is available at LZ Bravo near Gate 4 for rapid transfer to Valley Regional Toxicology Center. + +Prepared by Dr. Aris Thorne, Medical Unit Leader, signed Aris Thorne on 08/13/2026 16:30, and approved by Safety Officer Mark Davis, signed Mark Davis on 08/13/2026 17:15. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_3.txt b/benchmark/datasets/narratives/ics206_3.txt new file mode 100644 index 00000000..d94908d3 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_3.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. On-site responder health evaluation and emergency care are administered by two primary medical aid stations. The Berth 42 Decon & Medical Station is located at Berth 42 Dockside Command Post, communicating over 156.800 MHz / Ch 16, offering full paramedic service. The M/T Stolt Synergy Deck Medical Post is located at Vessel Starboard Main Deck, communicating over 467.525 MHz / Ch 3, also staffed with paramedic service. + +Ground ambulance transportation services are provided by Houston Fire Department Medic 42, stationed at Port Gate 5 Security Staging, reachable at (555) 456-7890 / Ch 1 with paramedic service. Harris County Emergency Corps Unit 18 is located at Channelview Staging Area, reachable at (555) 456-7891 / Ch 1, providing paramedic service. Air ambulance transport is supported by Memorial Hermann Life Flight, located at Houston Medical Center Helipad, accessible at (555) 777-9111 / VHF 155.280 MHz, offering advanced paramedic service. + +Two specialized medical receiving facilities are designated for transport. Houston Methodist Hospital Baytown, located at 4401 Garth Road, Baytown, 29.7355 N, 94.9774 W, can be contacted at (555) 890-2200 / Marine Channel 22; this facility has an air ambulance helipad, is not a burn center, but is a certified trauma center. Memorial Hermann Texas Medical Center, located at 6411 Fannin St, Houston, 29.7108 N, 95.3986 W, can be contacted at (555) 890-9900 / Marine Channel 24; this facility features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures dictate that given high ambient heat and humidity during Level A chemical suit entry, responder entry times are strictly capped at 20 minutes followed by 40 minutes active cooling and hydration. In the event of chemical acid splashes or suit breaches, perform instant water deluge at the Berth 42 Decon Station for a minimum of 15 minutes. Memorial Hermann Life Flight is designated for critical chemical burn transport from Berth 42 Pier Helipad. + +Prepared by LT Commander James Miller, Medical Unit Leader, signed James Miller on 10/14/2026 06:00, and approved by Safety Officer Sarah Jenkins, signed Sarah Jenkins on 10/14/2026 06:30. Assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_4.txt b/benchmark/datasets/narratives/ics206_4.txt new file mode 100644 index 00000000..81da8106 --- /dev/null +++ b/benchmark/datasets/narratives/ics206_4.txt @@ -0,0 +1,9 @@ +This Medical Plan applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. On-scene emergency medical support and responder health protection are maintained by two medical aid stations. The Staging Area Charlie Medical Station is located at Skykomish School Gym Briefing Room, communicating on 461.2250 MHz / Ch 1, providing full paramedic service. The Warm Decon Hydration & Medical Aid Station is located at State Route 2 Derailment Overlook, communicating on 466.2250 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation is supplied by Cascade Regional EMS Medic 3, located at Staging Area Charlie, reachable via (555) 567-8901 / Ch 1 with paramedic service. Skykomish Volunteer Fire Department Ambulance 8 is located at Skykomish Fire Station, reachable via (555) 567-8902 / Ch 1, providing transport without paramedic service. Air ambulance transport is assigned to Airlift Northwest Helicopter 1, stationed at Arlington Regional Helipad, reachable at (555) 666-5432 / VHF 155.355 MHz, equipped with paramedic service. + +Medical care facilities receiving incident transport include two regional centers. Everett General Hospital, located at 1330 Colby Ave, Everett, 47.9789 N, 122.2012 W, can be contacted at (555) 901-3300 / State EMS Ch 4; this hospital has an air ambulance helipad, is not a burn center, but is a certified trauma center. Harborview Medical Center, located at 325 9th Ave, Seattle, 47.6042 N, 122.3242 W, can be contacted at (555) 901-9900 / State EMS Ch 8; this hospital features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that under freezing night conditions (28°F overnight) and toxic Chlorine threat, heated water decontamination trailers and active warming blankets must be operated continuously at the Warm Decon Station to prevent suit icing and hypothermia. Any exposure to Chlorine vapor requires immediate inhalation treatment with humidified oxygen and urgent transport to Harborview Medical Center. Airlift Northwest Helicopter 1 is positioned at Skykomish High School Football Field LZ. + +Prepared by Captain David Miller, Medical Unit Leader, signed David Miller on 11/02/2026 16:30, and approved by Safety Officer Carl Stevens, signed Carl Stevens on 11/02/2026 17:15. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics206_5.txt b/benchmark/datasets/narratives/ics206_5.txt new file mode 100644 index 00000000..07e14fca --- /dev/null +++ b/benchmark/datasets/narratives/ics206_5.txt @@ -0,0 +1,9 @@ +This Medical Plan governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. On-scene emergency medical support and responder health protection are maintained by two medical aid stations. The ICP Main Medical Station is located at Hesperia Fire Station 30, communicating on 460.1250 MHz / Ch 1, providing full paramedic service. The Sawpit Canyon Water Rescue & Medical Aid Station is located at Sawpit Canyon Boat Launch, communicating on 465.1250 MHz / Ch 2, also providing paramedic service. + +Ground ambulance transportation is supplied by San Bernardino County Fire Medic 30, located at Hesperia Fire Station 30, reachable via (555) 678-9012 / Ch 1 with paramedic service. Desert Ambulance Unit 14 is located at Silverwood Lake Main Entrance, reachable via (555) 678-9013 / Ch 1, providing transport without paramedic service. Air ambulance transport is assigned to Mercy Air Helicopter 2, stationed at Victorville Regional Airport Helipad, reachable at (555) 444-8765 / VHF 155.340 MHz, equipped with paramedic service. + +Medical care facilities receiving incident transport include two regional centers. Desert Valley Hospital, located at 16850 Bear Valley Rd, Victorville, 34.4712 N, 117.2954 W, can be contacted at (555) 012-4400 / County Med Net 2; this hospital has an air ambulance helipad, is not a burn center, but is a certified trauma center. Loma Linda University Medical Center, located at 11234 Anderson St, Loma Linda, 34.0489 N, 117.2641 W, can be contacted at (555) 012-9900 / County Med Net 9; this hospital features an air ambulance helipad, operates a specialized burn center, and is a certified trauma center. + +Special medical emergency procedures mandate that night operations on water present dual risks of hydrocarbon skin absorption/ingestion and water submersion. All personnel operating on skimmer vessels or boom lines must wear USCG-approved PFDs and safety tether lines. In case of accidental water entry or oil ingestion, immediately transport the patient to Sawpit Canyon Water Rescue Medical Aid Station for emergency decon and airway management. Mercy Air Helicopter 2 is on standby at Hesperia Fire Station 30 Helipad. + +Prepared by Dr. Lisa Martinez, Medical Unit Leader, signed Lisa Martinez on 05/18/2026 16:30, and approved by Safety Officer Frank Owens, signed Frank Owens on 05/18/2026 17:00. Form assigned IAP Page Number 5. diff --git a/benchmark/datasets/narratives/ics207_1.txt b/benchmark/datasets/narratives/ics207_1.txt new file mode 100644 index 00000000..86cb317e --- /dev/null +++ b/benchmark/datasets/narratives/ics207_1.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. Unified Command leadership is held by Incident Commanders Chief J. Thomas (County Fire), S. Albright (State DEQ), and D. Miller (CSX Railroad RP). The Command Staff consists of Safety Officer Captain R. Mendez, Public Information Officer A. Cho (County OEM), and Liaison Officer Inspector G. Sims (SO). + +The Operations Section is directed by Operations Section Chief B. Reynolds, with Staging Area Alpha Manager stationed at the County Fairgrounds. Operational elements under Operations include Hazardous Materials Branch Director Lt. T. Kincaid, Hazmat Entry Group Supervisor Capt. P. Gomez, Decontamination Group Supervisor Lt. S. Baker, and Fire Suppression Branch Director Batt. Chief E. Walters. + +The Planning Section is led by Planning Section Chief Marcus Vance, supported by Resources Unit Leader T. Jenkins, Situation Unit Leader E. Brooks, Documentation Unit Leader H. Martinez, and Demobilization Unit Leader R. Sterling. + +The Logistics Section is headed by Logistics Section Chief K. Dunavan. Under the Support Branch, managed by Director J. Myers, key units include Supply Unit Leader S. Taylor, Facilities Unit Leader C. Adams, and Ground Support Unit Leader M. Evans. Under the Service Branch, managed by Director B. Foster, unit leads are Communications Unit Leader R. Lee, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Robert Sterling, supervising Time Unit Leader C. White, Procurement Unit Leader G. Scott, Comp/Claims Unit Leader E. Green, and Cost Unit Leader W. Harris. + +This organization chart was prepared by T. Jenkins, Resources Unit Leader, signed T. Jenkins on 07/07/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_2.txt b/benchmark/datasets/narratives/ics207_2.txt new file mode 100644 index 00000000..ad79b233 --- /dev/null +++ b/benchmark/datasets/narratives/ics207_2.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing covers the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and concluding 08/14/2026 at 06:00. Unified Command leadership is staffed by Incident Commanders Chief R. Vance (Blackwood Fire Dept), Officer M. Ross (State EPA), and J. Vance (Blackwood Pipeline Co. RP). Command Staff includes Safety Officer Captain L. Hayes, Public Information Officer E. Wright, and Liaison Officer Deputy K. Miller. + +The Operations Section is directed by Operations Section Chief D. Kowalski, alongside Staging Area Bravo Manager. Key field leaders under Operations include Pipeline Isolation Branch Director Capt. Donald Kross, Hot Zone Entry Group Supervisor Lt. R. Mendez, Vapor Suppression Group Supervisor Capt. A. Ross, and River Spill Containment Branch Director Commander Thomas Blake. + +The Planning Section is led by Planning Section Chief Sarah L. Jenkins, with Resources Unit Leader A. Patel, Situation Unit Leader C. Webb, Documentation Unit Leader Dr. H. Thorne, and Demobilization Unit Leader M. Brody. + +The Logistics Section is headed by Logistics Section Chief T. Bradley. The Support Branch, managed by Director G. Kross, consists of Supply Unit Leader H. Lin, Facilities Unit Leader R. Sterling, and Ground Support Unit Leader D. Miller. The Service Branch, managed by Director S. Norris, comprises Communications Unit Leader K. Albright, Medical Unit Leader Dr. T. Vance, and Food Unit Leader A. Cho. + +The Finance/Administration Section is directed by Finance/Admin Section Chief A. Patel, supervising Time Unit Leader J. Mercer, Procurement Unit Leader Laura Martinez, Comp/Claims Unit Leader Inspector R. Sterling, and Cost Unit Leader Sandra Keller. + +Prepared by A. Patel, Resources Unit Leader, signed A. Patel on 08/13/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_3.txt b/benchmark/datasets/narratives/ics207_3.txt new file mode 100644 index 00000000..9ef2699b --- /dev/null +++ b/benchmark/datasets/narratives/ics207_3.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing applies to the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. Unified Command leadership comprises Captain H. Vance (USCG Unified Command IC), OSC M. Reynolds (EPA Co-IC), Chief D. Garcia (Port Authority Fire IC), and K. Lindqvist (Stolt Tankers RP IC). The Command Staff consists of Safety Officer Commander Thomas Blake, Public Information Officer Laura Martinez (Port Authority), and Liaison Officer Inspector R. Sterling (TCEQ). + +The Operations Section is directed by Operations Section Chief Battalion Chief A. Ross, supported by Staging Area Bravo Manager at Jacintoport Terminal. Field branch and group leaders include Vessel Salvage & Entry Branch Director Lt. J. Thorne, Deck Neutralization Group Supervisor Lt. S. Baker, Manifold Isolation Team Supervisor Capt. D. Ross, and Ship Channel Protection Branch Director Capt. Donald Kross. + +The Planning Section is led by Planning Section Chief Commander Richard Croft, with Resources Unit Leader Dr. V. Patel, Situation Unit Leader Sandra Keller, Documentation Unit Leader Wayne Miller, and Demobilization Unit Leader Battalion Chief A. Ross. + +The Logistics Section is headed by Logistics Section Chief Sandra Keller. Under Support Branch Director Capt. H. Nelson, units are staffed by Supply Unit Leader C. White, Facilities Unit Leader G. Scott, and Ground Support Unit Leader E. Green. Under Service Branch Director W. Harris, unit leads are Communications Unit Leader J. Myers, Medical Unit Leader Dr. A. Chen, and Food Unit Leader S. Taylor. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Wayne Miller, supervising Time Unit Leader C. Adams, Procurement Unit Leader M. Evans, Comp/Claims Unit Leader B. Foster, and Cost Unit Leader R. Lee. + +Prepared by Dr. V. Patel, Resources Unit Leader, signed Dr. V. Patel on 10/14/2026 06:00. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_4.txt b/benchmark/datasets/narratives/ics207_4.txt new file mode 100644 index 00000000..8262afe6 --- /dev/null +++ b/benchmark/datasets/narratives/ics207_4.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. Unified Command leadership is held by Incident Commanders Captain E. Miller (WSP Unified Command IC), OSC R. Brooks (EPA R10 Co-IC), Chief D. Olson (Skykomish Fire IC), and M. Campbell (BNSF Railway RP IC). Command Staff includes Safety Officer Captain Marcus Vance, Public Information Officer Jennifer Hayes (WSDOT), and Liaison Officer Deputy S. Kowalski (KCSO). + +The Operations Section is directed by Operations Section Chief Battalion Chief T. Higgins, with Staging Area Charlie Manager at Stevens Pass Yard. Tactical leadership positions include Hazmat Capping Branch Director Capt. P. Gomez, Railcar Entry Group 1 Supervisor Capt. D. Ross, Decontamination Group Supervisor Lt. M. Turner, and Evacuation & Security Branch Director Sgt. H. Lin. + +The Planning Section is led by Planning Section Chief Lt. Colonel Alan Vance, supported by Resources Unit Leader Patricia Ross, Situation Unit Leader Battalion Chief T. Higgins, Documentation Unit Leader Sgt. H. Lin, and Demobilization Unit Leader G. Peterson. + +The Logistics Section is headed by Logistics Section Chief David Sterling. Support Branch Director L. King oversees Supply Unit Leader J. Myers, Facilities Unit Leader S. Taylor, and Ground Support Unit Leader C. Adams. Service Branch Director M. Evans oversees Communications Unit Leader B. Foster, Medical Unit Leader Dr. N. Howard, and Food Unit Leader R. Lee. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Patricia Ross, managing Time Unit Leader W. Harris, Procurement Unit Leader C. White, Comp/Claims Unit Leader G. Scott, and Cost Unit Leader E. Green. + +Prepared by Patricia Ross, Resources Unit Leader, signed Patricia Ross on 11/02/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics207_5.txt b/benchmark/datasets/narratives/ics207_5.txt new file mode 100644 index 00000000..b4f9b25b --- /dev/null +++ b/benchmark/datasets/narratives/ics207_5.txt @@ -0,0 +1,11 @@ +This Incident Organization Chart briefing governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. Unified Command leadership comprises Incident Commanders OSC C. Martinez (EPA R9 Co-IC), Chief M. Thorne (SBCo Fire IC), Chief Inspector A. Kim (Cal OES), and W. Vance (Pipeline RP IC). The Command Staff consists of Safety Officer Captain Gregory Hall, Public Information Officer Samantha Norris (Cal OES), and Liaison Officer Ranger D. Stevens (State Parks). + +The Operations Section is directed by Operations Section Chief Battalion Chief Kevin Ross, supported by Staging Area Delta Manager. Operational group leads include Canyon Pipeline Repair Branch Director Capt. Gregory Hall, Clamp Repair Group Supervisor Capt. A. Ross, Reservoir Skimming & Booming Branch Director Captain B. Walsh, and South Arm Skimmer Division Supervisor Capt. B. Walsh. + +The Planning Section is led by Planning Section Chief Captain Rachel Brooks, with Resources Unit Leader Jason Wu, Situation Unit Leader Battalion Chief Kevin Ross, Documentation Unit Leader Dr. L. Arispe, and Demobilization Unit Leader Captain B. Walsh. + +The Logistics Section is headed by Logistics Section Chief Megan Taylor. Under Support Branch Director Capt. P. Gomez, unit leads are Supply Unit Leader Lt. S. Baker, Facilities Unit Leader Capt. D. Ross, and Ground Support Unit Leader Lt. M. Turner. Under Service Branch Director Capt. H. Nelson, unit leads are Communications Unit Leader Sgt. R. Hall, Medical Unit Leader Dr. N. Howard, and Food Unit Leader L. King. + +The Finance/Administration Section is directed by Finance/Admin Section Chief Jason Wu, supervising Time Unit Leader J. Myers, Procurement Unit Leader S. Taylor, Comp/Claims Unit Leader C. Adams, and Cost Unit Leader M. Evans. + +Prepared by Jason Wu, Resources Unit Leader, signed Jason Wu on 05/18/2026 16:30. Form assigned IAP Page Number 4. diff --git a/benchmark/datasets/narratives/ics208_1.txt b/benchmark/datasets/narratives/ics208_1.txt new file mode 100644 index 00000000..8c0c6d74 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_1.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) applies to the Whispering Pines Derailment operational period starting 07/07/2026 at 18:00 and concluding 07/08/2026 at 06:00. + +The primary safety message directs entry teams to place primary tactical focus on air-monitoring metrics and localized vapor suppression, particularly during the transition into night-shift hours when atmospheric inversion layers can trap Benzene vapors closer to the ground. Mandate Level B SCBA PPE within the 300-foot exclusion zone. Mandatory buddy system and continuous photoionization detector air monitoring required for all entry crews along creek banks. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Mobile Command Post inside the Forward Operations Briefing Trailer. + +This form was prepared by Captain R. Mendez, Safety Officer, signed R. Mendez on date and time 07/07/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_2.txt b/benchmark/datasets/narratives/ics208_2.txt new file mode 100644 index 00000000..a297c3b7 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_2.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) covers the Blackwood Chemical Pipeline Breach operational period starting 08/13/2026 at 18:00 and ending 08/14/2026 at 06:00. + +The safety message mandates: Focus tactical operations on maintaining river boom integrity under rising night tides and ensuring strict atmospheric air monitoring during the overnight temperature drop when Anhydrous Ammonia vapors hug low ground. SCBA Level B PPE is mandatory within the 500-foot exclusion zone. Vigilance required for reduced visibility, riverbank slip hazards, and toxic vapor pockets. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Mobile Command Post Briefing Trailer at Station 12. + +This document was prepared by Captain L. Hayes, Safety Officer, signed L. Hayes on date and time 08/13/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_3.txt b/benchmark/datasets/narratives/ics208_3.txt new file mode 100644 index 00000000..ac9fa901 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_3.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) governs the Port of Houston Sulfuric Acid Tanker Leak operational period starting 10/14/2026 at 07:00 and concluding 10/15/2026 at 19:00. + +The safety directive dictates: Primary tactical focus is placed on safe chemical neutralization and pH stabilization in the ship channel water column. Mandatory Level A chemical suit entry procedures enforced for all vessel deck operations. Due to extreme daytime heat (88°F, 78% humidity), entry work cycles are capped at 20 minutes active entry followed by 40 minutes active hydration and cooling. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at USCG Sector Houston Command Center Safety Office. + +Prepared by Commander Thomas Blake, Safety Officer, signed Thomas Blake on date and time 10/14/2026 06:00. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_4.txt b/benchmark/datasets/narratives/ics208_4.txt new file mode 100644 index 00000000..154fe178 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_4.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) applies to the Cascade Pass Chlorine Railcar Derailment operational period starting 11/02/2026 at 18:00 and concluding 11/03/2026 at 06:00. + +The safety message emphasizes: Command emphasizes absolute safety of capping entry teams working under nighttime freezing conditions (28°F overnight). Enforce mandatory Level A PPE compliance and buddy system protocols. Heated decontamination water and warm hydration must be maintained continuously to prevent suit icing and hypothermia. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Skykomish School Gym Operations Briefing Room. + +Prepared by Captain Marcus Vance, Safety Officer, signed Marcus Vance on date and time 11/02/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics208_5.txt b/benchmark/datasets/narratives/ics208_5.txt new file mode 100644 index 00000000..a8551d89 --- /dev/null +++ b/benchmark/datasets/narratives/ics208_5.txt @@ -0,0 +1,7 @@ +This Safety Message and Plan (ICS 208) governs the Silverwood Reservoir Crude Oil Pipeline Rupture operational period starting 05/18/2026 at 18:00 and concluding 05/19/2026 at 06:00. + +The safety directive states: Focus tactical priority on preventing any oil migration toward the municipal water intake. Night skimming operations must maintain illuminated safety perimeters and 100% life-vest compliance at all times. High vigilance required for steep, oil-covered riprap banks and wildlife hazards. + +A site safety plan is required for this incident (true). The approved Site Safety Plan is located at Incident Command Post at Hesperia Fire Station 30. + +Prepared by Captain Gregory Hall, Safety Officer, signed Gregory Hall on date and time 05/18/2026 16:30. Form assigned IAP Page Number 6. diff --git a/benchmark/datasets/narratives/ics213_1.txt b/benchmark/datasets/narratives/ics213_1.txt new file mode 100644 index 00000000..ecf8a133 --- /dev/null +++ b/benchmark/datasets/narratives/ics213_1.txt @@ -0,0 +1,5 @@ +This General Message form applies to the Whispering Pines Derailment incident. The message is addressed to Marcus Vance, Planning Section Chief, sent from Commander Eric Sterling, Operations Section Chief, regarding the subject Request for Additional Vapor Suppression Foam and Boom Resources. The message was recorded on date 07/07/2026 at time 19:15. + +The message reads as follows: Due to increased ambient vapor concentrations of Benzene detected downwind along Whispering Pines Creek, Operations urgently requests five additional totes (1,250 gallons) of fluoroprotein vapor suppression foam and 500 feet of sorbent boom to reinforce Checkpoint Bravo before 22:00 hours. The message was approved by Chief J. Thomas, Incident Commander, signed Chief J. Thomas. + +The formal reply states: Supply Unit has authorized immediate dispatch of five totes of foam and 500 feet of sorbent boom from Regional Logistics Depot. ETA to Staging Area Alpha is 21:00 hours. The reply was submitted by Marcus Vance, Planning Section Chief, signed Marcus Vance on date and time 07/07/2026 19:45. diff --git a/benchmark/datasets/narratives/ics213_2.txt b/benchmark/datasets/narratives/ics213_2.txt new file mode 100644 index 00000000..c2041b44 --- /dev/null +++ b/benchmark/datasets/narratives/ics213_2.txt @@ -0,0 +1,5 @@ +This General Message document is prepared for the Blackwood Chemical Pipeline Breach incident. The message is directed to Sarah L. Jenkins, Planning Section Chief, from Division Supervisor Alan Cole, Division A Supervisor, regarding the subject Downstream Municipal Water Intake Precautionary Shutoff Notice. The message was transmitted on date 08/13/2026 at time 19:30. + +The message content specifies: Field water monitoring team at Boom Site 2 reports low-level dissolved ammonia readings reaching 0.05 ppm near Blackwood River mile 14. Recommend immediately notifying Blackwood Water Authority to initiate precautionary intake gate closure. Approval was granted by Chief R. Henderson, Incident Commander, signed Chief R. Henderson. + +The message reply states: Liaison Officer contacted Blackwood Water Authority at 19:50 hours. Water intake gates closed at 20:00 hours. Alternate reservoir supply activated. The reply was signed and completed by Sarah L. Jenkins, Planning Section Chief, signed Sarah L. Jenkins on date and time 08/13/2026 20:10. diff --git a/benchmark/datasets/narratives/ics213_3.txt b/benchmark/datasets/narratives/ics213_3.txt new file mode 100644 index 00000000..d3d788aa --- /dev/null +++ b/benchmark/datasets/narratives/ics213_3.txt @@ -0,0 +1,5 @@ +This General Message covers the Port of Houston Sulfuric Acid Tanker Leak response. The message is sent to Commander Richard Croft, Planning Section Chief, from Hazmat Group Supervisor Mark Ross, Hazmat Group Supervisor, regarding subject Authorization for Sodium Bicarbonate Neutralization Slurry Application. Sent on date 10/14/2026 at time 08:45. + +The message details: Hazmat entry team has contained deck pooling at Berth 42. Request formal authorization from UC to apply 10 tons of dry sodium bicarbonate slurry to deck pooling to neutralize concentrated sulfuric acid prior to washdown. Approved by Captain H. Vance, Incident Commander, signed Captain H. Vance. + +The message reply states: Unified Command approves application of 10 tons sodium bicarbonate slurry. Technical Specialists from TCEQ are monitoring runoff pH at Berth 42 outfall. The response was authorized by Commander Richard Croft, Planning Section Chief, signed Richard Croft on date and time 10/14/2026 09:15. diff --git a/benchmark/datasets/narratives/ics213_4.txt b/benchmark/datasets/narratives/ics213_4.txt new file mode 100644 index 00000000..f2d3237d --- /dev/null +++ b/benchmark/datasets/narratives/ics213_4.txt @@ -0,0 +1,5 @@ +This General Message is filed for the Cascade Pass Chlorine Railcar Derailment incident. Addressed to Lt. Colonel Alan Vance, Planning Section Chief, sent from Rail Tactical Specialist George Miller, Technical Specialist, regarding the subject Urgent Transport Request for Emergency B-Kit Capping Assembly. Sent on date 11/02/2026 at time 19:00. + +The message states: Capping team at car BNSF-77402 requires specialized torque wrench set and secondary seal gaskets for Emergency B-Kit capping assembly. Request immediate dispatch from Staging Area Charlie via all-terrain vehicle. Approved by Captain E. Miller, Incident Commander, signed Captain E. Miller. + +The reply text reads: Ground Support Unit dispatched ATV-02 with requested torque wrench set and B-Kit gaskets at 19:20 hours. ETA to railcar site is 19:45 hours. Signed by Lt. Colonel Alan Vance, Planning Section Chief, signed Alan Vance on date and time 11/02/2026 19:30. diff --git a/benchmark/datasets/narratives/ics213_5.txt b/benchmark/datasets/narratives/ics213_5.txt new file mode 100644 index 00000000..34c0713a --- /dev/null +++ b/benchmark/datasets/narratives/ics213_5.txt @@ -0,0 +1,5 @@ +This General Message document concerns the Silverwood Reservoir Crude Oil Pipeline Rupture incident. The message is sent to Rachel Brooks, Planning Section Chief, from Skimmer Operations Leader Frank Gomez, Operations Group Supervisor, regarding the subject Skimmer Vessel Relocation and Sorbent Boom Realignment. Prepared on date 05/18/2026 at time 20:00. + +The message content reads: Due to shifts in surface currents toward Sawpit Canyon inlet, requesting authorization to reposition Skimmer Vessel RV-LC-01 to South Arm and deploy an additional 500 feet of hard containment boom by 22:00 hours. The message was approved by Chief M. Thorne, Incident Commander, signed Chief M. Thorne. + +The message reply states: Operations Chief approves relocation of RV-LC-01 and deployment of 500 feet hard boom. Work Boat 3 assigned to assist boom rigging. The reply was executed by Rachel Brooks, Planning Section Chief, signed Rachel Brooks on date and time 05/18/2026 20:30. diff --git a/benchmark/datasets/pdfs/ics_201.pdf b/benchmark/datasets/pdfs/ics_201.pdf new file mode 100644 index 00000000..fe26bef5 Binary files /dev/null and b/benchmark/datasets/pdfs/ics_201.pdf differ diff --git a/benchmark/datasets/pdfs/ics_202.pdf b/benchmark/datasets/pdfs/ics_202.pdf new file mode 100644 index 00000000..3aa02ebc Binary files /dev/null and b/benchmark/datasets/pdfs/ics_202.pdf differ diff --git a/benchmark/datasets/pdfs/ics_203.pdf b/benchmark/datasets/pdfs/ics_203.pdf new file mode 100644 index 00000000..b813e257 Binary files /dev/null and b/benchmark/datasets/pdfs/ics_203.pdf differ diff --git a/benchmark/datasets/pdfs/ics_204.pdf b/benchmark/datasets/pdfs/ics_204.pdf new file mode 100644 index 00000000..9444ca76 Binary files /dev/null and b/benchmark/datasets/pdfs/ics_204.pdf differ diff --git a/benchmark/datasets/pdfs/ics_205.pdf b/benchmark/datasets/pdfs/ics_205.pdf new file mode 100644 index 00000000..9fcd321d Binary files /dev/null and b/benchmark/datasets/pdfs/ics_205.pdf differ diff --git a/benchmark/datasets/pdfs/ics_205a.pdf b/benchmark/datasets/pdfs/ics_205a.pdf new file mode 100644 index 00000000..f175338f Binary files /dev/null and b/benchmark/datasets/pdfs/ics_205a.pdf differ diff --git a/benchmark/datasets/pdfs/ics_206.pdf b/benchmark/datasets/pdfs/ics_206.pdf new file mode 100644 index 00000000..885aa89b Binary files /dev/null and b/benchmark/datasets/pdfs/ics_206.pdf differ diff --git a/benchmark/datasets/pdfs/ics_207.pdf b/benchmark/datasets/pdfs/ics_207.pdf new file mode 100644 index 00000000..b39f64f5 Binary files /dev/null and b/benchmark/datasets/pdfs/ics_207.pdf differ diff --git a/benchmark/datasets/pdfs/ics_208.pdf b/benchmark/datasets/pdfs/ics_208.pdf new file mode 100644 index 00000000..362cbb4e Binary files /dev/null and b/benchmark/datasets/pdfs/ics_208.pdf differ diff --git a/benchmark/datasets/pdfs/ics_213.pdf b/benchmark/datasets/pdfs/ics_213.pdf new file mode 100644 index 00000000..6fccb255 Binary files /dev/null and b/benchmark/datasets/pdfs/ics_213.pdf differ diff --git a/benchmark/datasets/templates/ics_201.json b/benchmark/datasets/templates/ics_201.json new file mode 100644 index 00000000..f308358c --- /dev/null +++ b/benchmark/datasets/templates/ics_201.json @@ -0,0 +1,62 @@ +{ + "1_incident_name": "string", + "2_incident_number": "string", + "3_date_time_initiated": { + "date": "string", + "time": "string" + }, + "4_map_sketch": { + "total_area_of_operations": "string", + "incident_site_area": "string", + "impacted_areas": "string", + "threatened_areas": "string", + "overflight_results": "string", + "trajectories": "string", + "impacted_shorelines": "string", + "graphics_and_symbology": "string" + }, + "5_situation_summary_health_safety_briefing": { + "situation_summary": "string", + "health_and_safety_hazards": "string", + "necessary_measures": "string" + }, + "6_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "7_current_and_planned_objectives": ["string"], + "8_current_and_planned_actions_strategies_tactics": [ + { + "time": "string", + "actions": "string" + } + ], + "9_current_organization": { + "incident_commanders": ["string"], + "safety_officer": "string", + "public_information_officer": "string", + "liaison_officer": "string", + "operations_section_chief": "string", + "planning_section_chief": "string", + "logistics_section_chief": "string", + "finance_administration_section_chief": "string", + "additional_positions": [ + { + "position": "string", + "name": "string" + } + ] + }, + "10_resource_summary": [ + { + "resource": "string", + "resource_identifier": "string", + "date_time_ordered": "string", + "eta": "string", + "arrived": "boolean", + "notes": "string" + } + ] +} diff --git a/benchmark/datasets/templates/ics_202.json b/benchmark/datasets/templates/ics_202.json new file mode 100644 index 00000000..2adf6ebe --- /dev/null +++ b/benchmark/datasets/templates/ics_202.json @@ -0,0 +1,38 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_objectives": ["string"], + "4_operational_period_command_emphasis": "string", + "general_situational_awareness": "string", + "5_site_safety_plan_required": "boolean", + "approved_site_safety_plans_located_at": "string", + "6_incident_action_plan_attachments": { + "ics_203": "boolean", + "ics_204": "boolean", + "ics_205": "boolean", + "ics_205a": "boolean", + "ics_206": "boolean", + "ics_207": "boolean", + "ics_208": "boolean", + "map_chart": "boolean", + "weather_forecast_tides_currents": "boolean", + "other_attachments": ["string"] + }, + "7_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "8_approved_by_incident_commander": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_203.json b/benchmark/datasets/templates/ics_203.json new file mode 100644 index 00000000..380f9115 --- /dev/null +++ b/benchmark/datasets/templates/ics_203.json @@ -0,0 +1,88 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_incident_commander_and_command_staff": { + "ic_ucs": ["string"], + "deputy": "string", + "safety_officer": "string", + "public_info_officer": "string", + "liaison_officer": "string" + }, + "4_agency_organization_representatives": [ + { + "agency_organization": "string", + "name": "string" + } + ], + "5_planning_section": { + "chief": "string", + "deputy": "string", + "resources_unit": "string", + "situation_unit": "string", + "documentation_unit": "string", + "demobilization_unit": "string", + "technical_specialists": [ + { + "specialty": "string", + "name": "string" + } + ] + }, + "6_logistics_section": { + "chief": "string", + "deputy": "string", + "support_branch": { + "director": "string", + "supply_unit": "string", + "facilities_unit": "string", + "ground_support_unit": "string" + }, + "service_branch": { + "director": "string", + "communications_unit": "string", + "medical_unit": "string", + "food_unit": "string" + } + }, + "7_operations_section": { + "chief": "string", + "deputy": "string", + "staging_area": "string", + "branches": [ + { + "branch_name": "string", + "branch_director": "string", + "deputy": "string", + "divisions_groups": [ + { + "identifier": "string", + "supervisor": "string" + } + ] + } + ], + "air_operations_branch": { + "air_ops_branch_dir": "string" + } + }, + "8_finance_administration_section": { + "chief": "string", + "deputy": "string", + "time_unit": "string", + "procurement_unit": "string", + "comp_claims_unit": "string", + "cost_unit": "string" + }, + "9_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_204.json b/benchmark/datasets/templates/ics_204.json new file mode 100644 index 00000000..615d3ddb --- /dev/null +++ b/benchmark/datasets/templates/ics_204.json @@ -0,0 +1,53 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_branch_division_group_staging_area": { + "branch": "string", + "division": "string", + "group": "string", + "staging_area": "string" + }, + "4_operations_personnel": { + "operations_section_chief": { + "name": "string", + "contact_number": "string" + }, + "branch_director": { + "name": "string", + "contact_number": "string" + }, + "division_group_supervisor": { + "name": "string", + "contact_number": "string" + } + }, + "5_resources_assigned": [ + { + "resource_identifier": "string", + "leader": "string", + "number_of_persons": "string", + "contact": "string", + "reporting_location_special_equipment_remarks_notes_information": "string" + } + ], + "6_work_assignments": "string", + "7_special_instructions": "string", + "8_communications": [ + { + "name_function": "string", + "primary_contact": "string" + } + ], + "9_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_205.json b/benchmark/datasets/templates/ics_205.json new file mode 100644 index 00000000..0e1af7a7 --- /dev/null +++ b/benchmark/datasets/templates/ics_205.json @@ -0,0 +1,35 @@ +{ + "1_incident_name": "string", + "2_date_time_prepared": { + "date": "string", + "time": "string" + }, + "3_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "4_basic_radio_channel_use": [ + { + "zone_grp": "string", + "channel_number": "string", + "function": "string", + "channel_name_trunked_radio_system_talkgroup": "string", + "assignment": "string", + "rx_frequency_n_or_w": "string", + "rx_tone_nac": "string", + "tx_frequency_n_or_w": "string", + "tx_tone_nac": "string", + "mode_a_d_or_m": "string", + "remarks": "string" + } + ], + "5_special_instructions": "string", + "6_prepared_by": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_205a.json b/benchmark/datasets/templates/ics_205a.json new file mode 100644 index 00000000..92883977 --- /dev/null +++ b/benchmark/datasets/templates/ics_205a.json @@ -0,0 +1,23 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_basic_local_communications_information": [ + { + "incident_assigned_position": "string", + "name": "string", + "methods_of_contact": "string" + } + ], + "4_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_206.json b/benchmark/datasets/templates/ics_206.json new file mode 100644 index 00000000..140242bd --- /dev/null +++ b/benchmark/datasets/templates/ics_206.json @@ -0,0 +1,58 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_medical_aid_stations": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ], + "4_transportation_services": { + "ambulance_services": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ], + "air_ambulance_services": [ + { + "name": "string", + "location": "string", + "contact_number_frequency": "string", + "paramedic_service": "boolean" + } + ] + }, + "5_hospitals": [ + { + "hospital_name": "string", + "address_latitude_longitude": "string", + "contact_number_frequency": "string", + "air_ambulance_helipad": "boolean", + "burn_center": "boolean", + "trauma_center": "boolean" + } + ], + "6_special_medical_emergency_procedures": "string", + "7_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "8_approved_by_safety_officer": { + "name": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_207.json b/benchmark/datasets/templates/ics_207.json new file mode 100644 index 00000000..0ef33901 --- /dev/null +++ b/benchmark/datasets/templates/ics_207.json @@ -0,0 +1,63 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_organization_chart": { + "incident_commanders": ["string"], + "command_staff": { + "safety_officer": "string", + "public_information_officer": "string", + "liaison_officer": "string" + }, + "operations_section": { + "chief": "string", + "staging_area_manager": "string", + "branches_divisions_groups": [ + { + "title": "string", + "name": "string" + } + ] + }, + "planning_section": { + "chief": "string", + "resources_unit_ldr": "string", + "situation_unit_ldr": "string", + "documentation_unit_ldr": "string", + "demobilization_unit_ldr": "string" + }, + "logistics_section": { + "chief": "string", + "support_branch": { + "director": "string", + "supply_unit_ldr": "string", + "facilities_unit_ldr": "string", + "ground_spt_unit_ldr": "string" + }, + "service_branch": { + "director": "string", + "comms_unit_ldr": "string", + "medical_unit_ldr": "string", + "food_unit_ldr": "string" + } + }, + "finance_administration_section": { + "chief": "string", + "time_unit_ldr": "string", + "procurement_unit_ldr": "string", + "comp_claims_unit_ldr": "string", + "cost_unit_ldr": "string" + } + }, + "4_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_208.json b/benchmark/datasets/templates/ics_208.json new file mode 100644 index 00000000..a49113b3 --- /dev/null +++ b/benchmark/datasets/templates/ics_208.json @@ -0,0 +1,19 @@ +{ + "1_incident_name": "string", + "2_operational_period": { + "date_from": "string", + "date_to": "string", + "time_from": "string", + "time_to": "string" + }, + "3_safety_message_expanded_safety_message_safety_plan_site_safety_plan": "string", + "4_site_safety_plan_required": "boolean", + "approved_site_safety_plan_located_at": "string", + "5_prepared_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + }, + "iap_page_number": "string" +} diff --git a/benchmark/datasets/templates/ics_213.json b/benchmark/datasets/templates/ics_213.json new file mode 100644 index 00000000..749a77d1 --- /dev/null +++ b/benchmark/datasets/templates/ics_213.json @@ -0,0 +1,27 @@ +{ + "1_incident_name": "string", + "2_to": { + "name": "string", + "position": "string" + }, + "3_from": { + "name": "string", + "position": "string" + }, + "4_subject": "string", + "5_date": "string", + "6_time": "string", + "7_message": "string", + "8_approved_by": { + "name": "string", + "position_title": "string", + "signature": "string" + }, + "9_reply": "string", + "10_replied_by": { + "name": "string", + "position_title": "string", + "signature": "string", + "date_time": "string" + } +} diff --git a/benchmark/evaluators/__init__.py b/benchmark/evaluators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/evaluators/accuracy.py b/benchmark/evaluators/accuracy.py new file mode 100644 index 00000000..3024f901 --- /dev/null +++ b/benchmark/evaluators/accuracy.py @@ -0,0 +1,76 @@ +import re + + +def calculate_accuracy(extracted: any, ground_truth: any) -> float: + """ + Computes a score from 0.0 to 1.0 representing accuracy. + Handles nested dicts, lists, booleans, and string fuzzy matching. + """ + if type(extracted) != type(ground_truth): + # Allow string representations of booleans/numbers + if isinstance(ground_truth, bool) and isinstance(extracted, str): + extracted = extracted.lower() in ("true", "1", "yes") + elif isinstance(ground_truth, (int, float)) and isinstance(extracted, str): + try: + extracted = float(extracted) if isinstance(ground_truth, float) else int(extracted) + except ValueError: + return 0.0 + else: + return 0.0 + + if isinstance(ground_truth, bool): + return 1.0 if extracted == ground_truth else 0.0 + + if isinstance(ground_truth, (int, float)): + return 1.0 if extracted == ground_truth else 0.0 + + if isinstance(ground_truth, str): + return fuzzy_string_similarity(extracted, ground_truth) + + if isinstance(ground_truth, dict): + if not ground_truth: + return 1.0 + total_score = 0.0 + keys = ground_truth.keys() + for k in keys: + if k in extracted: + total_score += calculate_accuracy(extracted[k], ground_truth[k]) + return total_score / len(keys) + + if isinstance(ground_truth, list): + if not ground_truth: + return 1.0 if not extracted else 0.0 + # Compute match score for list items + matched_scores = [] + temp_extracted = list(extracted) + for gt_item in ground_truth: + best_match = 0.0 + best_idx = -1 + for idx, ext_item in enumerate(temp_extracted): + score = calculate_accuracy(ext_item, gt_item) + if score > best_match: + best_match = score + best_idx = idx + matched_scores.append(best_match) + if best_idx != -1: + temp_extracted.pop(best_idx) + return sum(matched_scores) / len(ground_truth) + + return 0.0 + + +def fuzzy_string_similarity(s1: str, s2: str) -> float: + """ + Computes a simple token-based overlap similarity score between 0.0 and 1.0. + """ + s1_clean = set(re.findall(r'\w+', s1.lower())) + s2_clean = set(re.findall(r'\w+', s2.lower())) + + if not s1_clean or not s2_clean: + return 1.0 if s1_clean == s2_clean else 0.0 + + intersection = s1_clean.intersection(s2_clean) + union = s1_clean.union(s2_clean) + + # Jaccard index + return len(intersection) / len(union) diff --git a/benchmark/evaluators/reference_accuracy.py b/benchmark/evaluators/reference_accuracy.py new file mode 100644 index 00000000..a738d99c --- /dev/null +++ b/benchmark/evaluators/reference_accuracy.py @@ -0,0 +1,77 @@ +import hashlib +import json +import os + +import requests + +from benchmark.evaluators.accuracy import calculate_accuracy + +PROMPT_VERSION = "1" +SYSTEM_PROMPT = """You create reference answers for extraction benchmarks. +Use only facts supported by the narrative, preserve source wording when practical, +and return only JSON matching the supplied template. Do not add fields.""" + + +def calculate_reference_accuracy( + extracted: dict, narrative: str, template: dict, cache_path: str +) -> tuple[float, dict]: + """Score an on-device extraction against a cached large-model reference.""" + model = os.environ["BENCHMARK_REFERENCE_MODEL"] + input_hash = hashlib.sha256( + ( + narrative + + json.dumps(template, sort_keys=True) + + model + + PROMPT_VERSION + ).encode() + ).hexdigest() + + if os.path.exists(cache_path): + with open(cache_path) as file: + cached = json.load(file) + if cached["metadata"]["input_hash"] == input_hash: + reference = cached["reference"] + return calculate_accuracy(extracted, reference), reference + + headers = {"Content-Type": "application/json"} + if api_key := os.getenv("BENCHMARK_REFERENCE_API_KEY"): + headers["Authorization"] = f"Bearer {api_key}" + + response = requests.post( + os.environ["BENCHMARK_REFERENCE_URL"], + headers=headers, + json={ + "model": model, + "temperature": 0, + "response_format": {"type": "json_object"}, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + { + "role": "user", + "content": json.dumps( + {"template": template, "narrative": narrative} + ), + }, + ], + }, + timeout=int(os.getenv("BENCHMARK_REFERENCE_TIMEOUT", "120")), + ) + response.raise_for_status() + reference = json.loads(response.json()["choices"][0]["message"]["content"]) + + os.makedirs(os.path.dirname(cache_path), exist_ok=True) + with open(cache_path, "w") as file: + json.dump( + { + "metadata": { + "model": model, + "prompt_version": PROMPT_VERSION, + "input_hash": input_hash, + }, + "reference": reference, + }, + file, + indent=2, + ) + + return calculate_accuracy(extracted, reference), reference diff --git a/benchmark/pipelines/__init__.py b/benchmark/pipelines/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/pipelines/base.py b/benchmark/pipelines/base.py new file mode 100644 index 00000000..e92f9478 --- /dev/null +++ b/benchmark/pipelines/base.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass +class PipelineExtractionOutput: + """Output structure for extraction pipelines.""" + extracted_fields: dict[str, any] + field_confidence: dict[str, float] + latency_seconds: float + + +class BasePipeline(ABC): + """Base class for all extraction pipelines.""" + + @abstractmethod + def run(self, narrative: str, template_schema: dict, pdf_path: str) -> PipelineExtractionOutput: + pass \ No newline at end of file diff --git a/benchmark/pipelines/pipeline.py b/benchmark/pipelines/pipeline.py new file mode 100644 index 00000000..becebbd4 --- /dev/null +++ b/benchmark/pipelines/pipeline.py @@ -0,0 +1,25 @@ +from benchmark.pipelines.base import BasePipeline, PipelineExtractionOutput + + +class Pipeline(BasePipeline): + def run(self, narrative: str, template_schema: dict, pdf_path: str) -> PipelineExtractionOutput: + ''' + You guys should here implement the whole pipeline implementation depending on approach B or C. + + Expected behavior: + - Create and fill templates, then wait for jobs to complete. + - Return extracted fields, confidence scores, and latency. + + Example of a single step: + >>> template_id = create_template(...) + >>> form_response = fill_form(...) + >>> result = wait_for_job_completion(form_response.job_id) + >>> extracted = result.extracted_fields + >>> confidence = result.field_confidence + >>> latency = result.latency_seconds + ''' + return PipelineExtractionOutput( + extracted_fields={}, + field_confidence={}, + latency_seconds=0.0 + ) diff --git a/benchmark/runners/__init__.py b/benchmark/runners/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/benchmark/runners/runner.py b/benchmark/runners/runner.py new file mode 100644 index 00000000..31ecaa17 --- /dev/null +++ b/benchmark/runners/runner.py @@ -0,0 +1,90 @@ +import json +import os +import time + +from benchmark.evaluators.accuracy import calculate_accuracy + + +class Runner: + def __init__(self, pipeline_class, pipeline_name: str): + self.pipeline = pipeline_class() + self.pipeline_name = pipeline_name + + def run_benchmark(self) -> dict[str, any]: + datasets_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "datasets") + narratives_dir = os.path.join(datasets_dir, "narratives") + ground_truth_dir = os.path.join(datasets_dir, "ground_truth") + templates_dir = os.path.join(datasets_dir, "templates") + pdfs_dir = os.path.join(templates_dir, "pdfs") + + results = [] + total_latency = 0.0 + + # Scan narratives directory to find matching ground_truth and template files + narrative_files = [f for f in os.listdir(narratives_dir) if f.endswith(".txt")] + + for narrative_file in sorted(narrative_files): + case_name = os.path.splitext(narrative_file)[0] + # Match ics201_1.txt -> ics201_1.json or ics201.json + # Find matching ground truth file + gt_name = case_name.split("_")[0] + "_1.json" # default fallback + possible_gts = [case_name + ".json", case_name.split("_")[0] + "_1.json"] + gt_file = None + for p in possible_gts: + if os.path.exists(os.path.join(ground_truth_dir, p)): + gt_file = p + break + + # Find matching template file + # e.g., ics201_1 -> ics_201.json + base_form_name = case_name.split("_")[0] + # convert ics201 to ics_201 if needed + if "ics" in base_form_name and "_" not in base_form_name: + base_form_name = "ics_" + base_form_name[3:] + template_file = f"{base_form_name}.json" + + narrative_path = os.path.join(narratives_dir, narrative_file) + gt_path = os.path.join(ground_truth_dir, gt_file) if gt_file else "" + template_path = os.path.join(templates_dir, template_file) + pdf_path = os.path.join(pdfs_dir, template_file.replace(".json", ".pdf")) + + if not os.path.exists(gt_path) or not os.path.exists(template_path) or not os.path.exists(pdf_path): + continue + + with open(narrative_path, "r") as f: + narrative_text = f.read() + + with open(gt_path, "r") as f: + gt_data = json.load(f) + # Unwrap the outer wrapper key if present + gt_key = list(gt_data.keys())[0] + gt_content = gt_data[gt_key] + + with open(template_path, "r") as f: + template_schema = json.load(f) + + start_time = time.time() + output = self.pipeline.run(narrative_text, template_schema, pdf_path) + latency = time.time() - start_time + total_latency += latency + + accuracy = calculate_accuracy(output.extracted_fields, gt_content) + + results.append({ + "case_id": case_name, + "latency_seconds": latency, + "accuracy_score": accuracy, + "extracted_fields": output.extracted_fields, + "ground_truth": gt_content + }) + + avg_accuracy = sum(r["accuracy_score"] for r in results) / len(results) if results else 0.0 + + return { + "pipeline_name": self.pipeline_name, + "metrics": { + "total_latency_seconds": total_latency, + "average_accuracy": avg_accuracy + }, + "results": results + } diff --git a/benchmark/test_benchmark.py b/benchmark/test_benchmark.py new file mode 100644 index 00000000..38fe9404 --- /dev/null +++ b/benchmark/test_benchmark.py @@ -0,0 +1,28 @@ +import json +import os +from datetime import datetime + +from benchmark.pipelines.pipeline import Pipeline +from benchmark.runners.runner import Runner + + +def test_pipeline_execution(): + """ + Standard test executor that finds the available Pipeline class, + runs the benchmark dataset, writes execution results to a file, and asserts accuracy. + """ + # Pipeline name contains current date and hour, minute and second (e.g. Pipeline_2026-07-08_12h_12m_12s) + timestamp = datetime.now().strftime("%Y-%m-%d_%Hh_%Mm_%Ss") + pipeline_name = f"Pipeline_{timestamp}" + + runner = Runner(Pipeline, pipeline_name) + report = runner.run_benchmark() + + # Save results to a report file to be compared in CI/CD pipeline + report_path = os.path.join(os.path.dirname(__file__), "benchmark_report.json") + with open(report_path, "w") as f: + json.dump(report, f, indent=2) + + # Assert basic quality sanity check + assert report["metrics"]["average_accuracy"] >= 0.0 + print(f"\n{pipeline_name} evaluation complete. Average Accuracy: {report['metrics']['average_accuracy']:.2f}") diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml new file mode 100644 index 00000000..16931f98 --- /dev/null +++ b/contracts/openapi.yaml @@ -0,0 +1,110 @@ +openapi: 3.0.0 + +info: + title: FireForm API + version: 1.0.0 + description: | + Proposed API contract for FireForm as GSoC project 2026 by [chetanr25.in](https://chetanr25.in) + contact: + name: Chetan R + url: https://github.com/chetanr25 + email: chetan250204@gmail.com + +servers: + - url: http://localhost:8080 + description: Local FireForm instance (default) + +tags: + - name: input + description: Submit voice or text incident narratives + - name: extraction + description: AI-powered data extraction from narratives into canonical JSON + - name: forms + description: Generate agency-specific PDF forms from extracted data + - name: incidents + description: Manage incident records linking inputs, extractions, and forms + - name: reporting + description: Aggregate statistics and periodic report generation + - name: templates + description: Form template configuration and management + - name: system + description: Health checks, schema introspection, and system status + - name: jobs + description: Cross-cutting async job status polling + - name: weather + description: Weather forecast lookup via external API + - name: zipcode + description: Address and postal code geocoding via external API + +paths: + # ── Layer 1: Input ────────────────────────────────────────────── + /api/v1/input/voice: + $ref: "path/input.yaml#/voice" + /api/v1/input/text: + $ref: "path/input.yaml#/text" + /api/v1/input/{input_id}: + $ref: "path/input.yaml#/input_by_id" + + # ── Layer 2: AI Extraction ───────────────────────────────────── + /api/v1/extract/{input_id}: + $ref: "path/extraction.yaml#/extract_by_input" + /api/v1/extract/{extract_id}: + $ref: "path/extraction.yaml#/extract_by_id" + /api/v1/extract/{extract_id}/validate: + $ref: "path/extraction.yaml#/validate" + + # ── Layer 3: Form Generation ─────────────────────────────────── + /api/v1/forms/generate/all: + $ref: "path/forms.yaml#/generate_all" + /api/v1/forms/generate/{form_type}: + $ref: "path/forms.yaml#/generate_single" + /api/v1/forms/{form_id}: + $ref: "path/forms.yaml#/form_by_id" + /api/v1/forms/{form_id}/pdf: + $ref: "path/forms.yaml#/form_pdf" + /api/v1/forms/{form_id}/json: + $ref: "path/forms.yaml#/form_json" + /api/v1/forms/batch/{batch_id}: + $ref: "path/forms.yaml#/batch_by_id" + + # ── Layer 4: Incident Management ─────────────────────────────── + /api/v1/incidents: + $ref: "path/incidents.yaml#/incidents" + /api/v1/incidents/{incident_id}: + $ref: "path/incidents.yaml#/incident_by_id" + + # ── Layer 5: Reporting & Analytics ───────────────────────────── + /api/v1/reports/summary: + $ref: "path/reporting.yaml#/summary" + /api/v1/reports/generate: + $ref: "path/reporting.yaml#/generate" + /api/v1/reports/{report_id}: + $ref: "path/reporting.yaml#/report_by_id" + + # ── Layer 6: Templates & Configuration ───────────────────────── + /api/v1/templates: + $ref: "path/templates.yaml#/templates" + /api/v1/templates/{template_id}: + $ref: "path/templates.yaml#/template_by_id" + /api/v1/templates/{template_id}/fields: + $ref: "path/templates.yaml#/template_fields" + /api/v1/templates/pdf: + $ref: "path/templates.yaml#/templates_pdf" + + # ── Layer 7: System ──────────────────────────────────────────── + /api/v1/health: + $ref: "path/system.yaml#/health" + /api/v1/schema/incident: + $ref: "path/system.yaml#/schema_incident" + /api/v1/schema/incident/versions: + $ref: "path/system.yaml#/schema_versions" + + # ── Layer 8: Async Jobs ──────────────────────────────────────── + /api/v1/jobs/{job_id}: + $ref: "path/jobs.yaml#/job_by_id" + + # ── Layer 9: External APIs ───────────────────────────────────── + /api/v1/weather/forecast: + $ref: "path/weather.yaml#/forecast" + /api/v1/zipcode/lookup-address: + $ref: "path/zipcode.yaml#/lookup_address" diff --git a/contracts/path/extraction.yaml b/contracts/path/extraction.yaml new file mode 100644 index 00000000..865e20c4 --- /dev/null +++ b/contracts/path/extraction.yaml @@ -0,0 +1,313 @@ +# Layer 2 AI Extraction Endpoints +# POST /api/v1/extract/{input_id} +# GET /api/v1/extract/{extract_id} +# PATCH /api/v1/extract/{extract_id} +# POST /api/v1/extract/{extract_id}/validate + +extract_by_input: + post: + operationId: createExtraction + summary: Start AI extraction from input narrative + description: | + Sends the narrative (from a previously submitted input) to the local Ollama + LLM with a structured prompt to extract all incident fields into the canonical + FireForm JSON schema. This is an asynchronous operation the LLM may take + 30–120 seconds. Returns an extract_id and job_id for polling. + tags: + - extraction + parameters: + - name: input_id + in: path + required: true + description: ID of the input record to extract from + schema: + type: string + format: uuid + requestBody: + required: false + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionRequest" + example: + model_override: "llama3:8b" + extraction_hints: + incident_type: "wildland_fire" + state: "CA" + responses: + "202": + description: Extraction job queued successfully + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + input_id: "550e8400-e29b-41d4-a716-446655440001" + queued_at: "2024-07-15T14:30:00Z" + estimated_seconds: 60 + poll_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + "404": + description: Input ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" + "409": + description: Conflict input not ready or extraction already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + not_ready: + summary: Input still transcribing + value: + error_code: "INPUT_NOT_READY" + message: "Input is in 'transcribing' state. Wait until status is 'ready'." + detail: + current_status: "transcribing" + already_exists: + summary: Extraction already initiated for this input + value: + error_code: "EXTRACTION_EXISTS" + message: "An extraction already exists for this input" + detail: + existing_extract_id: "550e8400-e29b-41d4-a716-446655440020" + "503": + description: Ollama LLM service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "LLM_UNAVAILABLE" + message: "Ollama LLM service is not available" + retry_after_seconds: 30 + detail: + ollama_status: "connection_refused" + +extract_by_id: + get: + operationId: getExtraction + summary: Get extraction result by ID + description: | + Returns the full canonical FireForm JSON when extraction is complete, or + the current job status while still processing. When status is "completed", + the response body contains the entire canonical incident schema. When still + processing, includes a retry_after_seconds hint for polling. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction + schema: + type: string + format: uuid + responses: + "200": + description: Extraction record (completed or in-progress) + content: + application/json: + schema: + oneOf: + - $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + - $ref: "../schemas/extraction-record.yaml#/ExtractionProcessing" + examples: + completed: + summary: Extraction completed with canonical JSON + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "completed" + completed_at: "2024-07-15T14:31:05Z" + incident_contract: + schema_version: "1.1.0" + schema_name: "fireform_incident_contract" + extraction_metadata: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + confidence_score: 0.91 + incident: + name: "Bear Creek Wildfire" + processing: + summary: Extraction still in progress + value: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "processing" + started_at: "2024-07-15T14:30:00Z" + retry_after_seconds: 5 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_FOUND" + message: "Extraction with ID 550e8400-e29b-41d4-a716-446655440099 not found" + + patch: + operationId: updateExtraction + summary: Manually correct extracted fields + description: | + Allows a responder to correct any field in the canonical JSON after LLM + extraction. Uses JSON Merge Patch (RFC 7396) only send the fields that + changed. The server records an audit trail of all changes vs the original + LLM output and recalculates completeness scores and applicable_forms. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to update + schema: + type: string + format: uuid + requestBody: + required: true + description: JSON Merge Patch (RFC 7396) only include changed fields + content: + application/merge-patch+json: + schema: + $ref: "../schemas/incident-contract.yaml#/IncidentContract" + example: + fire: + estimated_damage_usd: 250000 + casualties: + total_responder_injuries: 2 + responses: + "200": + description: Extraction updated returns full updated canonical JSON + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ExtractionCompleted" + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction is locked (already submitted) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_LOCKED" + message: "Cannot modify extraction incident report has been submitted" + detail: + report_status: "submitted" + submitted_at: "2024-07-15T18:00:00Z" + "422": + description: Invalid field path or value + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid field path or value in patch" + validation_errors: + - field: "fire.cause_certainty" + issue: "Must be one of: confirmed, probable, suspected, undetermined" + value: "maybe" + +validate: + post: + operationId: validateExtraction + summary: Validate extraction against a form's requirements + description: | + Validates the canonical JSON against a specific form type's field requirements. + Returns whether the extraction has all required fields, which recommended + fields are missing, and any warnings. Useful for checking "can I generate + a NERIS report with what I have?" before triggering form generation. + tags: + - extraction + parameters: + - name: extract_id + in: path + required: true + description: Unique identifier of the extraction to validate + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - form_type + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + example: + form_type: "neris" + responses: + "200": + description: Validation result + content: + application/json: + schema: + $ref: "../schemas/extraction-record.yaml#/ValidationResult" + example: + valid: true + form_type: "neris" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + missing_required: [] + missing_recommended: + - "fire.detector_present" + - "fire.detector_operated" + warnings: + - "fire.estimated_damage_usd is null NERIS recommends providing damage estimates" + field_coverage_percent: 94 + "404": + description: Extraction not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Unknown form type + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNKNOWN_FORM_TYPE" + message: "Form type 'xyz' is not recognized" + detail: + valid_form_types: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york diff --git a/contracts/path/forms.yaml b/contracts/path/forms.yaml new file mode 100644 index 00000000..b920b41a --- /dev/null +++ b/contracts/path/forms.yaml @@ -0,0 +1,348 @@ +# Layer 3 Form Generation Endpoints +# POST /api/v1/forms/generate/all +# POST /api/v1/forms/generate/{form_type} +# GET /api/v1/forms/{form_id} +# GET /api/v1/forms/{form_id}/pdf +# GET /api/v1/forms/{form_id}/json +# GET /api/v1/forms/batch/{batch_id} + +generate_all: + post: + operationId: generateAllForms + summary: Generate all applicable forms from an extraction + description: | + Triggers batch generation of ALL forms listed in the extraction's + extraction_metadata.applicable_forms. This is an async batch job. + Use skip_incomplete to skip forms that fail validation, or force_partial + to generate forms with blank fields where data is missing. + tags: + - forms + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateAllRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + skip_incomplete: true + force_partial: false + responses: + "202": + description: Batch form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchGenerateResponse" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + forms_queued: + - "neris" + - "nfirs_basic" + - "nfirs_wildland" + forms_skipped: + - form_type: "nemsis_epcr" + reason: "Missing required fields: ems.patients[0].date_of_birth" + estimated_seconds: 45 + poll_url: "/api/v1/forms/batch/550e8400-e29b-41d4-a716-446655440030" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Extraction not yet completed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "EXTRACT_NOT_COMPLETED" + message: "Extraction is still processing. Wait until status is 'completed'." + detail: + current_status: "processing" + "422": + description: No applicable forms found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NO_APPLICABLE_FORMS" + message: "No forms are applicable for this extraction's incident type" + +generate_single: + post: + operationId: generateSingleForm + summary: Generate one specific form type + description: | + Generates a single agency-specific form from the canonical extraction data. + The form_type path parameter specifies which form to generate. If the extraction + is missing required fields for this form, returns 422 unless force_partial is true. + tags: + - forms + parameters: + - name: form_type + in: path + required: true + description: Type of form to generate + schema: + $ref: "../schemas/enums.yaml#/FormType" + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/GenerateSingleRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + options: + output_format: "pdf" + force_partial: false + responses: + "202": + description: Form generation job accepted + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormGenerateResponse" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "processing" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + job_id: "550e8400-e29b-41d4-a716-446655440097" + estimated_seconds: 15 + poll_url: "/api/v1/forms/550e8400-e29b-41d4-a716-446655440040" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Validation failure or form not in applicable list + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FORM_VALIDATION_FAILED" + message: "Extraction is missing required fields for NERIS form" + detail: + form_type: "neris" + missing_fields: + - "incident.types[0].neris_code" + - "location.coordinates" + validation_errors: + - field: "incident.types[0].neris_code" + issue: "Required field is null" + +form_by_id: + get: + operationId: getForm + summary: Get form metadata and status + description: | + Returns the form record including generation status, associated extract and + incident IDs, and a field_mapping_summary showing how canonical fields were + mapped to the form's agency-specific fields (for audit/transparency). + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the generated form + schema: + type: string + format: uuid + responses: + "200": + description: Form record found + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormRecord" + example: + form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_id: "550e8400-e29b-41d4-a716-446655440050" + created_at: "2024-07-15T14:32:00Z" + completed_at: "2024-07-15T14:32:12Z" + pdf_ready: true + json_ready: true + field_mapping_summary: + total_form_fields: 85 + fields_filled: 72 + fields_blank: 13 + coverage_percent: 84.7 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +form_pdf: + get: + operationId: downloadFormPdf + summary: Download filled PDF form + description: | + Returns the filled PDF binary file for the specified form. If the form + generation is still in progress, returns 202 Accepted with a retry_after + hint. The Content-Disposition header is set for browser download. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form to download + schema: + type: string + format: uuid + responses: + "200": + description: Filled PDF file + content: + application/pdf: + schema: + type: string + format: binary + headers: + Content-Disposition: + description: Attachment filename for download + schema: + type: string + example: 'attachment; filename="NERIS_FF-2024-CA-0157.pdf"' + "202": + description: Form generation still in progress + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Form generation is still in progress" + status: "processing" + retry_after_seconds: 5 + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "500": + description: PDF generation failed + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "PDF_GENERATION_FAILED" + message: "Failed to generate PDF for form" + detail: + reason: "Template file corrupted or missing" + +form_json: + get: + operationId: getFormJson + summary: Get form-specific field-mapped JSON + description: | + Returns the form-specific JSON with fields mapped to the agency's expected + format not the canonical FireForm schema, but the actual field names and + structure that the target agency system expects. Useful for future direct + API submission to agency systems. + tags: + - forms + parameters: + - name: form_id + in: path + required: true + description: Unique identifier of the form + schema: + type: string + format: uuid + responses: + "200": + description: Form-specific mapped JSON + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/FormMappedJson" + example: + form_type: "neris" + form_version: "2.0" + form_id: "550e8400-e29b-41d4-a716-446655440040" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + agency_fields: + incident_type: "wildland-fire" + incident_date: "2024-07-10" + alarm_time: "13:52" + acres_burned: 1247 + cause: "natural" + "404": + description: Form not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +batch_by_id: + get: + operationId: getBatchStatus + summary: Get batch form generation status + description: | + Returns the status of a batch form generation job including progress + for each individual form. Poll this endpoint after POST /forms/generate/all. + tags: + - forms + parameters: + - name: batch_id + in: path + required: true + description: Unique identifier of the batch job + schema: + type: string + format: uuid + responses: + "200": + description: Batch job status + content: + application/json: + schema: + $ref: "../schemas/form-record.yaml#/BatchStatus" + example: + batch_id: "550e8400-e29b-41d4-a716-446655440030" + status: "completed" + total: 3 + completed: 3 + failed: 0 + forms: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440041" + form_type: "nfirs_basic" + status: "completed" + - form_id: "550e8400-e29b-41d4-a716-446655440042" + form_type: "nfirs_wildland" + status: "completed" + "404": + description: Batch job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/incidents.yaml b/contracts/path/incidents.yaml new file mode 100644 index 00000000..810a311a --- /dev/null +++ b/contracts/path/incidents.yaml @@ -0,0 +1,313 @@ +# Layer 4 Incident Management Endpoints +# POST /api/v1/incidents +# GET /api/v1/incidents +# GET /api/v1/incidents/{incident_id} +# PATCH /api/v1/incidents/{incident_id} +# DELETE /api/v1/incidents/{incident_id} + +incidents: + post: + operationId: createIncident + summary: Create a full incident record + description: | + Creates a permanent incident record that links input, extraction, and + generated forms into a single coherent unit. Assigns a permanent incident_id + and stores the complete incident for future retrieval and reporting. + tags: + - incidents + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/CreateIncidentRequest" + example: + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + responses: + "201": + description: Incident record created + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + extract_id: "550e8400-e29b-41d4-a716-446655440020" + incident_number: "CA-SQF-2024-0421" + status: "draft" + forms_generated: + - form_id: "550e8400-e29b-41d4-a716-446655440040" + form_type: "neris" + status: "completed" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + created_at: "2024-07-15T14:35:00Z" + "404": + description: Extract ID not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Duplicate incident number + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "DUPLICATE_INCIDENT_NUMBER" + message: "Incident number CA-SQF-2024-0421 already exists" + detail: + existing_incident_id: "550e8400-e29b-41d4-a716-446655440049" + + get: + operationId: listIncidents + summary: List incidents with filtering + description: | + Returns a paginated list of incident records with optional filtering by + date range, incident type, and status. Supports sorting by date. + Maximum 100 results per page. + tags: + - incidents + parameters: + - name: date_from + in: query + description: Start date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date filter (inclusive, ISO 8601) + schema: + type: string + format: date + - name: incident_type + in: query + description: Filter by incident category + schema: + $ref: "../schemas/enums.yaml#/IncidentCategory" + - name: status + in: query + description: Filter by report status + schema: + $ref: "../schemas/enums.yaml#/ReportStatus" + - name: page + in: query + description: Page number (1-based) + schema: + type: integer + minimum: 1 + default: 1 + - name: per_page + in: query + description: Items per page (max 100) + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + description: Sort order + schema: + type: string + enum: + - date_asc + - date_desc + default: date_desc + responses: + "200": + description: Paginated list of incidents + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentListResponse" + example: + data: + - incident_id: "550e8400-e29b-41d4-a716-446655440050" + incident_number: "CA-SQF-2024-0421" + status: "draft" + incident_name: "Bear Creek Wildfire" + incident_type: "fire" + incident_date: "2024-07-10" + forms_count: 3 + created_at: "2024-07-15T14:35:00Z" + pagination: + total: 42 + page: 1 + per_page: 20 + total_pages: 3 + has_next: true + has_prev: false + "422": + description: Invalid query parameter format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Invalid date format for date_from" + validation_errors: + - field: "date_from" + issue: "Must be ISO 8601 date format (YYYY-MM-DD)" + value: "15/07/2024" + +incident_by_id: + get: + operationId: getIncident + summary: Get full incident record + description: | + Returns the complete incident record including the linked canonical + extraction, all generated forms and their statuses, submission log, + and audit trail. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident + schema: + type: string + format: uuid + responses: + "200": + description: Full incident record + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecordFull" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + patch: + operationId: updateIncident + summary: Update incident metadata + description: | + Updates incident metadata such as status, tags, incident number, and notes. + Does NOT allow changing the underlying extraction data use PATCH /extract + for that. Submitted incidents cannot be modified. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/UpdateIncidentRequest" + example: + status: "approved" + tags: + - "wildland" + - "mutual_aid" + - "lightning" + - "reviewed" + notes: "Reviewed by BC Wilson. Ready for submission." + responses: + "200": + description: Incident updated + content: + application/json: + schema: + $ref: "../schemas/incident-record.yaml#/IncidentRecord" + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot modify a submitted incident + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INCIDENT_SUBMITTED" + message: "Cannot modify an incident that has been submitted" + detail: + submitted_at: "2024-07-15T18:00:00Z" + + delete: + operationId: deleteIncident + summary: Soft-delete an incident + description: | + Performs a soft delete sets a deleted_at timestamp but never removes data. + Submitted incidents cannot be deleted. Soft-deleted incidents are excluded + from list queries by default but can be recovered. + tags: + - incidents + parameters: + - name: incident_id + in: path + required: true + description: Unique identifier of the incident to delete + schema: + type: string + format: uuid + responses: + "200": + description: Incident soft-deleted + content: + application/json: + schema: + type: object + properties: + incident_id: + type: string + format: uuid + deleted_at: + type: string + format: date-time + recoverable: + type: boolean + example: + incident_id: "550e8400-e29b-41d4-a716-446655440050" + deleted_at: "2024-07-16T10:00:00Z" + recoverable: true + "404": + description: Incident not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Cannot delete already deleted or submitted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + already_deleted: + summary: Incident already soft-deleted + value: + error_code: "ALREADY_DELETED" + message: "Incident has already been deleted" + detail: + deleted_at: "2024-07-16T09:00:00Z" + submitted: + summary: Submitted incidents cannot be deleted + value: + error_code: "INCIDENT_SUBMITTED" + message: "Submitted incidents cannot be deleted" diff --git a/contracts/path/input.yaml b/contracts/path/input.yaml new file mode 100644 index 00000000..def2f2ce --- /dev/null +++ b/contracts/path/input.yaml @@ -0,0 +1,247 @@ +# Layer 1 Input Endpoints +# POST /api/v1/input/voice, POST /api/v1/input/text, GET /api/v1/input/{input_id} + +voice: + post: + operationId: submitVoiceInput + summary: Submit a voice recording for transcription + description: | + Accepts an audio file (voice memo) from a first responder along with optional + incident metadata. The audio is saved and queued for transcription via Whisper + running on the local Ollama instance. Returns an input_id immediately the + transcription runs asynchronously. Poll GET /api/v1/input/{input_id} for status. + tags: + - input + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - audio_file + properties: + audio_file: + type: string + format: binary + description: Audio file in wav, mp3, m4a, ogg, or webm format. Max 500MB. + station_id: + type: string + description: Station identifier (e.g. "STA-045") + responder_badge: + type: string + description: Badge number of the responder submitting the report + incident_date_hint: + type: string + format: date + description: Approximate date of the incident to aid extraction + example: + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-15" + responses: + "201": + description: Voice input accepted and queued for transcription + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/VoiceInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "queued" + input_type: "voice" + estimated_processing_seconds: 30 + created_at: "2024-07-15T14:25:00Z" + job_id: "550e8400-e29b-41d4-a716-446655440099" + poll_url: "/api/v1/input/550e8400-e29b-41d4-a716-446655440001" + "400": + description: Malformed request (missing audio file or invalid form data) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Audio file exceeds 500MB limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "FILE_TOO_LARGE" + message: "Audio file exceeds maximum size of 500MB" + detail: + max_size_bytes: 524288000 + received_size_bytes: 600000000 + "415": + description: Unsupported audio format + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "UNSUPPORTED_FORMAT" + message: "Audio format not supported. Accepted formats: wav, mp3, m4a, ogg, webm" + detail: + accepted_formats: + - wav + - mp3 + - m4a + - ogg + - webm + "503": + description: Whisper transcription service unavailable + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "STT_UNAVAILABLE" + message: "Whisper transcription service is not available" + retry_after_seconds: 30 + +text: + post: + operationId: submitTextInput + summary: Submit a text narrative for processing + description: | + Accepts a free-text incident narrative from a first responder. This is + synchronous the text is stored immediately and the input is marked as + "ready" for extraction. No transcription step needed. + tags: + - input + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputRequest" + example: + narrative: > + Responded to a wildfire at Bear Creek Trailhead around 1:45 PM on July 10th. + Lightning strike ignited timber litter in mixed conifer and chaparral. Fire + spread rapidly northeast driven by 15-25 mph SW winds. We deployed 18 engines, + 6 water tenders, and 3 helicopters. 247 personnel total. One firefighter + treated for heat exhaustion, returned to duty July 12. 1247 acres burned across + federal, state, and private land. 47 structures threatened, 3 damaged, none + destroyed. Fire contained July 14, controlled July 15. + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + responses: + "201": + description: Text input accepted and ready for extraction + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/TextInputResponse" + example: + input_id: "550e8400-e29b-41d4-a716-446655440001" + status: "ready" + input_type: "text" + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + "400": + description: Malformed JSON request body + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: Narrative exceeds 50,000 character limit + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "NARRATIVE_TOO_LONG" + message: "Narrative exceeds maximum length of 50,000 characters" + detail: + max_characters: 50000 + received_characters: 52000 + "422": + description: Validation error (empty or too-short narrative) + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "VALIDATION_ERROR" + message: "Narrative is too short to extract meaningful incident data" + validation_errors: + - field: "narrative" + issue: "Must contain at least 10 words" + value: "Fire happened" + +input_by_id: + get: + operationId: getInput + summary: Get input record by ID + description: | + Returns the full input record including the original text or transcript, + processing status for voice inputs, and metadata. For voice inputs, poll + this endpoint until status transitions from "queued"/"transcribing" to "ready". + tags: + - input + parameters: + - name: input_id + in: path + required: true + description: Unique identifier of the input record + schema: + type: string + format: uuid + responses: + "200": + description: Input record found + content: + application/json: + schema: + $ref: "../schemas/input-record.yaml#/InputRecord" + examples: + voice_ready: + summary: Voice input with completed transcription + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + original_filename: "incident_memo.m4a" + audio_duration_seconds: 180 + character_count: 612 + word_count: 98 + station_id: "STA-045" + responder_badge: "FD-7842" + incident_date_hint: "2024-07-10" + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:30Z" + voice_processing: + summary: Voice input still transcribing + value: + input_id: "550e8400-e29b-41d4-a716-446655440001" + input_type: "voice" + status: "transcribing" + transcript: null + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:10Z" + retry_after_seconds: 5 + text_ready: + summary: Text input ready for extraction + value: + input_id: "550e8400-e29b-41d4-a716-446655440002" + input_type: "text" + status: "ready" + transcript: "Responded to a wildfire at Bear Creek..." + character_count: 612 + word_count: 98 + created_at: "2024-07-15T14:25:00Z" + updated_at: "2024-07-15T14:25:00Z" + "404": + description: Input record not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "INPUT_NOT_FOUND" + message: "Input with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/jobs.yaml b/contracts/path/jobs.yaml new file mode 100644 index 00000000..2ac042f6 --- /dev/null +++ b/contracts/path/jobs.yaml @@ -0,0 +1,79 @@ +# Layer 8 Async Job Status (Cross-cutting) +# GET /api/v1/jobs/{job_id} + +job_by_id: + get: + operationId: getJobStatus + summary: Get async job status + description: | + Universal job status endpoint for any asynchronous operation in FireForm — + transcription, LLM extraction, form generation, and report generation. + Returns the current status, progress percentage, and a result URL when + the job completes. Poll this endpoint for long-running operations. + tags: + - jobs + parameters: + - name: job_id + in: path + required: true + description: Unique identifier of the async job + schema: + type: string + format: uuid + responses: + "200": + description: Job status + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/Job" + examples: + queued: + summary: Job waiting in queue + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "queued" + progress_percent: 0 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:00Z" + processing: + summary: Job currently processing + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "processing" + progress_percent: 45 + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:30:30Z" + completed: + summary: Job completed successfully + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "completed" + progress_percent: 100 + result_url: "/api/v1/extract/550e8400-e29b-41d4-a716-446655440020" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:31:05Z" + failed: + summary: Job failed with error + value: + job_id: "550e8400-e29b-41d4-a716-446655440098" + job_type: "extraction" + status: "failed" + progress_percent: 23 + error: + error_code: "LLM_TIMEOUT" + message: "Ollama did not respond within 120 seconds" + created_at: "2024-07-15T14:30:00Z" + updated_at: "2024-07-15T14:32:00Z" + "404": + description: Job not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "JOB_NOT_FOUND" + message: "Job with ID 550e8400-e29b-41d4-a716-446655440099 not found" diff --git a/contracts/path/reporting.yaml b/contracts/path/reporting.yaml new file mode 100644 index 00000000..ad101723 --- /dev/null +++ b/contracts/path/reporting.yaml @@ -0,0 +1,191 @@ +# Layer 5 Reporting & Analytics Endpoints +# GET /api/v1/reports/summary +# POST /api/v1/reports/generate +# GET /api/v1/reports/{report_id} + +summary: + get: + operationId: getReportSummary + summary: Get aggregate incident statistics + description: | + Returns aggregate statistics for a dashboard view total incidents, + breakdown by type and status, forms generated, average completeness + scores, and average processing times. Supports filtering by date range + and grouping by time period or incident type. + tags: + - reporting + parameters: + - name: date_from + in: query + description: Start date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: date_to + in: query + description: End date (inclusive, ISO 8601) + schema: + type: string + format: date + - name: group_by + in: query + description: Group results by time period or incident type + schema: + type: string + enum: + - day + - week + - month + - incident_type + default: month + responses: + "200": + description: Aggregate statistics + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/ReportSummary" + example: + date_from: "2024-01-01" + date_to: "2024-07-15" + total_incidents: 157 + by_type: + fire: 42 + ems: 68 + rescue: 12 + hazardous_conditions: 8 + service_call: 15 + good_intent: 7 + false_alarm: 5 + by_status: + draft: 3 + under_review: 2 + approved: 12 + submitted: 140 + forms_generated: 489 + avg_completeness_score: 88.3 + avg_processing_time_seconds: 47.2 + groups: + - period: "2024-07" + incident_count: 23 + forms_generated: 71 + +generate: + post: + operationId: generateReport + summary: Generate a periodic report + description: | + Generates a periodic (monthly, quarterly, or annual) aggregate report + as PDF or JSON. This includes statistics, incident summaries, and + compliance metrics required by agencies like USFA/NERIS. This is an + async operation returns a report_id for polling. + + Note: USFA encourages monthly submission but requires quarterly at minimum. + This endpoint supports generating reports aligned with those cadences. + tags: + - reporting + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/reporting.yaml#/GenerateReportRequest" + example: + period_type: "monthly" + year: 2024 + month: 7 + format: "pdf" + responses: + "202": + description: Report generation job accepted + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/AsyncJobResponse" + example: + job_id: "550e8400-e29b-41d4-a716-446655440096" + job_type: "report_generation" + status: "processing" + estimated_seconds: 30 + poll_url: "/api/v1/reports/550e8400-e29b-41d4-a716-446655440060" + report_id: "550e8400-e29b-41d4-a716-446655440060" + "422": + description: Invalid period or future date + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + examples: + future_period: + summary: Future period requested + value: + error_code: "FUTURE_PERIOD" + message: "Cannot generate a report for a future period" + invalid_quarter: + summary: Invalid quarter value + value: + error_code: "VALIDATION_ERROR" + message: "Invalid quarter value" + validation_errors: + - field: "quarter" + issue: "Must be 1, 2, 3, or 4" + value: 5 + +report_by_id: + get: + operationId: getReport + summary: Retrieve a generated report + description: | + Returns a previously generated periodic report. If the report is still + being generated, returns 202 Accepted with a retry hint. When complete, + returns the report in the originally requested format (PDF binary or JSON). + tags: + - reporting + parameters: + - name: report_id + in: path + required: true + description: Unique identifier of the generated report + schema: + type: string + format: uuid + responses: + "200": + description: Generated report (PDF or JSON depending on original request) + content: + application/pdf: + schema: + type: string + format: binary + application/json: + schema: + $ref: "../schemas/reporting.yaml#/PeriodicReport" + headers: + Content-Disposition: + description: Attachment filename for PDF downloads + schema: + type: string + example: 'attachment; filename="FireForm_Monthly_2024-07.pdf"' + "202": + description: Report still being generated + content: + application/json: + schema: + type: object + properties: + message: + type: string + status: + type: string + retry_after_seconds: + type: integer + example: + message: "Report generation is still in progress" + status: "processing" + retry_after_seconds: 10 + "404": + description: Report not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/system.yaml b/contracts/path/system.yaml new file mode 100644 index 00000000..cd00901c --- /dev/null +++ b/contracts/path/system.yaml @@ -0,0 +1,152 @@ +# Layer 7 System Endpoints +# GET /api/v1/health +# GET /api/v1/schema/incident +# GET /api/v1/schema/incident/versions + +health: + get: + operationId: getHealth + summary: System health check + description: | + Returns the health status of all FireForm components — database, Ollama LLM + (including loaded models, GPU status, and current load), Whisper transcription, + and file storage. Returns 200 even when degraded (so load balancers don't kill + it), 503 only if the system is truly unhealthy and cannot serve any requests. + tags: + - system + responses: + "200": + description: System is healthy or degraded + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + examples: + healthy: + summary: All systems operational + value: + status: "healthy" + version: "1.0.0" + uptime_seconds: 86400 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "healthy" + response_time_ms: 15 + model_loaded: "llama3:8b" + ollama_version: "0.3.0" + models_available: + - name: "llama3:8b" + size_gb: 4.7 + quantization: "Q4_K_M" + loaded: true + - name: "mistral:7b" + size_gb: 4.1 + quantization: "Q4_0" + loaded: false + current_load: + active_requests: 1 + queued_requests: 0 + whisper: + status: "healthy" + response_time_ms: 10 + storage: + status: "healthy" + disk_free_gb: 120.5 + degraded: + summary: Some components have issues + value: + status: "degraded" + version: "1.0.0" + uptime_seconds: 3600 + components: + database: + status: "healthy" + response_time_ms: 2 + ollama: + status: "degraded" + response_time_ms: 5000 + detail: "High latency model may be loading" + current_load: + active_requests: 3 + queued_requests: 2 + whisper: + status: "unhealthy" + detail: "Whisper model not loaded" + storage: + status: "healthy" + disk_free_gb: 120.5 + "503": + description: System is unhealthy cannot serve requests + content: + application/json: + schema: + $ref: "../schemas/system.yaml#/HealthStatus" + example: + status: "unhealthy" + version: "1.0.0" + uptime_seconds: 10 + components: + database: + status: "unhealthy" + detail: "Connection refused" + ollama: + status: "unhealthy" + detail: "Connection refused" + +schema_incident: + get: + operationId: getIncidentSchema + summary: Get the canonical FireForm JSON Schema + description: | + Returns the full canonical FireForm incident JSON Schema (JSON Schema + draft-07). Clients can use this to validate incident data locally before + submitting. This is a schema-as-API pattern for interoperability. + tags: + - system + responses: + "200": + description: Full JSON Schema document + content: + application/json: + schema: + type: object + description: JSON Schema draft-07 document for the canonical incident model + example: + $schema: "http://json-schema.org/draft-07/schema#" + title: "FireForm Canonical Incident" + type: "object" + properties: + schema_version: + type: "string" + +schema_versions: + get: + operationId: getSchemaVersions + summary: Get schema version history + description: | + Returns the version history of the canonical FireForm incident schema, + including changelogs and whether each version introduced breaking changes. + Useful for clients tracking schema evolution across FireForm updates. + tags: + - system + responses: + "200": + description: Schema version history + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/system.yaml#/SchemaVersion" + example: + - version: "1.1.0" + released_at: "2026-02-01T00:00:00Z" + changelog: "Added NERIS fields replacing legacy NFIRS. Added wildland.aerial_operations. Updated incident.types to include neris_code." + breaking_changes: true + - version: "1.0.0" + released_at: "2024-01-01T00:00:00Z" + changelog: "Initial canonical schema with NFIRS support." + breaking_changes: false diff --git a/contracts/path/templates.yaml b/contracts/path/templates.yaml new file mode 100644 index 00000000..c2621cac --- /dev/null +++ b/contracts/path/templates.yaml @@ -0,0 +1,406 @@ +# Layer 6 Template & Configuration Endpoints +# GET /api/v1/templates +# GET /api/v1/templates/{template_id} +# POST /api/v1/templates +# PUT /api/v1/templates/{template_id} +# GET /api/v1/templates/{template_id}/fields +# POST /api/v1/templates/pdf + +templates: + get: + operationId: listTemplates + summary: List all available form templates + description: | + Returns all registered form templates including built-in standard templates + (NERIS, NEMSIS, NIBRS, NFIRS modules, OSHA, etc.) and any custom templates + added for specific jurisdictions. Each template defines the fields, validation + rules, and mapping from the FireForm incident schema. + tags: + - templates + responses: + "200": + description: List of all templates + content: + application/json: + schema: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateSummary" + example: + - template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + display_name: "NERIS Incident Report" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "2.0" + last_updated: "2026-02-01" + field_count: 85 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440071" + form_type: "nemsis_epcr" + display_name: "NEMSIS Electronic Patient Care Report" + jurisdiction: "US-Federal" + agency_type: "ems" + version: "3.5" + last_updated: "2024-01-15" + field_count: 142 + status: "active" + - template_id: "550e8400-e29b-41d4-a716-446655440072" + form_type: "nfirs_basic" + display_name: "NFIRS Basic Module (Legacy)" + jurisdiction: "US-Federal" + agency_type: "fire_department" + version: "5.0" + last_updated: "2015-01-01" + field_count: 65 + status: "legacy" + + post: + operationId: createTemplate + summary: Register a new custom form template + description: | + Registers a new form template for a jurisdiction or agency not yet supported. + This is how FireForm extends to new states, countries, or custom agency forms + without code changes. The template defines all fields, their types, validation + rules, and how each maps from the FireForm incident schema. + tags: + - templates + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + example: + form_type: "state_texas" + display_name: "Texas State Fire Marshal Incident Report" + jurisdiction: "US-TX" + agency_type: "fire_department" + pdf_template_ref: "templates/state_texas.pdf" + fields: + - field_name: "incident_number" + field_type: "string" + required: true + max_length: 20 + description: "State-assigned incident number" + incident_mapping: "report_metadata.incident_number" + layout: + page: 0 + x: 188.33 + y: 621.33 + width: 127.33 + height: 28.67 + font: "Helvetica" + font_size: 10 + color: "#000000" + align: "left" + - field_name: "fire_cause" + field_type: "enum" + required: true + allowed_values: + - "accidental" + - "natural" + - "intentional" + - "undetermined" + incident_mapping: "fire.cause_category" + layout: + page: 0 + x: 188.33 + y: 560.0 + width: 200.0 + height: 18.0 + - field_name: "report_footer" + field_type: "string" + required: false + static_text: "Generated by FireForm" + incident_mapping: null + layout: + page: 0 + x: 72.0 + y: 40.0 + width: 300.0 + height: 12.0 + font_size: 8 + align: "center" + responses: + "201": + description: Template created + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "409": + description: Template with this form_type already exists + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "422": + description: Invalid template definition + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +template_by_id: + get: + operationId: getTemplate + summary: Get full template schema + description: | + Returns the complete template definition including all fields, their types, + required/optional status, validation rules, and the mapping from the FireForm + incident schema fields. Includes the source standard reference where applicable. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + responses: + "200": + description: Full template definition + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + + put: + operationId: updateTemplate + summary: Update an existing template + description: | + Replaces an existing template definition. Used when an upstream standard + changes (e.g., NERIS schema update). Templates used by submitted incidents + cannot be modified create a new version instead. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template to update + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/CreateTemplateRequest" + responses: + "200": + description: Template updated + content: + application/json: + schema: + $ref: "../schemas/template.yaml#/Template" + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "409": + description: Template in use by submitted incidents cannot modify + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + example: + error_code: "TEMPLATE_IN_USE" + message: "Template is referenced by submitted incidents. Create a new version instead." + detail: + submitted_incident_count: 15 + recommendation: "POST /api/v1/templates to create a new version" + +template_fields: + get: + operationId: getTemplateFields + summary: Get template field definitions + description: | + Returns just the fields list for a template with type, required/optional + status, validation rules, and incident-schema mapping. Useful for building + validation checklists and for the validate endpoint to determine requirements. + tags: + - templates + parameters: + - name: template_id + in: path + required: true + description: Unique identifier of the template + schema: + type: string + format: uuid + - name: required_only + in: query + description: If true, return only required fields + schema: + type: boolean + default: false + responses: + "200": + description: List of template fields + content: + application/json: + schema: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + total_fields: + type: integer + required_fields: + type: integer + optional_fields: + type: integer + fields: + type: array + items: + $ref: "../schemas/template.yaml#/TemplateField" + example: + template_id: "550e8400-e29b-41d4-a716-446655440070" + form_type: "neris" + total_fields: 85 + required_fields: 32 + optional_fields: 53 + fields: + - field_name: "incident_type" + field_type: "enum" + required: true + description: "Primary incident type code" + incident_mapping: "incident.types[0].neris_code" + layout: + page: 0 + x: 120.0 + y: 680.0 + width: 160.0 + height: 18.0 + - field_name: "incident_date" + field_type: "date" + required: true + description: "Date of incident" + incident_mapping: "incident.start_datetime" + layout: + page: 0 + x: 120.0 + y: 655.0 + width: 120.0 + height: 18.0 + "404": + description: Template not found + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + +templates_pdf: + post: + operationId: uploadTemplatePdf + summary: Upload a blank PDF for a template before defining its fields + description: | + Uploads the blank agency PDF that a template's field coordinates will be + written onto, and returns a `pdf_template_ref` plus the page geometry. + + This is the first step of the template-authoring flow. The visual editor + renders the returned pages, the user draws a box per field, and the box + coordinates become each field's `layout`. The resulting `pdf_template_ref` + is then sent in the `POST /api/v1/templates` body. Upload precedes create + because the layout coordinates only have meaning relative to this PDF. + + Page dimensions are returned in PDF points (1/72 inch, origin bottom-left) + so the editor can map canvas positions to the same coordinate system the + `layout` fields use. + tags: + - templates + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - pdf_file + properties: + pdf_file: + type: string + format: binary + description: Blank fillable or flat PDF form. Max 50MB. + responses: + "201": + description: PDF stored and ready to be referenced by a template + content: + application/json: + schema: + type: object + required: + - pdf_template_ref + - original_filename + - page_count + - pages + properties: + pdf_template_ref: + type: string + description: Opaque reference to the stored PDF, passed back in + the template body as pdf_template_ref + original_filename: + type: string + page_count: + type: integer + pages: + type: array + description: Per-page geometry in PDF points, index 0 = first page + items: + type: object + required: + - page + - width + - height + properties: + page: + type: integer + width: + type: number + height: + type: number + example: + pdf_template_ref: "templates/uploads/550e8400-e29b-41d4-a716-446655440080.pdf" + original_filename: "texas_sfm_incident.pdf" + page_count: 2 + pages: + - page: 0 + width: 612.0 + height: 792.0 + - page: 1 + width: 612.0 + height: 792.0 + "400": + description: Missing file or malformed multipart request + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "413": + description: PDF exceeds the maximum allowed size + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" + "415": + description: Uploaded file is not a valid PDF + content: + application/json: + schema: + $ref: "../schemas/common.yaml#/ErrorResponse" diff --git a/contracts/path/weather.yaml b/contracts/path/weather.yaml new file mode 100644 index 00000000..601911bd --- /dev/null +++ b/contracts/path/weather.yaml @@ -0,0 +1,108 @@ +forecast: + get: + summary: Fetch weather forecast + description: > + Fetch hourly weather data for the given location and date range. + Coordinates must be provided as decimal degrees (e.g. `40.7128` for latitude, `-74.0060` for longitude). + tags: + - weather + parameters: + - name: latitude + in: query + required: true + description: > + Latitude of the location in decimal degrees. Valid range: -90.0 to 90.0. + Example: `40.7128` (New York City). Do NOT use degree/minute/second notation. + schema: + type: number + format: float + minimum: -90.0 + maximum: 90.0 + example: 40.7128 + - name: longitude + in: query + required: true + description: > + Longitude of the location in decimal degrees. Valid range: -180.0 to 180.0. + Example: `-74.0060` (New York City). Do NOT use degree/minute/second notation. + schema: + type: number + format: float + minimum: -180.0 + maximum: 180.0 + example: -74.0060 + - name: start_date + in: query + required: true + description: > + Start date of the forecast period. Format: `YYYY-MM-DD` (ISO 8601). + Example: `2024-06-01`. + schema: + type: string + format: date + example: "2024-06-01" + - name: end_date + in: query + required: true + description: > + End date of the forecast period (inclusive). Format: `YYYY-MM-DD` (ISO 8601). + Example: `2024-06-07`. Must be >= start_date. + schema: + type: string + format: date + example: "2024-06-07" + - name: fields + in: query + required: false + description: > + Comma-separated list of hourly variables to include in the response. + When omitted, all supported variables are returned. + Supported values: `temperature_2m`, `relative_humidity_2m`, `rain`, + `apparent_temperature`, `precipitation_probability`, `precipitation`, + `wind_direction_10m`, `wind_speed_10m`, `soil_temperature_0cm`, `uv_index`. + Example: `temperature_2m,rain,wind_speed_10m`. + schema: + type: string + example: "temperature_2m,rain,wind_speed_10m" + responses: + '200': + description: Weather data retrieved successfully + content: + application/json: + schema: + type: object + properties: + latitude: + type: number + description: Actual latitude used by the weather model (may differ slightly from input). + longitude: + type: number + description: Actual longitude used by the weather model. + elevation: + type: number + description: Elevation of the location in metres above sea level. + timezone: + type: string + description: Timezone name (e.g. "GMT"). + timezone_abbreviation: + type: string + hourly: + type: object + description: > + Object containing hourly time series. The `date` key holds an array + of ISO 8601 timestamps; all other keys correspond to the requested + fields and contain arrays of the same length. + properties: + date: + type: array + items: + type: string + format: date-time + additionalProperties: + type: array + items: + type: number + '422': + description: Validation error (e.g. coordinates out of range, invalid date format) + '500': + description: Internal server error diff --git a/contracts/path/zipcode.yaml b/contracts/path/zipcode.yaml new file mode 100644 index 00000000..a3480171 --- /dev/null +++ b/contracts/path/zipcode.yaml @@ -0,0 +1,76 @@ +lookup_address: + get: + summary: Resolve address to location + description: > + Geocode a free-form address string to one or more geographic locations. + Each result includes postal code (when available), latitude, longitude, + place name, state, county, and country. + The list is ordered by relevance (most relevant first, up to 10 results). + tags: + - zipcode + parameters: + - name: address + in: query + required: true + description: > + Full address string to geocode. Include as much detail as possible for accurate results. + Examples: "1600 Amphitheatre Parkway, Mountain View, CA, US" + or "100 Main St, Santa Cruz, CA". + City-only queries (e.g. "Santa Cruz") are supported but may return multiple ambiguous results. + schema: + type: string + example: "1600 Amphitheatre Parkway, Mountain View, CA, US" + responses: + '200': + description: > + List of geocoded results ordered by relevance. An empty array means + no results were found for the given address. + content: + application/json: + schema: + type: array + items: + type: object + properties: + postal_code: + type: string + nullable: true + description: Postal/ZIP code for the location, if determinable. + example: "94043" + place_name: + type: string + description: City, town, or village name. + example: "Mountain View" + latitude: + type: number + format: float + description: Latitude in decimal degrees. + example: 37.4224764 + longitude: + type: number + format: float + description: Longitude in decimal degrees. + example: -122.0842499 + display_name: + type: string + description: Full human-readable address as returned by the geocoder. + example: "1600 Amphitheatre Parkway, Mountain View, Santa Clara County, California, 94043, United States" + country: + type: string + nullable: true + description: Country name. + example: "United States" + state: + type: string + nullable: true + description: State or region name. + example: "California" + county: + type: string + nullable: true + description: County or district name. + example: "Santa Clara County" + '504': + description: Geocoding service timed out + '500': + description: Internal server error diff --git a/contracts/schemas/common.yaml b/contracts/schemas/common.yaml new file mode 100644 index 00000000..0798f85d --- /dev/null +++ b/contracts/schemas/common.yaml @@ -0,0 +1,129 @@ +# Common schemas used across multiple endpoints + +ErrorResponse: + type: object + required: + - error_code + - message + properties: + error_code: + type: string + description: Machine-readable error code + example: "EXTRACT_NOT_FOUND" + message: + type: string + description: Human-readable error message + example: "Extraction with ID xyz not found" + detail: + type: object + description: Additional context specific to the error type + additionalProperties: true + retry_after_seconds: + type: integer + description: Present on 503 errors seconds to wait before retrying + validation_errors: + type: array + description: Present on 422 errors list of field-level validation issues + items: + $ref: "#/ValidationError" + +ValidationError: + type: object + properties: + field: + type: string + description: JSON path of the invalid field + example: "fire.cause_certainty" + issue: + type: string + description: Description of the validation issue + example: "Must be one of: confirmed, probable, suspected, undetermined" + value: + description: The invalid value that was provided + +Pagination: + type: object + properties: + total: + type: integer + description: Total number of items across all pages + page: + type: integer + description: Current page number (1-based) + per_page: + type: integer + description: Items per page + total_pages: + type: integer + description: Total number of pages + has_next: + type: boolean + description: Whether there is a next page + has_prev: + type: boolean + description: Whether there is a previous page + +AsyncJobResponse: + type: object + required: + - job_id + - status + properties: + job_id: + type: string + format: uuid + description: Unique job identifier for polling + job_type: + type: string + description: Type of async operation + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + estimated_seconds: + type: integer + description: Estimated time to completion in seconds + poll_url: + type: string + description: Full URL to poll for job status + +Job: + type: object + required: + - job_id + - job_type + - status + properties: + job_id: + type: string + format: uuid + job_type: + type: string + enum: + - transcription + - extraction + - form_generation + - batch_form_generation + - report_generation + status: + $ref: "enums.yaml#/JobStatus" + progress_percent: + type: integer + minimum: 0 + maximum: 100 + description: Progress percentage (0–100) + result_url: + type: string + description: URL of the completed result (present when status is "completed") + error: + $ref: "#/ErrorResponse" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/enums.yaml b/contracts/schemas/enums.yaml new file mode 100644 index 00000000..b79bedae --- /dev/null +++ b/contracts/schemas/enums.yaml @@ -0,0 +1,130 @@ +# Shared enums used across the FireForm API + +InputStatus: + type: string + enum: + - queued + - transcribing + - ready + - failed + description: Status of an input record + +ExtractionStatus: + type: string + enum: + - processing + - completed + - failed + - needs_review + description: Status of an AI extraction job + +FormStatus: + type: string + enum: + - queued + - generating + - completed + - failed + description: Status of a form generation job + +ReportStatus: + type: string + enum: + - draft + - under_review + - approved + - submitted + description: Status of an incident report in the approval workflow + +JobStatus: + type: string + enum: + - queued + - processing + - completed + - failed + description: Status of any async job + +FormType: + type: string + enum: + - neris + - nemsis_epcr + - nibrs + - nfirs_basic + - nfirs_fire + - nfirs_structure + - nfirs_wildland + - nfirs_ems + - nfirs_hazmat + - nfirs_apparatus + - nfirs_personnel + - nfirs_arson + - nfirs_casualty_civilian + - nfirs_casualty_responder + - cal_fire_ics209 + - osha_301 + - un_ssirs + - state_georgia + - state_california + - state_new_york + description: | + Stable string identifier for a form type. Includes the new NERIS standard + (replacing NFIRS as of Feb 2026), legacy NFIRS modules, NEMSIS, NIBRS, + OSHA, state-specific, and international (UN SSIRS) forms. + +IncidentCategory: + type: string + enum: + - fire + - ems + - rescue + - hazardous_conditions + - service_call + - good_intent + - false_alarm + - law_enforcement + description: High-level incident category + +CauseCertainty: + type: string + enum: + - confirmed + - probable + - suspected + - undetermined + description: Certainty level of fire cause determination + +InjurySeverity: + type: string + enum: + - minor + - moderate + - severe + - fatal + description: Severity classification for injuries + +RateOfSpread: + type: string + enum: + - slow + - moderate + - rapid + - extreme + description: Rate of fire spread classification + +PeriodType: + type: string + enum: + - monthly + - quarterly + - annual + description: Reporting period type + +OutputFormat: + type: string + enum: + - pdf + - json + - both + description: Output format for generated forms diff --git a/contracts/schemas/extraction-record.yaml b/contracts/schemas/extraction-record.yaml new file mode 100644 index 00000000..ae0fd487 --- /dev/null +++ b/contracts/schemas/extraction-record.yaml @@ -0,0 +1,134 @@ +# Extraction-related schemas + +ExtractionRequest: + type: object + properties: + model_override: + type: string + description: Override the default LLM model (e.g. "llama3:70b") + extraction_hints: + type: object + description: Optional hints to improve extraction accuracy + properties: + incident_type: + type: string + description: Hint about the incident type (e.g. "wildland_fire", "structure_fire") + state: + type: string + description: US state code to apply state-specific extraction rules + agency_type: + type: string + description: Agency type hint for form selection + additionalProperties: true + +ExtractionCompleted: + type: object + required: + - extract_id + - input_id + - status + - incident_contract + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - completed + completed_at: + type: string + format: date-time + model_used: + type: string + description: LLM model that performed the extraction + processing_time_seconds: + type: number + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" + corrections: + type: array + description: Audit trail of manual corrections applied via PATCH + items: + type: object + properties: + field_path: + type: string + original_value: {} + corrected_value: {} + corrected_at: + type: string + format: date-time + corrected_by: + type: string + +ExtractionProcessing: + type: object + required: + - extract_id + - input_id + - status + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + status: + type: string + enum: + - processing + - failed + started_at: + type: string + format: date-time + retry_after_seconds: + type: integer + description: Polling hint for clients + error_type: + type: string + nullable: true + description: Present when status is "failed" + error_detail: + type: string + nullable: true + partial_result: + $ref: "incident-contract.yaml#/IncidentContract" + +ValidationResult: + type: object + required: + - valid + - form_type + - extract_id + properties: + valid: + type: boolean + description: Whether all required fields for this form type are present + form_type: + $ref: "enums.yaml#/FormType" + extract_id: + type: string + format: uuid + missing_required: + type: array + items: + type: string + description: JSON paths of required fields that are missing + missing_recommended: + type: array + items: + type: string + description: JSON paths of recommended fields that are missing + warnings: + type: array + items: + type: string + description: Human-readable warnings about data quality + field_coverage_percent: + type: number + description: Percentage of form fields that have values diff --git a/contracts/schemas/form-record.yaml b/contracts/schemas/form-record.yaml new file mode 100644 index 00000000..a83ec41a --- /dev/null +++ b/contracts/schemas/form-record.yaml @@ -0,0 +1,198 @@ +# Form generation related schemas + +GenerateAllRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + skip_incomplete: + type: boolean + default: true + description: Skip forms that fail validation + force_partial: + type: boolean + default: false + description: Generate forms even with missing fields (leaving blanks) + +GenerateSingleRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + options: + type: object + properties: + output_format: + $ref: "../schemas/enums.yaml#/OutputFormat" + force_partial: + type: boolean + default: false + force: + type: boolean + default: false + description: Allow generation even if form_type is not in applicable_forms + +BatchGenerateResponse: + type: object + required: + - batch_id + - status + - extract_id + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing] + extract_id: + type: string + format: uuid + forms_queued: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_skipped: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + reason: + type: string + estimated_seconds: + type: integer + poll_url: + type: string + +FormGenerateResponse: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + type: string + enum: [processing, completed] + extract_id: + type: string + format: uuid + job_id: + type: string + format: uuid + estimated_seconds: + type: integer + poll_url: + type: string + +FormRecord: + type: object + required: + - form_id + - form_type + - status + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + extract_id: + type: string + format: uuid + incident_id: + type: string + format: uuid + nullable: true + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + nullable: true + pdf_ready: + type: boolean + json_ready: + type: boolean + field_mapping_summary: + type: object + properties: + total_form_fields: + type: integer + fields_filled: + type: integer + fields_blank: + type: integer + coverage_percent: + type: number + +FormMappedJson: + type: object + required: + - form_type + - form_id + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + form_version: + type: string + form_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + agency_fields: + type: object + additionalProperties: true + description: Agency-specific field names and values as the target system expects + +BatchStatus: + type: object + required: + - batch_id + - status + properties: + batch_id: + type: string + format: uuid + status: + type: string + enum: [processing, completed, failed] + total: + type: integer + completed: + type: integer + failed: + type: integer + forms: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" diff --git a/contracts/schemas/incident-contract.yaml b/contracts/schemas/incident-contract.yaml new file mode 100644 index 00000000..1a129df1 --- /dev/null +++ b/contracts/schemas/incident-contract.yaml @@ -0,0 +1,930 @@ +# Canonical FireForm Incident Schema +# This is the master superset schema single source of truth for all downstream forms + +IncidentContract: + type: object + description: | + The canonical FireForm incident data model. This is the superset schema containing + every field any downstream form could need. Form-specific mappers select only the + relevant fields for each agency template. + properties: + schema_version: + type: string + description: Schema version identifier + example: "1.1.0" + schema_name: + type: string + description: Schema name identifier + enum: + - fireform_incident_contract + + extraction_metadata: + $ref: "#/ExtractionMetadata" + report_metadata: + $ref: "#/ReportMetadata" + incident: + $ref: "#/Incident" + location: + $ref: "#/Location" + fire: + $ref: "#/Fire" + wildland: + $ref: "#/Wildland" + structure: + $ref: "#/Structure" + casualties: + $ref: "#/Casualties" + ems: + $ref: "#/EMS" + hazmat: + $ref: "#/Hazmat" + arson: + $ref: "#/Arson" + responding_agencies: + $ref: "#/RespondingAgencies" + resources_deployed: + $ref: "#/ResourcesDeployed" + weather: + $ref: "#/Weather" + environmental_impact: + $ref: "#/EnvironmentalImpact" + infrastructure_impact: + $ref: "#/InfrastructureImpact" + near_miss_and_safety: + $ref: "#/NearMissAndSafety" + lessons_learned: + $ref: "#/LessonsLearned" + follow_up: + $ref: "#/FollowUp" + periodic_reporting: + $ref: "#/PeriodicReporting" + attachments: + $ref: "#/Attachments" + +# --- Sub-schemas --- + +ExtractionMetadata: + type: object + properties: + extract_id: + type: string + format: uuid + input_id: + type: string + format: uuid + input_type: + type: string + enum: [voice, text] + extracted_at: + type: string + format: date-time + llm_model: + type: string + example: "llama3:8b" + confidence_score: + type: number + minimum: 0 + maximum: 1 + description: Overall confidence score from the LLM extraction (0.0–1.0) + completeness: + $ref: "#/Completeness" + applicable_forms: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +Completeness: + type: object + description: | + Tells the system which forms can be fully auto-generated vs which need + manual review. Recalculated server-side after every PATCH /extract. + properties: + overall_percent: + type: integer + minimum: 0 + maximum: 100 + missing_fields: + type: array + items: + type: string + description: JSON paths of fields that have no value + low_confidence_fields: + type: array + items: + type: string + description: JSON paths of fields where LLM confidence is low + inferred_fields: + type: array + items: + type: string + description: JSON paths of fields that were inferred (not explicitly stated) + forms_fully_generatable: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_needing_review: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + forms_missing_data: + type: array + items: + $ref: "../schemas/enums.yaml#/FormType" + +ReportMetadata: + type: object + properties: + report_id: + type: string + example: "FF-2024-CA-0157" + incident_number: + type: string + example: "CA-SQF-2024-0421" + report_date: + type: string + format: date + report_time: + type: string + format: time + report_status: + $ref: "../schemas/enums.yaml#/ReportStatus" + reporting_unit: + $ref: "#/ReportingUnit" + prepared_by: + type: array + items: + $ref: "#/Personnel" + reviewed_by: + type: array + items: + $ref: "#/Reviewer" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + +ReportingUnit: + type: object + properties: + station_name: + type: string + station_id: + type: string + agency_name: + type: string + agency_id: + type: string + format: uuid + agency_type: + type: string + +Personnel: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + contact_number: + type: string + signature_captured: + type: boolean + +Reviewer: + type: object + properties: + name: + type: string + badge_number: + type: string + rank: + type: string + role: + type: string + reviewed_at: + type: string + format: date-time + approved: + type: boolean + +Incident: + type: object + properties: + name: + type: string + description: Human-readable incident name + types: + type: array + items: + $ref: "#/IncidentType" + start_datetime: + type: string + format: date-time + alarm_datetime: + type: string + format: date-time + first_arrival_datetime: + type: string + format: date-time + containment_datetime: + type: string + format: date-time + nullable: true + controlled_datetime: + type: string + format: date-time + nullable: true + cleared_datetime: + type: string + format: date-time + nullable: true + total_duration_hours: + type: number + nullable: true + narrative: + type: string + description: Free text summary of the incident + raw_transcript: + type: string + description: Original voice or text input verbatim + +IncidentType: + type: object + properties: + primary: + type: boolean + category: + $ref: "../schemas/enums.yaml#/IncidentCategory" + subcategory: + type: string + neris_code: + type: string + description: NERIS incident type code (replaces NFIRS as of Feb 2026) + nfirs_code: + type: string + description: Legacy NFIRS incident type code + +Location: + type: object + properties: + address: + type: string + nullable: true + nearest_landmark: + type: string + nullable: true + nearest_town: + type: string + nullable: true + county: + type: string + nullable: true + state: + type: string + nullable: true + country: + type: string + nullable: true + postal_code: + type: string + nullable: true + coordinates: + $ref: "#/Coordinates" + ignition_point_coordinates: + $ref: "#/CoordinatesBasic" + elevation_range_ft: + type: string + nullable: true + legal_description: + type: string + nullable: true + jurisdiction: + type: object + properties: + federal: + type: boolean + state: + type: boolean + private: + type: boolean + tribal: + type: boolean + property_type: + type: string + nullable: true + property_use: + type: string + nullable: true + dispatch_center: + type: string + nullable: true + +Coordinates: + type: object + properties: + latitude: + type: number + longitude: + type: number + accuracy_meters: + type: number + +CoordinatesBasic: + type: object + properties: + latitude: + type: number + longitude: + type: number + +Fire: + type: object + properties: + cause_category: + type: string + nullable: true + cause_specific: + type: string + nullable: true + cause_certainty: + $ref: "../schemas/enums.yaml#/CauseCertainty" + arson_suspected: + type: boolean + material_first_ignited: + type: string + nullable: true + fuel_types: + type: array + items: + type: string + fire_spread_directions: + type: array + items: + type: string + rate_of_spread: + $ref: "../schemas/enums.yaml#/RateOfSpread" + flame_lengths_ft: + type: string + nullable: true + spotting_distance_miles: + type: number + nullable: true + unusual_behaviors: + type: array + items: + type: string + detector_present: + type: boolean + nullable: true + detector_operated: + type: boolean + nullable: true + suppression_system_present: + type: boolean + nullable: true + suppression_system_operated: + type: boolean + nullable: true + estimated_damage_usd: + type: number + nullable: true + contents_loss_usd: + type: number + nullable: true + +Wildland: + type: object + properties: + is_wildland_incident: + type: boolean + total_acres_burned: + type: number + nullable: true + land_ownership_breakdown: + type: object + properties: + federal_acres: + type: number + state_acres: + type: number + private_acres: + type: number + tribal_acres: + type: number + percent_contained: + type: integer + minimum: 0 + maximum: 100 + fire_lines: + type: object + properties: + primary_line_miles: + type: number + secondary_line_miles: + type: number + dozer_line_miles: + type: number + hand_line_miles: + type: number + aerial_operations: + type: object + properties: + water_drops_gallons: + type: integer + retardant_drops_gallons: + type: integer + total_flight_hours: + type: number + containment_strategies: + type: array + items: + type: string + +Structure: + type: object + properties: + is_structure_involved: + type: boolean + structures_threatened: + type: integer + nullable: true + structures_damaged: + type: integer + nullable: true + structures_destroyed: + type: integer + nullable: true + structures_protected: + type: integer + nullable: true + construction_type: + type: string + nullable: true + stories: + type: integer + nullable: true + area_sqft: + type: number + nullable: true + occupancy_at_time: + type: integer + nullable: true + +Casualties: + type: object + properties: + civilian: + type: array + items: + $ref: "#/CivilianCasualty" + responder: + type: array + items: + $ref: "#/ResponderCasualty" + total_civilian_injuries: + type: integer + total_civilian_fatalities: + type: integer + total_responder_injuries: + type: integer + total_responder_fatalities: + type: integer + +CivilianCasualty: + type: object + properties: + age: + type: integer + nullable: true + sex: + type: string + nullable: true + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + cause: + type: string + nullable: true + location_at_time: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + +ResponderCasualty: + type: object + properties: + personnel_id: + type: string + agency: + type: string + role: + type: string + injury_type: + type: string + severity: + $ref: "../schemas/enums.yaml#/InjurySeverity" + treatment: + type: string + nullable: true + transported: + type: boolean + hospital: + type: string + nullable: true + return_to_duty_date: + type: string + format: date + nullable: true + osha_recordable: + type: boolean + nfirs_5_required: + type: boolean + +EMS: + type: object + properties: + ems_response_required: + type: boolean + patients: + type: array + items: + $ref: "#/EMSPatient" + total_patients: + type: integer + ems_agency_responded: + type: string + nullable: true + nemsis_report_required: + type: boolean + nemsis_report_ids: + type: array + items: + type: string + +EMSPatient: + type: object + properties: + patient_ref_id: + type: string + age_approx: + type: integer + nullable: true + sex: + type: string + nullable: true + chief_complaint: + type: string + nullable: true + disposition: + type: string + nullable: true + transported: + type: boolean + date_of_birth: + type: string + format: date + nullable: true + nemsis_data_captured: + type: boolean + +Hazmat: + type: object + properties: + involved: + type: boolean + materials: + type: array + items: + type: object + properties: + name: + type: string + un_number: + type: string + nullable: true + quantity: + type: string + nullable: true + epa_reportable_quantity_exceeded: + type: boolean + spill_size_gallons: + type: number + nullable: true + +Arson: + type: object + properties: + suspected: + type: boolean + confirmed: + type: boolean + law_enforcement_notified: + type: boolean + investigation_required: + type: boolean + investigation_agency: + type: string + nullable: true + evidence_collected: + type: boolean + nibrs_report_required: + type: boolean + notes: + type: string + nullable: true + +RespondingAgencies: + type: object + properties: + primary_agency: + type: string + all_agencies: + type: array + items: + $ref: "#/RespondingAgency" + mutual_aid_activated: + type: boolean + mutual_aid_agencies: + type: array + items: + type: string + unified_command: + type: boolean + +RespondingAgency: + type: object + properties: + agency_name: + type: string + agency_type: + type: string + role: + type: string + personnel_count: + type: integer + +ResourcesDeployed: + type: object + properties: + total_personnel: + type: integer + personnel_breakdown: + type: object + properties: + firefighters: + type: integer + crew_supervisors: + type: integer + engineers: + type: integer + incident_command: + type: integer + support_staff: + type: integer + apparatus: + type: array + items: + $ref: "#/Apparatus" + crew_types: + type: array + items: + type: string + +Apparatus: + type: object + properties: + type: + type: string + count: + type: integer + +Weather: + type: object + properties: + on_arrival: + $ref: "#/WeatherReading" + worst_conditions: + $ref: "#/WeatherReadingExtended" + factors_influencing_fire: + type: array + items: + type: string + +WeatherReading: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_direction: + type: string + haines_index: + type: integer + nullable: true + +WeatherReadingExtended: + type: object + properties: + datetime: + type: string + format: date-time + temperature_f: + type: number + relative_humidity_percent: + type: number + wind_speed_mph: + type: number + wind_gusts_mph: + type: number + nullable: true + +EnvironmentalImpact: + type: object + properties: + wildlife_habitat_affected_acres: + type: number + nullable: true + watershed_impact: + type: string + nullable: true + soil_erosion_risk: + type: string + nullable: true + sensitive_species_affected: + type: array + items: + type: string + air_quality_impact: + type: string + nullable: true + water_body_affected: + type: boolean + nullable: true + +InfrastructureImpact: + type: object + properties: + items: + type: array + items: + $ref: "#/InfrastructureItem" + +InfrastructureItem: + type: object + properties: + type: + type: string + unit: + type: string + quantity: + type: number + severity: + type: string + +NearMissAndSafety: + type: object + properties: + near_miss_events: + type: array + items: + type: object + properties: + description: + type: string + date: + type: string + format: date + contributing_factors: + type: array + items: + type: string + lessons_learned: + type: string + nullable: true + corrective_action: + type: string + nullable: true + safety_breaches: + type: integer + weather_related_risks: + type: array + items: + type: string + +LessonsLearned: + type: object + properties: + successful_tactics: + type: array + items: + type: string + areas_for_improvement: + type: array + items: + type: string + recommendations: + type: array + items: + type: string + +FollowUp: + type: object + properties: + mop_up: + type: object + properties: + percent_complete: + type: integer + estimated_completion_date: + type: string + format: date + personnel_assigned: + type: integer + rehabilitation: + type: object + properties: + erosion_control_acres: + type: number + reseeding_acres: + type: number + hazard_tree_removal_required: + type: boolean + next_inspection_date: + type: string + format: date + nullable: true + investigation_ongoing: + type: boolean + +PeriodicReporting: + type: object + properties: + contributes_to_monthly_report: + type: boolean + contributes_to_quarterly_report: + type: boolean + contributes_to_annual_report: + type: boolean + neris_submitted: + type: boolean + neris_submitted_at: + type: string + format: date-time + nullable: true + state_submitted: + type: boolean + state_submitted_at: + type: string + format: date-time + nullable: true + +Attachments: + type: object + properties: + maps: + type: boolean + photos_count: + type: integer + weather_charts: + type: boolean + resource_tracking_logs: + type: boolean + incident_action_plans: + type: boolean + attachment_refs: + type: array + items: + type: object + properties: + ref_id: + type: string + format: uuid + filename: + type: string + content_type: + type: string + size_bytes: + type: integer diff --git a/contracts/schemas/incident-record.yaml b/contracts/schemas/incident-record.yaml new file mode 100644 index 00000000..686e7ce8 --- /dev/null +++ b/contracts/schemas/incident-record.yaml @@ -0,0 +1,150 @@ +# Incident management schemas + +CreateIncidentRequest: + type: object + required: + - extract_id + properties: + extract_id: + type: string + format: uuid + incident_number: + type: string + description: Optional org-assigned incident number + tags: + type: array + items: + type: string + +UpdateIncidentRequest: + type: object + properties: + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + tags: + type: array + items: + type: string + incident_number: + type: string + notes: + type: string + +IncidentRecord: + type: object + required: + - incident_id + - extract_id + - status + properties: + incident_id: + type: string + format: uuid + extract_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_generated: + type: array + items: + type: object + properties: + form_id: + type: string + format: uuid + form_type: + $ref: "../schemas/enums.yaml#/FormType" + status: + $ref: "../schemas/enums.yaml#/FormStatus" + tags: + type: array + items: + type: string + notes: + type: string + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + deleted_at: + type: string + format: date-time + nullable: true + +IncidentRecordFull: + description: Full incident record with linked extraction and forms + allOf: + - $ref: "#/IncidentRecord" + - type: object + properties: + incident_contract: + $ref: "incident-contract.yaml#/IncidentContract" + forms: + type: array + items: + $ref: "form-record.yaml#/FormRecord" + submission_log: + type: array + items: + type: object + properties: + form_type: + $ref: "../schemas/enums.yaml#/FormType" + submitted_at: + type: string + format: date-time + submitted_to: + type: string + status: + type: string + +IncidentListResponse: + type: object + properties: + data: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + nullable: true + status: + $ref: "../schemas/enums.yaml#/ReportStatus" + incident_name: + type: string + nullable: true + incident_type: + type: string + nullable: true + incident_date: + type: string + format: date + nullable: true + forms_count: + type: integer + created_at: + type: string + format: date-time + pagination: + $ref: "common.yaml#/Pagination" diff --git a/contracts/schemas/input-record.yaml b/contracts/schemas/input-record.yaml new file mode 100644 index 00000000..65dd2055 --- /dev/null +++ b/contracts/schemas/input-record.yaml @@ -0,0 +1,138 @@ +# Input-related schemas + +TextInputRequest: + type: object + required: + - narrative + properties: + narrative: + type: string + description: Free-text incident narrative (10+ words, max 50,000 characters) + minLength: 20 + maxLength: 50000 + station_id: + type: string + description: Station identifier + responder_badge: + type: string + description: Badge number of the reporting responder + incident_date_hint: + type: string + format: date + description: Approximate date of the incident + +VoiceInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - queued + input_type: + type: string + enum: + - voice + estimated_processing_seconds: + type: integer + created_at: + type: string + format: date-time + job_id: + type: string + format: uuid + description: Job ID for polling transcription status + poll_url: + type: string + description: URL to poll for input status + +TextInputResponse: + type: object + required: + - input_id + - status + - input_type + properties: + input_id: + type: string + format: uuid + status: + type: string + enum: + - ready + input_type: + type: string + enum: + - text + character_count: + type: integer + word_count: + type: integer + created_at: + type: string + format: date-time + +InputRecord: + type: object + required: + - input_id + - input_type + - status + properties: + input_id: + type: string + format: uuid + input_type: + type: string + enum: + - voice + - text + status: + $ref: "enums.yaml#/InputStatus" + transcript: + type: string + nullable: true + description: Transcribed text (for voice) or original narrative (for text). Null while transcribing. + original_filename: + type: string + nullable: true + description: Original audio filename (voice inputs only) + audio_duration_seconds: + type: number + nullable: true + description: Duration of audio in seconds (voice inputs only) + character_count: + type: integer + nullable: true + word_count: + type: integer + nullable: true + station_id: + type: string + nullable: true + responder_badge: + type: string + nullable: true + incident_date_hint: + type: string + format: date + nullable: true + error_detail: + type: string + nullable: true + description: Error message if transcription failed + retry_after_seconds: + type: integer + description: Polling hint present when status is queued or transcribing + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time diff --git a/contracts/schemas/reporting.yaml b/contracts/schemas/reporting.yaml new file mode 100644 index 00000000..f352e709 --- /dev/null +++ b/contracts/schemas/reporting.yaml @@ -0,0 +1,112 @@ +# Reporting & Analytics schemas + +ReportSummary: + type: object + properties: + date_from: + type: string + format: date + date_to: + type: string + format: date + total_incidents: + type: integer + by_type: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by category + by_status: + type: object + additionalProperties: + type: integer + description: Incident counts grouped by report status + forms_generated: + type: integer + avg_completeness_score: + type: number + avg_processing_time_seconds: + type: number + groups: + type: array + items: + type: object + properties: + period: + type: string + description: Period label (e.g. "2024-07" for monthly, "fire" for by type) + incident_count: + type: integer + forms_generated: + type: integer + +GenerateReportRequest: + type: object + required: + - period_type + - year + properties: + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + year: + type: integer + minimum: 2000 + maximum: 2100 + month: + type: integer + minimum: 1 + maximum: 12 + description: Required when period_type is "monthly" + quarter: + type: integer + minimum: 1 + maximum: 4 + description: Required when period_type is "quarterly" + format: + $ref: "../schemas/enums.yaml#/OutputFormat" + +PeriodicReport: + type: object + properties: + report_id: + type: string + format: uuid + period_type: + $ref: "../schemas/enums.yaml#/PeriodType" + period_label: + type: string + description: Human-readable period (e.g. "July 2024", "Q3 2024", "2024") + generated_at: + type: string + format: date-time + summary: + $ref: "#/ReportSummary" + incidents: + type: array + items: + type: object + properties: + incident_id: + type: string + format: uuid + incident_number: + type: string + incident_date: + type: string + format: date + incident_type: + type: string + status: + type: string + forms_generated: + type: integer + compliance: + type: object + properties: + neris_submission_rate: + type: number + description: Percentage of incidents with NERIS submitted + average_submission_delay_days: + type: number + overdue_incidents: + type: integer diff --git a/contracts/schemas/system.yaml b/contracts/schemas/system.yaml new file mode 100644 index 00000000..8c256932 --- /dev/null +++ b/contracts/schemas/system.yaml @@ -0,0 +1,101 @@ +# System & health check schemas + +HealthStatus: + type: object + required: + - status + - version + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + version: + type: string + description: FireForm API version + uptime_seconds: + type: integer + components: + type: object + properties: + database: + $ref: "#/ComponentHealth" + ollama: + $ref: "#/ComponentHealth" + whisper: + $ref: "#/ComponentHealth" + storage: + $ref: "#/ComponentHealth" + +ComponentHealth: + type: object + required: + - status + properties: + status: + type: string + enum: + - healthy + - degraded + - unhealthy + response_time_ms: + type: integer + nullable: true + detail: + type: string + nullable: true + model_loaded: + type: string + nullable: true + description: Currently loaded model (ollama component) + disk_free_gb: + type: number + nullable: true + description: Free disk space (storage component) + ollama_version: + type: string + nullable: true + description: Ollama server version (ollama component) + models_available: + type: array + nullable: true + description: All models pulled and available on the Ollama server (ollama component) + items: + type: object + properties: + name: + type: string + size_gb: + type: number + quantization: + type: string + nullable: true + loaded: + type: boolean + current_load: + type: object + nullable: true + description: Current processing load (ollama component) + properties: + active_requests: + type: integer + queued_requests: + type: integer + +SchemaVersion: + type: object + required: + - version + - released_at + properties: + version: + type: string + released_at: + type: string + format: date-time + changelog: + type: string + breaking_changes: + type: boolean diff --git a/contracts/schemas/template.yaml b/contracts/schemas/template.yaml new file mode 100644 index 00000000..f9ce9214 --- /dev/null +++ b/contracts/schemas/template.yaml @@ -0,0 +1,203 @@ +# Template & Configuration schemas + +TemplateSummary: + type: object + properties: + template_id: + type: string + format: uuid + form_type: + type: string + description: Unique form type identifier (built-in or custom jurisdiction) + display_name: + type: string + jurisdiction: + type: string + nullable: true + description: Jurisdiction code (e.g. "US-Federal", "US-CA", "US-GA") + agency_type: + type: string + nullable: true + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: + - active + - legacy + - draft + +CreateTemplateRequest: + type: object + required: + - form_type + - display_name + - fields + properties: + form_type: + type: string + description: Unique form type identifier + display_name: + type: string + jurisdiction: + type: string + nullable: true + description: Jurisdiction code. Optional — the visual editor may register a + template before jurisdiction is assigned. + agency_type: + type: string + nullable: true + fields: + type: array + items: + $ref: "#/TemplateField" + source_standard: + type: string + nullable: true + description: Reference to the source standard (e.g. "NERIS v2.0", "NFIRS 5.0") + pdf_template_ref: + type: string + nullable: true + description: Reference to the PDF template file the layout coordinates apply to + +Template: + allOf: + - type: object + properties: + template_id: + type: string + format: uuid + version: + type: string + last_updated: + type: string + format: date + field_count: + type: integer + status: + type: string + enum: [active, legacy, draft] + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + - $ref: "#/CreateTemplateRequest" + +TemplateField: + type: object + required: + - field_name + - field_type + - required + properties: + field_name: + type: string + description: Field identifier within this template + field_type: + type: string + enum: + - string + - integer + - number + - boolean + - date + - datetime + - time + - enum + - text + - array + description: Data type of the field + required: + type: boolean + description: + type: string + nullable: true + max_length: + type: integer + nullable: true + min_value: + type: number + nullable: true + max_value: + type: number + nullable: true + allowed_values: + type: array + items: + type: string + nullable: true + description: Valid values for enum fields + incident_mapping: + type: string + nullable: true + description: | + JSON path in the FireForm incident schema this field pulls its value from + (e.g. "fire.cause_category"). Null for a static field. Exactly one of + incident_mapping or static_text is expected. + static_text: + type: string + nullable: true + description: | + Fixed text drawn into the field instead of a mapped value. Null for a + data-mapped field. Mutually exclusive with incident_mapping. + default_value: + nullable: true + description: Default value if the mapped incident field is null + layout: + nullable: true + description: Visual placement of the field on the PDF. Null for fields with + no fixed position. + allOf: + - $ref: "#/TemplateFieldLayout" + +TemplateFieldLayout: + type: object + description: | + Visual placement of a field on the PDF page. Coordinates are in PDF points + with the origin at the bottom-left of the page, so the field box runs from + start = (x, y) to end = (x + width, y + height). The editor is responsible + for converting rendered-canvas pixels to PDF points before saving. + required: + - page + - x + - y + - width + - height + properties: + page: + type: integer + description: Zero-based page index + x: + type: number + description: Lower-left X of the box, in PDF points + y: + type: number + description: Lower-left Y of the box, in PDF points (origin bottom-left) + width: + type: number + height: + type: number + font: + type: string + default: Helvetica + font_size: + type: number + default: 10 + color: + type: string + default: "#000000" + description: Hex color, e.g. "#000000" + align: + type: string + enum: + - left + - center + - right + default: left diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 00000000..d5ccca58 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,32 @@ +# Copy this file to .env.dev and fill in values. + +# --- App ------------------------------------------------------------------ +APP_PORT=8000 + +# --- Database ------------------------------------------------------------- +# Dev: postgres runs inside Docker Compose, no config needed. +DATABASE_URL=postgresql://fireform:fireform@localhost:5432/fireform + +# --- Ollama --------------------------------------------------------------- +# Ollama runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Ollama instance (e.g. a GPU server). +OLLAMA_HOST=http://ollama:11434 +OLLAMA_MODEL=qwen2.5:1.5b +OLLAMA_TIMEOUT=300 + +# --- Whisper -------------------------------------------------------------- +# Whisper runs in Docker with port mapped to host. App runs on host. +# Override to point at an external Whisper endpoint. +WHISPER_HOST=http://whisper:9000 +WHISPER_MODEL=small.en + +# --- Celery / Redis ------------------------------------------------------- +CELERY_BROKER_URL=redis://redis:6379/0 +CELERY_RESULT_BACKEND=redis://redis:6379/0 + +# --- Gunicorn (prod only) ------------------------------------------------- +GUNICORN_WORKERS=2 + +# --- CORS ----------------------------------------------------------------- +# Comma-separated list of allowed frontend origins. +FRONTEND_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 00000000..e238c640 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,31 @@ +# Docker + +``` +docker/ + dev/ + Dockerfile # uvicorn --reload, source-mounted for hot reload + compose.yml + prod/ + Dockerfile # multi-stage build, gunicorn, no source mount + compose.yml + .env.example # template — copy to .env.dev or .env.prod + .env.dev # gitignored, dev values + .env.prod # gitignored, prod values + entrypoint.sh # runs DB migrations then exec's the server command + README.md +``` + +## Env vars + +See `.env.example` for the full list with descriptions. + +## Volumes + +`docker compose down` never removes volumes. Use `docker compose down -v` only to intentionally wipe all data. + +| Volume | Path in container | Purpose | +| ------------------ | ---------------------- | ----------------------------------- | +| `fireform_db` | `/data/db/fireform.db` | SQLite database | +| `fireform_uploads` | `/data/uploads` | Uploaded templates + generated PDFs | +| `ollama_data` | `/root/.ollama` | Ollama model weights | +| `whisper_models` | `/data/whisper` | Whisper model cache | diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 00000000..a5a42c1b --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 +FROM python:3.11-slim + +WORKDIR /app + +# apt cache mounts keep downloaded .debs across builds +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 \ + libpq-dev \ + build-essential \ + g++ + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +COPY requirements.txt . + +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt + +# Bake a hash of the deps the image was built with. The entrypoint compares this +# against the live (bind-mounted) requirements.txt to decide whether to reinstall. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash + +ENV PYTHONPATH=/app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker/dev/compose.yml b/docker/dev/compose.yml new file mode 100644 index 00000000..820711d1 --- /dev/null +++ b/docker/dev/compose.yml @@ -0,0 +1,161 @@ +services: + postgres: + image: postgres:17-alpine + container_name: fireform-postgres + environment: + POSTGRES_USER: fireform + POSTGRES_PASSWORD: fireform + POSTGRES_DB: fireform + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fireform"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + start_interval: 1s + + ollama: + image: ollama/ollama:latest + container_name: fireform-ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "ollama list || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + start_interval: 2s + + whisper: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: fireform-whisper + environment: + - ASR_ENGINE=faster_whisper + - ASR_MODEL=${WHISPER_MODEL:-small.en} + - ASR_MODEL_PATH=/data/whisper + volumes: + - whisper_models:/data/whisper + ports: + - "127.0.0.1:9000:9000" + networks: + - fireform-network + healthcheck: + test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 60s + start_interval: 3s + + redis: + image: redis:7-alpine + container_name: fireform-redis + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis_data:/data + networks: + - fireform-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + start_interval: 1s + + app: + build: + context: ../.. + dockerfile: docker/dev/Dockerfile + container_name: fireform-app + depends_on: + postgres: + condition: service_healthy + ollama: + condition: service_healthy + whisper: + condition: service_started + redis: + condition: service_healthy + entrypoint: ["sh", "docker/entrypoint.sh"] + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + volumes: + - ../..:/app + - fireform_uploads:/data/uploads + ports: + - "${APP_PORT:-8000}:8000" + environment: + - PYTHONUNBUFFERED=1 + - CUDA_VISIBLE_DEVICES= + - PYTHONPATH=/app + - DATABASE_URL=postgresql://fireform:fireform@postgres:5432/fireform + - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} + - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} + - WHISPER_HOST=${WHISPER_HOST:-http://whisper:9000} + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=${FIREFORM_DB_ECHO:-true} + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS:-http://localhost:5173,http://127.0.0.1:5173} + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + networks: + - fireform-network + + celery-worker: + build: + context: ../.. + dockerfile: docker/dev/Dockerfile + container_name: fireform-celery-worker + depends_on: + redis: + condition: service_healthy + postgres: + condition: service_healthy + ollama: + condition: service_healthy + command: ["celery", "-A", "app.core.celery:celery_app", "worker", "--loglevel=info", "--concurrency=4"] + volumes: + - ../..:/app + - fireform_uploads:/data/uploads + environment: + - PYTHONUNBUFFERED=1 + - PYTHONPATH=/app + - DATABASE_URL=postgresql://fireform:fireform@postgres:5432/fireform + - OLLAMA_HOST=${OLLAMA_HOST:-http://ollama:11434} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT:-300} + - OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b} + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + networks: + - fireform-network + +volumes: + postgres_data: + driver: local + ollama_data: + driver: local + whisper_models: + driver: local + fireform_uploads: + driver: local + redis_data: + driver: local + +networks: + fireform-network: + driver: bridge diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..a4832be7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +mkdir -p /data/uploads + +# Reinstall deps only when the live requirements.txt differs from what the image +# was built with. The image bakes the hash at /opt/req_hash; in dev the live file +# comes from the bind mount. Matching hash => deps already baked in => skip (instant). +BAKED_HASH=$(cat /opt/req_hash 2>/dev/null || echo "none") +LIVE_HASH=$(sha256sum requirements.txt 2>/dev/null | cut -d' ' -f1 || echo "unknown") + +if [ "$BAKED_HASH" = "$LIVE_HASH" ]; then + echo "[entrypoint] dependencies up to date — skipping install" +else + echo "[entrypoint] requirements.txt changed since image build — syncing deps..." + if command -v uv > /dev/null 2>&1; then + UV_TORCH_BACKEND=cpu uv pip install --system -r requirements.txt + else + pip install -r requirements.txt + fi +fi + +python3 -m app.db.init_db + +exec "$@" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 00000000..ed65a283 --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,51 @@ +# ---- builder ---- +FROM python:3.11-slim AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential g++ libpq-dev && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +WORKDIR /build +COPY requirements.txt . + +RUN --mount=type=cache,target=/root/.cache/uv \ + UV_TORCH_BACKEND=cpu \ + uv pip install --system -r requirements.txt + +# ---- runtime ---- +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + libgl1 \ + libglib2.0-0 \ + libxcb1 \ + libpq5 && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +COPY app/ ./app/ +COPY requirements.txt . +COPY docker/entrypoint.sh /entrypoint.sh + +# Bake deps hash so the entrypoint can skip the redundant install when unchanged. +RUN sha256sum requirements.txt | cut -d' ' -f1 > /opt/req_hash + +ENV PYTHONPATH=/app + +RUN mkdir -p /data/db /data/uploads && chmod +x /entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["gunicorn", "app.main:app", \ + "--worker-class", "uvicorn.workers.UvicornWorker", \ + "--bind", "0.0.0.0:8000", \ + "--access-logfile", "-", \ + "--error-logfile", "-"] diff --git a/docker/prod/compose.postgres.yml b/docker/prod/compose.postgres.yml new file mode 100644 index 00000000..30e13dc5 --- /dev/null +++ b/docker/prod/compose.postgres.yml @@ -0,0 +1,22 @@ +services: + postgres: + image: postgres:17-alpine + container_name: fireform-postgres-prod + environment: + POSTGRES_USER: ${POSTGRES_USER:-fireform} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-fireform} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fireform}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/docker-compose.yml b/docker/prod/compose.yml similarity index 51% rename from docker-compose.yml rename to docker/prod/compose.yml index a9e8e6ed..9ac8f39f 100644 --- a/docker-compose.yml +++ b/docker/prod/compose.yml @@ -8,6 +8,7 @@ services: - ollama_data:/root/.ollama networks: - fireform-network + restart: unless-stopped healthcheck: test: ["CMD-SHELL", "ollama list || exit 1"] interval: 10s @@ -16,15 +17,11 @@ services: start_period: 30s whisper: - # Multi-arch (arm64 + amd64) Whisper ASR service — runs natively on Apple - # Silicon. Uses the faster-whisper (CTranslate2) engine and bundles ffmpeg, - # so it accepts any audio the browser produces. Model is pulled from - # Hugging Face on first request into the whisper_models volume. image: onerahmet/openai-whisper-asr-webservice:latest container_name: fireform-whisper environment: - ASR_ENGINE=faster_whisper - - ASR_MODEL=small.en + - ASR_MODEL=${WHISPER_MODEL} - ASR_MODEL_PATH=/data/whisper volumes: - whisper_models:/data/whisper @@ -32,6 +29,7 @@ services: - "127.0.0.1:9000:9000" networks: - fireform-network + restart: unless-stopped healthcheck: test: ["CMD-SHELL", "python3 -c \"import urllib.request; urllib.request.urlopen('http://localhost:9000/docs')\" || exit 1"] interval: 15s @@ -41,55 +39,47 @@ services: app: build: - context: . - dockerfile: Dockerfile + context: ../.. + dockerfile: docker/prod/Dockerfile container_name: fireform-app depends_on: ollama: condition: service_healthy whisper: condition: service_started - command: /bin/sh -c "python3 -m api.db.init_db && python3 -m uvicorn api.main:app --host 0.0.0.0 --port 8000" + command: ["gunicorn", "app.main:app", + "--worker-class", "uvicorn.workers.UvicornWorker", + "--bind", "0.0.0.0:8000", + "--access-logfile", "-", + "--error-logfile", "-"] volumes: - - .:/app - # Persist the SQLite DB (~/.fireform) across container rebuilds so created - # templates aren't wiped each time the image is recreated. - - fireform_db:/root/.fireform + - fireform_uploads:/data/uploads ports: - - "8000:8000" + - "${APP_PORT}:8000" environment: - PYTHONUNBUFFERED=1 - - CUDA_VISIBLE_DEVICES="" - # Fix #118 #116 — correct PYTHONPATH to project root + - CUDA_VISIBLE_DEVICES= - PYTHONPATH=/app - - OLLAMA_HOST=http://ollama:11434 - - OLLAMA_TIMEOUT=300 - - OLLAMA_MODEL=qwen2.5:1.5b - - WHISPER_HOST=http://whisper:9000 - networks: - - fireform-network - - frontend: - image: python:3.11-slim - container_name: fireform-frontend - working_dir: /app - command: python3 -m http.server 5173 --directory frontend - volumes: - - .:/app - ports: - - "5173:5173" - depends_on: - app: - condition: service_started + - DATABASE_URL=${DATABASE_URL} + - OLLAMA_HOST=${OLLAMA_HOST} + - OLLAMA_TIMEOUT=${OLLAMA_TIMEOUT} + - OLLAMA_MODEL=${OLLAMA_MODEL} + - WHISPER_HOST=${WHISPER_HOST} + - FIREFORM_DATA_DIR=/data/uploads + - FIREFORM_TEMPLATE_DIR=/data/uploads + - FIREFORM_DB_ECHO=false + - FRONTEND_ORIGINS=${FRONTEND_ORIGINS} + - WEB_CONCURRENCY=${GUNICORN_WORKERS} networks: - fireform-network + restart: unless-stopped volumes: ollama_data: driver: local whisper_models: driver: local - fireform_db: + fireform_uploads: driver: local networks: diff --git a/docs/1. SETUP.md b/docs/1. SETUP.md new file mode 100644 index 00000000..30d2aeee --- /dev/null +++ b/docs/1. SETUP.md @@ -0,0 +1,99 @@ +# Setup Guide + +This guide gets the FireForm backend running locally with Docker. It assumes you are comfortable with git and a terminal, but new to this project. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) **24 or newer**, with the Docker daemon running +- Docker Compose v2 (bundled with Docker Desktop; verify with `docker compose version`) +- `make` +- ~3 GB of free disk space (Docker images + LLM model weights) + +## Setup + +### 1. Clone the repository + +```bash +git clone https://github.com/fireform-core/FireForm.git +cd FireForm +``` + +### 2. Run first-time setup + +```bash +make init +``` + +This will: + +1. Check that Docker meets the requirements above +2. Create `docker/.env.dev` from `docker/.env.example` (gitignored; defaults work out of the box) +3. Prompt you to pick an Ollama model (the default, `qwen2.5:1.5b`, is the smallest and fine for development) +4. Offer to build and start everything answer `y`, or run `make fireform` later + +### 3. Build and start (if you skipped it in step 2) + +```bash +make fireform +``` + +This builds the Docker images, starts the containers, waits for Ollama, and pulls the LLM model. The first run takes several minutes (image build + model download); later runs are fast. + +When it finishes you'll see: + +``` +FireForm is ready! + API: http://localhost:8000 + API Docs: http://localhost:8000/docs +``` + +### 4. Verify it works + +Open **http://localhost:8000/docs** in your browser. This is the interactive Swagger UI you can explore and test every API endpoint directly from there (expand an endpoint, click _Try it out_, then _Execute_). + +## Day-to-day commands + +| Command | What it does | +| ------------ | ------------------------------------------------------------------------ | +| `make up` | Start containers | +| `make down` | Stop containers (data is preserved) | +| `make logs` | Stream all container logs (`make logs-app` / `make logs-ollama` for one) | +| `make shell` | Open a shell inside the app container | +| `make test` | Run the test suite | +| `make help` | List all commands | + +The dev container mounts the source code, so code changes reload automatically — no rebuild needed. Rebuild (`make build`) only when dependencies in `requirements.txt` or the Dockerfile change. + +## Frontend (optional) + +The desktop/web frontend lives in a separate repository: + +```bash +git clone https://github.com/fireform-core/fireform-frontend.git +``` + +Follow the README in that repository to run it. The backend from this guide must be running for the frontend to work. + +## Troubleshooting + +**`make init` fails dependency checks** +Docker isn't running or is too old. Start Docker Desktop (or the Docker daemon) and confirm `docker version` reports 24+. + +**Port 8000 already in use** +Another process is bound to the port. Either stop it, or change `APP_PORT` in `docker/.env.dev` and run `make down && make up`. + +**Model pull is slow or times out** +The first `make fireform` downloads the LLM weights (~1 GB for the default model). On a slow connection just wait, or re-run `make pull-model` it resumes safely. + +**Containers start but the API doesn't respond** +Check `make logs-app` for the actual error. The entrypoint runs database migrations on startup, so the API takes a few seconds after the container starts. + +**Want a clean slate** +`make super-clean` stops everything and **deletes all volumes** database, uploads, and downloaded model weights. Only use it when you intend to wipe all local data. + +## Where to go next + +- **Join our [Discord](https://discord.gg/nBv5b6kF68)** — ask questions and coordinate with other contributors +- [CONTRIBUTING.md](../CONTRIBUTING.md) - how to contribute +- [PROJECT_STRUCTURE.md](2.%20PROJECT_STRUCTURE.md) - how the codebase is organized and where new code goes +- [docker/README.md](../docker/README.md) - Docker layout, env vars, and volumes in detail diff --git a/docs/2. PROJECT_STRUCTURE.md b/docs/2. PROJECT_STRUCTURE.md new file mode 100644 index 00000000..cee9c40f --- /dev/null +++ b/docs/2. PROJECT_STRUCTURE.md @@ -0,0 +1,87 @@ +# Project Structure + +This document explains how the FireForm repository is organized and, when you add new code, where it should go. + +## Top-level layout + +``` +FireForm/ +├── app/ # Backend source code (FastAPI application) +├── tests/ # Test suite (pytest) +├── docker/ # Dockerfiles, compose files, env templates +├── scripts/ # Shell scripts +├── docs/ # Project documentation +├── examples/ # Standalone demo scripts +├── data/ # Local runtime data (gitignored contents) +├── Makefile # Entry point for all dev commands (`make help`) +└── requirements.txt # Python dependencies +``` + +| You want to… | Go to | +| -------------------------------- | ------------------------------------------------------- | +| Change backend behavior | `app/` | +| Add or fix tests | `tests/` | +| Change container setup, env vars | `docker/` (see [docker/README.md](../docker/README.md)) | +| Change setup/bootstrap scripts | `scripts/` | +| Add documentation | `docs/` | +| Add dev commands | `Makefile` | + +## Inside `app/` + +The backend follows a layered structure: **routes → services → repositories → database**. Requests enter through the API layer, business logic lives in services, and all database access goes through repositories. + +``` +app/ +├── main.py # App factory: creates FastAPI app, wires middleware and routers +├── api/ # HTTP layer - request/response handling only, no business logic +│ ├── router.py # Aggregates all route modules into one router; main.py mounts this +│ ├── deps.py # Shared FastAPI dependencies (e.g. DB session injection) +│ ├── routes/ # One module per feature (forms.py, templates.py, …) +│ └── schemas/ # Pydantic request/response models, one module per feature +├── core/ # App-wide infrastructure +│ ├── config.py # All settings and env var reading nothing else reads os.environ +│ ├── lifespan.py # Startup/shutdown logic (DB init, etc.) +│ ├── logging.py # Logging configuration +│ └── errors/ # Custom exception classes (base.py) and handlers (handlers.py) +├── services/ # Business logic — LLM extraction, PDF filling, orchestration +├── db/ # Database engine/session (database.py) and repositories.py +├── models/ # SQLAlchemy ORM models +└── utils/ # Small generic helpers with no business logic +``` + +### Where does my new code go? + +**Adding a new API endpoint:** + +1. `app/api/schemas/<...>.py` - Pydantic models for the request and response bodies +2. `app/api/routes/<...>.py` - the route handlers; keep them thin, delegate to a service +3. `app/api/router.py` - register the new router (one `include_router` line) +4. Business logic goes in `app/services/`, not in the route handler + +**Adding business logic:** `app/services/`. A service should not know about HTTP (no FastAPI imports) it takes plain data in and returns plain data, so it can be tested and reused independently. + +**Adding a database table:** define the ORM model in `app/models/models.py`, and add its query/persistence functions to `app/db/repositories.py`. Services call repositories; routes never touch the database directly. + +**Adding a setting or env var:** declare it in `app/core/config.py` and document it in `docker/.env.example`. Code elsewhere imports from `config`, never reads `os.environ` itself. + +**Adding a custom error:** subclass in `app/core/errors/base.py`; map it to an HTTP response in `app/core/errors/handlers.py`. + +## Tests + +Tests live in `tests/`, run with `make test` (pytest inside the app container). `conftest.py` holds shared fixtures. Name files `test_.py` and mirror what you're testing: API endpoint tests alongside `test_api.py`, model/DB tests alongside `test_model.py`. +Note: Tests will be undergoing many changes hence the docs can be outdated, Please raise issue if tests or docs are outdated. + +## Docker & scripts + +- `docker/dev/` - development image (hot reload, source mounted) and its compose file. This is what `make up` runs. +- `docker/prod/` - production image (multi-stage build, gunicorn, no source mount). +- `docker/.env.example` - template for env vars; copied to `.env.dev` by `make init`. +- `scripts/` - shell scripts invoked by `make init` (dependency checks, env file creation, model selection) and by container startup. Not meant to be run directly. + +Full details: [docker/README.md](../docker/README.md). + +## Everything else + +- `examples/` - runnable demo scripts showing the pipeline end to end; not imported by the app. +- `data/` - runtime working data on your machine; contents are not part of the codebase. +- `src/`, `temp/` - scratch/legacy directories slated for cleanup; don't add new code here. diff --git a/docs/dpg.html b/docs/dpg.html deleted file mode 100644 index 47371bc6..00000000 --- a/docs/dpg.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - FireForm - Digital Public Good & SDG Relevance - - - - - - - -
-
-
-
- - - -
-
-
-
Digital Public Good
-

SDG Relevance

-

- FireForm is proud to be an open-source Digital Public Good, dedicated to advancing the United Nations' Sustainable Development Goals (SDGs). By drastically reducing the administrative burden on first responders, we enable emergency services to operate more efficiently, safely, and transparently. -

-
-
-
- -
-
-

Relevant Sustainable Development Goals

- -
- -
-
🏢
-

SDG 16: Peace, Justice and Strong Institutions

-

Target 16.6: Develop effective, accountable and transparent institutions at all levels.

-

How FireForm contributes: By unifying and digitizing the reporting structure for firefighters and other emergency responders, FireForm builds stronger, more accountable local institutions. Emergency services can seamlessly share critical incident data across county lines, sheriff departments, and EMS, ensuring transparency and highly effective public service administration without the overhead of repetitive paperwork.

-
- -
-
🏙️
-

SDG 11: Sustainable Cities and Communities

-

Target 11.b: By 2020, substantially increase the number of cities and human settlements adopting and implementing integrated policies and plans towards inclusion, resource efficiency, mitigation and adaptation to climate change, resilience to disasters...

-

How FireForm contributes: FireForm directly enhances a city's resilience to disasters by giving first responders hours of their time back. Instead of doing administrative work, firefighters can focus on training, disaster mitigation, and responding to emergencies, ultimately creating safer and more resilient communities.

-
- -
-
💡
-

SDG 9: Industry, Innovation and Infrastructure

-

Target 9.4: By 2030, upgrade infrastructure and retrofit industries to make them sustainable, with increased resource-use efficiency and greater adoption of clean and environmentally sound technologies and industrial processes...

-

How FireForm contributes: FireForm modernizes the outdated infrastructure of first responder reporting. By leveraging open-source AI locally, it serves as an innovative digital infrastructure that makes emergency management more resource-efficient and environmentally sound (eliminating physical paperwork and reducing digital redundancies).

-
- -
- -
-

Clear Ownership & Accountability

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software, the ownership and accountability of the FireForm project and all its produced assets are clearly defined. -

-

- Accountable Entity: The intellectual property and copyright of the software code and content are owned by the original core creators: Juan Álvarez Sánchez, Manuel Carriedo Garrido, Vincent Harkins, Marc Vergés, and Jan Sans. -

-

- This ownership is publicly documented and verifiable across our project's assets: -

-
    -
  • Software License: Detailed in our public MIT License.
  • -
  • Source Repository: Explicitly stated in our GitHub README page.
  • -
  • Public Website: Documented here on our official project website.
  • -
-
-
- -
-

Platform Independence

-
-

- In accordance with the Digital Public Goods Alliance requirements for Open Software and Open AI Systems, FireForm guarantees platform independence. We do not rely on mandatory proprietary dependencies or closed components that would restrict our MIT License. -

-

- Core Dependencies: -

-
    -
  • Frontend: React, Electron, Node.js. (Declared in frontend/package.json)
  • -
  • Backend: Python, FastAPI, SQLite. (Declared in requirements.txt)
  • -
  • AI System (Optional): Ollama running open-weight models like Mistral. All inference runs locally. Note: The AI features are optional and not core to the main functionality of FireForm (which operates as a digital form and template manager). The AI extraction can be disabled in the application settings. Furthermore, this local Ollama dependency can be swapped with any other LLM service.
  • -
-

- Dependency Graph (SBOM): All components and their versions in our software supply chain are automatically tracked by our source repository. You can view our full dependency graph and SBOM directly on our GitHub repository. If any proprietary components are ever integrated, they will be strictly optional and replaceable with open alternatives without modifying the core solution. -

-
-
- -
-

Mechanism for Extracting Data

-
-

- Digital public goods must ensure that data and content can be extracted or imported in a non-proprietary format. FireForm embraces this requirement at the core of its architecture: -

-
    -
  • Standard Format (JSON): All data extracted by the LLM—whether non-PII or PII—is formatted and exported natively as a non-proprietary JSON object. This makes it instantly compatible with any modern software or data pipeline without vendor lock-in.
  • -
  • Open Storage (SQLite): Metadata, templates, and local configuration are stored using SQLite, an open, serverless database engine. The data is saved locally in a fireform.db file, which can be easily extracted, backed up, and read by hundreds of open-source tools.
  • -
  • API Access: Our local Python FastAPI backend exposes these JSON objects and database entries, allowing automated systems to securely extract the data.
  • -
-

- By standardizing on JSON and SQLite, FireForm guarantees that emergency departments retain full ownership and accessibility of their data at all times. -

-
-
- -
-

Privacy & Applicable Laws

-
-

- FireForm is designed from the ground up with a privacy-first, local-only architecture. Because emergency incident reports frequently contain sensitive Personally Identifiable Information (PII), we guarantee that no data ever leaves the user's device. -

-

- By eliminating cloud servers and third-party APIs, FireForm enables first responder organizations to easily comply with the world's most stringent data protection laws, including: -

-
    -
  • HIPAA (Health Insurance Portability and Accountability Act): Ensures EMS medical data remains entirely offline and secure.
  • -
  • CCPA (California Consumer Privacy Act): Ensures no consumer data is shared or sold.
  • -
  • GDPR (General Data Protection Regulation): Enforces strict data minimization and localizes data subjects' rights.
  • -
-

- For full details on our consent management procedures, data minimization practices, and how the solution was designed to comply with HIPAA, CCPA, and GDPR, please read our official Privacy Policy. -

-
-
- -
-

Do No Harm by Design

-
-

- FireForm is committed to anticipating and preventing harm by design, strictly adhering to the DPGA's framework: -

-
    -
  • 9A. Data Privacy & Security: Because our solution handles sensitive incident data (PII), we mitigate risk by strictly operating offline. Data is never exposed to public networks, avoiding data breaches. (See our Privacy Policy).
  • -
  • 9B. Inappropriate & Illegal Content: FireForm is an enterprise productivity tool for emergency responders, not a social platform. It does not host public user-generated content, completely mitigating the risk of distributing illegal or misleading public content.
  • -
  • 9C. Protection from Harassment: There is no public user-to-user interaction within the app. For our open-source contributor community, we strictly enforce our Code of Conduct to protect against harassment and abuse.
  • -
-
-
- -
-

Standards & Best Practices

-
-

- FireForm strictly aligns with globally recognized standards and best practices curated by the DPGA to ensure high-quality, interoperable, and sustainable software: -

-

Adhered Standards:

-
    -
  • JSON & UTF-8 (Data Interchange): All incident data extracted by our AI system is structured purely as JSON with UTF-8 encoding.
  • -
  • OpenAPI & REST (Data Exchange): Our Python backend is built with FastAPI, which automatically generates interactive OpenAPI specifications and follows RESTful architectural patterns.
  • -
-

Adhered Best Practices:

-
    -
  • Community: We maintain a welcoming environment through our Code of Conduct and clear Contribution Guidelines.
  • -
  • Lifecycle Management: All codebase modifications use Git for Change Management. We track progress with Tagged Releases and strictly adhere to Semantic Versioning.
  • -
  • Interoperability & Architecture: We prioritize Open Standards and File Formats, modularized Programmatic APIs, and strict Dependency Management.
  • -
-
-
- -
-

Public Communications & Mission

-

- Our mission is to build software that protects the people who protect us. FireForm was originally conceived and won 1st place at the Reboot the Earth hackathon, hosted by the UN and UC Santa Cruz, specifically targeting solutions for a better, more sustainable future. -

-

- We believe that modern technology like Local LLMs should be leveraged as public goods, accessible to any department regardless of budget, to help them better serve their communities. -

-
-
-
- -
-
-

FireForm is a Digital Public Good. Licensed under MIT.

-
-
- - diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index d01e95ec..00000000 --- a/docs/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - FireForm - Report Once, File Everywhere - - - - - - - -
-
-
-
- - - -
-
-
-
Reboot the Earth Hackathon Winner
-

Report Once,
File Everywhere.

-

- An open-source, AI-powered system built to solve administrative overhead for first responders. Save hundreds of hours by eliminating redundant paperwork. -

- - -
- -
-

Digital Public Good

-
-
-

FireForm is proudly recognized as a Digital Public Good (DPG). We are committed to advancing the United Nations' Sustainable Development Goals (SDGs).

-
- Learn about our SDG Relevance → -
-
- -
-

Current Features

-
-
-
📝
-

Smart PDF Conversion

-

Automatically turn any non-fillable PDF into a fillable, standardized template.

-
-
-
💾
-

Local Template Database

-

Store your templates in a local DB. Recover past templates and quickly generate new forms.

-
-
-
🎙️
-

Voice or Text Input

-

Provide input via text or voice transcription. A single input drives everything.

-
-
-
🤖
-

Automated LLM Filling

-

Our local LLM extracts relevant information and automatically populates the documents.

-
-
-
- -
-

Future Roadmap

-
-
-
-
-
🌍
-

External Data APIs

-

Connect to APIs to automatically fetch climate, location, and other external data.

-
-
-
-
-
-
📊
-

Insightful Dashboard

-

Visualize your reports with comprehensive graphics and statistics.

-
-
-
-
-
-
🧠
-

ML Field Detection

-

Machine learning models for advanced, automatic form field detection.

-
-
-
-
-
-
📑
-

Advanced PDF Features

-

More powerful PDF manipulation, merging, and management tools.

-
-
-
-
- -
-

About the Project

-
-
-

FireForm was born at a Silicon Valley hackathon organized by the United Nations, University of California, and CalFire.

-

After winning first place, the project was awarded a 6-month mentorship by Salesforce. We are also proud to be part of Google Summer of Code, welcoming new collaborators to scale our impact.

-
- -
-
-
-
- - - - diff --git a/docs/privacy.html b/docs/privacy.html deleted file mode 100644 index 461a3703..00000000 --- a/docs/privacy.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - FireForm - Privacy Policy - - - - - - - -
-
-
-
- - - -
-
-
-

Privacy Policy

-

- FireForm is built on the principle of privacy by design. Our architecture guarantees that your data, including Personally Identifiable Information (PII), never leaves your device. -

-
-
-
- -
-
- -
-

1. Local-First Architecture

-

- FireForm is a completely offline, local-first application. All processing, including voice transcription and AI-based information extraction, is performed on your local hardware using local Large Language Models (LLMs) via Ollama. -

-

- We do not use cloud APIs, we do not have central servers that collect your data, and we do not track telemetry. Therefore, we do not collect, process, or share any PII with third parties. -

-
- -
-

2. Data Collection and Usage

-

- The only data collected by FireForm is the information you explicitly input (voice memos, text, and PDF templates) to generate incident reports. This data is stored locally on your device in a local SQLite database and standard JSON files. You maintain full ownership and control over this data, and can delete it or export it at any time. -

-

- Because all data remains on your device, consent management and data subject requests are fully within the control of the user or the deploying organization (e.g., the fire department). -

-
- -
-

3. Compliance with Applicable Laws

-

- By guaranteeing that data never leaves the host machine, FireForm enables deploying organizations to easily comply with the strictest data protection and privacy laws worldwide. We specifically support compliance with: -

-
    -
  • Health Insurance Portability and Accountability Act (HIPAA): By ensuring EMS and healthcare-related incident data remains entirely offline and secure on the local device.
  • -
  • California Consumer Privacy Act (CCPA) / CPRA: No consumer data is shared or sold. The local architecture defaults to maximum privacy.
  • -
  • General Data Protection Regulation (GDPR): Complete data minimization and localized processing ensures that no unauthorized cross-border data transfers occur, and data subjects' rights are easily managed locally.
  • -
-
- -
-

4. Contact Us

-

- If you have any questions about this Privacy Policy or how FireForm handles data, please contact the core maintainers via our GitHub Repository. -

-
- -
-
- - - - diff --git a/docs/styles.css b/docs/styles.css deleted file mode 100644 index 4f3dae70..00000000 --- a/docs/styles.css +++ /dev/null @@ -1,488 +0,0 @@ -:root { - --bg-dark: #0f0a0a; - --bg-card: rgba(20, 10, 10, 0.6); - --bg-card-hover: rgba(40, 15, 15, 0.8); - --text-primary: #ffffff; - --text-secondary: #ffb8b8; - --accent-red: #ff3b30; - --accent-orange: #ff9500; - --border-color: rgba(255, 59, 48, 0.2); - --glow-color: rgba(255, 59, 48, 0.4); - --font-main: 'Inter', system-ui, -apple-system, sans-serif; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - background-color: var(--bg-dark); - color: var(--text-primary); - font-family: var(--font-main); - line-height: 1.6; - overflow-x: hidden; - min-height: 100vh; - display: flex; - flex-direction: column; -} - -.container { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 0 2rem; -} - -/* Background Effects */ -.background-effects { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: -1; - overflow: hidden; - pointer-events: none; -} - -.glow { - position: absolute; - width: 60vw; - height: 60vw; - border-radius: 50%; - filter: blur(100px); - opacity: 0.15; - animation: pulse 10s ease-in-out infinite alternate; -} - -.red-glow { - top: -20%; - right: -10%; - background: var(--accent-red); -} - -.orange-glow { - bottom: -20%; - left: -10%; - background: var(--accent-orange); - animation-delay: -5s; -} - -@keyframes pulse { - 0% { transform: scale(1) translate(0, 0); opacity: 0.15; } - 100% { transform: scale(1.1) translate(-5%, 5%); opacity: 0.25; } -} - -/* Navigation */ -.navbar { - padding: 1.5rem 0; - border-bottom: 1px solid var(--border-color); - background: rgba(15, 10, 10, 0.8); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - position: sticky; - top: 0; - z-index: 100; -} - -.navbar .container { - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - font-size: 1.5rem; - font-weight: 800; - letter-spacing: -0.5px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.fire-icon { - font-size: 1.8rem; - filter: drop-shadow(0 0 8px var(--accent-orange)); -} - -.github-link { - color: var(--text-primary); - text-decoration: none; - font-weight: 600; - padding: 0.5rem 1rem; - border: 1px solid var(--border-color); - border-radius: 8px; - transition: all 0.3s ease; -} - -.github-link:hover { - background: var(--border-color); - box-shadow: 0 0 15px var(--glow-color); -} - -/* Hero Section */ -.hero { - flex: 1; - display: flex; - align-items: center; - padding: 6rem 0; -} - -.hero-content { - text-align: center; - max-width: 800px; - margin: 0 auto; -} - -.badge { - display: inline-block; - padding: 0.4rem 1rem; - background: rgba(255, 149, 0, 0.1); - border: 1px solid var(--accent-orange); - color: var(--accent-orange); - border-radius: 20px; - font-size: 0.9rem; - font-weight: 600; - margin-bottom: 1.5rem; - box-shadow: 0 0 15px rgba(255, 149, 0, 0.2); -} - -.hero-title { - font-size: 4rem; - font-weight: 800; - line-height: 1.1; - margin-bottom: 1.5rem; - letter-spacing: -1px; -} - -.gradient-text { - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.hero-subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - margin-bottom: 3rem; - font-weight: 300; -} - -/* Download Section */ -.download-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem 2rem; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - box-shadow: 0 20px 40px rgba(0,0,0,0.4), inset 0 0 0 1px rgba(255,255,255,0.05); -} - -.download-heading { - font-size: 1.5rem; - margin-bottom: 2rem; - font-weight: 600; -} - -.button-group { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.btn { - display: flex; - align-items: center; - gap: 1rem; - padding: 1.25rem; - border-radius: 16px; - text-decoration: none; - transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); - background: rgba(25, 15, 15, 0.8); - border: 1px solid var(--border-color); - position: relative; - overflow: hidden; -} - -.btn::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(135deg, rgba(255,59,48,0.1), rgba(255,149,0,0.1)); - opacity: 0; - transition: opacity 0.3s ease; -} - -.btn:hover { - transform: translateY(-5px) scale(1.02); - border-color: var(--accent-orange); - box-shadow: 0 15px 30px rgba(255, 59, 48, 0.2); -} - -.btn:hover::before { - opacity: 1; -} - -.os-icon { - font-size: 2rem; - z-index: 1; -} - -.btn-text { - display: flex; - flex-direction: column; - align-items: flex-start; - z-index: 1; -} - -.os-name { - color: var(--text-primary); - font-weight: 600; - font-size: 1.1rem; -} - -.file-type { - color: var(--text-secondary); - font-size: 0.85rem; -} - -.view-all-link { - color: var(--accent-orange); - text-decoration: none; - font-size: 0.95rem; - font-weight: 600; - transition: color 0.2s ease; -} - -.view-all-link:hover { - color: var(--text-primary); -} - -/* Features Sections */ -.features-section { - margin-top: 5rem; -} - -.section-heading { - text-align: center; - font-size: 2.2rem; - margin-bottom: 2.5rem; - color: var(--text-primary); - font-weight: 800; -} - -.section-heading::after { - content: ''; - display: block; - width: 60px; - height: 4px; - background: linear-gradient(135deg, var(--accent-red), var(--accent-orange)); - margin: 1rem auto 0; - border-radius: 2px; -} - -.features-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 2rem; -} - -/* Timeline Styles */ -.timeline { - position: relative; - max-width: 800px; - margin: 0 auto; - padding: 2rem 0; -} -.timeline::before { - content: ''; - position: absolute; - left: 50%; - top: 0; - bottom: 0; - width: 2px; - background: linear-gradient(to bottom, var(--accent-red), var(--accent-orange)); - transform: translateX(-50%); -} -.timeline-item { - position: relative; - width: 50%; - padding: 1.5rem 3rem; -} -.timeline-item:nth-child(odd) { - left: 0; - text-align: right; -} -.timeline-item:nth-child(even) { - left: 50%; - text-align: left; -} -.timeline-dot { - position: absolute; - top: 50%; - width: 20px; - height: 20px; - background: var(--bg-dark); - border: 4px solid var(--accent-orange); - border-radius: 50%; - transform: translateY(-50%); - box-shadow: 0 0 10px var(--accent-orange); - z-index: 2; -} -.timeline-item:nth-child(odd) .timeline-dot { - right: -10px; -} -.timeline-item:nth-child(even) .timeline-dot { - left: -10px; -} -.timeline-content { - background: rgba(15, 10, 10, 0.4); - border: 1px dashed rgba(255, 149, 0, 0.3); - padding: 1.5rem; - border-radius: 16px; - transition: all 0.3s ease; -} -.timeline-item:hover .timeline-content { - background: rgba(255, 149, 0, 0.05); - border-style: solid; - border-color: var(--accent-orange); - transform: translateY(-5px); -} - -.feature-card { - background: rgba(15, 10, 10, 0.4); - border: 1px solid rgba(255, 255, 255, 0.05); - padding: 2rem; - border-radius: 16px; - text-align: left; - transition: transform 0.3s ease, border-color 0.3s ease; -} - -.feature-card:hover { - transform: translateY(-5px); - border-color: var(--border-color); -} - -.feature-icon { - font-size: 2.5rem; - margin-bottom: 1rem; -} - -.feature-card h3 { - margin-bottom: 0.5rem; - font-size: 1.2rem; -} - -.feature-card p { - color: var(--text-secondary); - font-size: 0.95rem; -} - -/* About Section Styles */ -.about-section { - margin-top: 5rem; - padding-bottom: 3rem; -} -.about-card { - background: linear-gradient(135deg, rgba(20, 10, 10, 0.8), rgba(40, 15, 15, 0.6)); - border: 1px solid var(--border-color); - border-radius: 24px; - padding: 3rem; - display: flex; - flex-direction: column; - gap: 2rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,59,48,0.1); -} -.about-content { - font-size: 1.1rem; - color: var(--text-secondary); -} -.about-content p { - margin-bottom: 1rem; -} -.about-content strong { - color: var(--text-primary); -} -.contact-links { - margin-top: 1rem; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.05); -} -.contact-links h3 { - margin-bottom: 1rem; - font-size: 1.2rem; -} -.social-badges { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} -.badge-link { - display: inline-flex; - align-items: center; - padding: 0.5rem 1rem; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - color: var(--text-primary); - text-decoration: none; - border-radius: 8px; - font-size: 0.95rem; - transition: all 0.2s ease; -} -.badge-link:hover { - background: rgba(255, 59, 48, 0.1); - border-color: var(--accent-red); - transform: translateY(-2px); -} - -/* Footer */ -footer { - text-align: center; - padding: 2rem 0; - border-top: 1px solid rgba(255, 255, 255, 0.05); - color: rgba(255, 255, 255, 0.4); - font-size: 0.9rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .hero-title { - font-size: 2.8rem; - } - - .hero { - padding: 3rem 0; - } - - .download-section { - padding: 2rem 1.5rem; - } - - .timeline::before { - left: 20px; - } - .timeline-item { - width: 100%; - padding-left: 50px; - padding-right: 0; - } - .timeline-item:nth-child(odd), .timeline-item:nth-child(even) { - left: 0; - text-align: left; - } - .timeline-item:nth-child(odd) .timeline-dot, .timeline-item:nth-child(even) .timeline-dot { - left: 10px; - right: auto; - } - .about-card { - padding: 2rem 1.5rem; - } -} diff --git a/docs/terms.html b/docs/terms.html deleted file mode 100644 index 557c1277..00000000 --- a/docs/terms.html +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - FireForm - Terms of Service - - - - - - - -
-
-
-
- - - -
-
-
-

Terms of Service

-

- By downloading, installing, or using FireForm you agree to the following terms. Please read them carefully. If you do not agree, do not use the software. -

-

- Last updated: May 2026 -

-
-
-
- -
-
- -
-

1. Acceptance of Terms

-

- These Terms of Service ("Terms") govern your access to and use of the FireForm software application, source code, documentation, and related materials (collectively, the "Software"). By using the Software you confirm that you are at least 18 years old (or the age of majority in your jurisdiction) and have the legal authority to accept these Terms on behalf of yourself or the organization you represent. -

-
- -
-

2. Open-Source License

-

- FireForm is released under the MIT License. You are free to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software subject to the conditions stated in the LICENSE file included with every distribution and available in our GitHub repository. -

-

- Nothing in these Terms restricts rights granted to you by the MIT License; they are intended solely to clarify responsibilities and limitations not addressed by that license. -

-
- -
-

3. Permitted Use

-

You may use FireForm for any lawful purpose, including:

-
    -
  • Internal use by emergency services, public safety agencies, and fire departments.
  • -
  • Research, academic, and non-commercial projects.
  • -
  • Commercial deployments, provided you comply with the MIT License attribution requirements.
  • -
  • Developing derivative works or integrations, subject to the MIT License terms.
  • -
-
- -
-

4. Prohibited Use

-

You must not use FireForm to:

-
    -
  • Violate any applicable law or regulation, including but not limited to laws governing data protection, privacy, and public safety reporting.
  • -
  • Intentionally generate false, fraudulent, or misleading incident reports.
  • -
  • Infringe the intellectual property rights of any third party.
  • -
  • Introduce malware, exploits, or other harmful code into the Software or its dependencies.
  • -
  • Use the Software in a manner that endangers the health or safety of any person.
  • -
-
- -
-

5. No Warranty

-

- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THE FIREFORM CONTRIBUTORS DO NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE, UNINTERRUPTED, SECURE, OR THAT ANY DEFECTS WILL BE CORRECTED. USE OF THE SOFTWARE FOR CRITICAL PUBLIC-SAFETY OPERATIONS IS ENTIRELY AT YOUR OWN RISK AND YOUR ORGANIZATION'S DISCRETION. -

-
- -
-

6. Limitation of Liability

-

- TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE FIREFORM CONTRIBUTORS, MAINTAINERS, SPONSORS, OR AFFILIATED ORGANIZATIONS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES — INCLUDING LOSS OF DATA, REVENUE, GOODWILL, OR LIFE — ARISING OUT OF OR IN CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN JURISDICTIONS THAT DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES OR LIMITATIONS OF LIABILITY, OUR LIABILITY IS LIMITED TO THE FULLEST EXTENT PERMITTED BY LAW. -

-
- -
-

7. User-Generated Content and Data

-

- FireForm processes data exclusively on your local device. You retain full ownership of all incident reports, templates, voice recordings, and any other data you create or import. We do not access, store, or transmit your data. -

-

- You are solely responsible for the accuracy and legality of the data you enter into FireForm and for complying with any reporting obligations imposed by your jurisdiction or regulatory authority. -

-
- -
-

8. Third-Party Components

-

- FireForm integrates with third-party software, including Ollama for local LLM inference and Whisper for voice transcription. Your use of those components is governed by their respective licenses and terms. We make no representations or warranties regarding third-party software and are not responsible for any issues arising from their use. -

-
- -
-

9. Contributions

-

- Contributions to FireForm (pull requests, issues, documentation, etc.) are subject to our Contributing Guidelines and Code of Conduct. By submitting a contribution you confirm that you have the right to license it under the MIT License and that you grant the FireForm project a perpetual, worldwide, royalty-free license to use, reproduce, and distribute your contribution. -

-
- -
-

10. Changes to These Terms

-

- We may update these Terms from time to time. When we do, we will revise the "Last updated" date at the top of this page and, for material changes, post a notice in our GitHub repository. Continued use of FireForm after changes are posted constitutes your acceptance of the revised Terms. -

-
- -
-

11. Contact

-

- If you have questions about these Terms, please reach out to the core maintainers via our GitHub Repository. We welcome feedback from deploying organizations and community members. -

-
- -
-
- - - - diff --git a/examples/pgeocode_demo.py b/examples/pgeocode_demo.py new file mode 100644 index 00000000..10201a8c --- /dev/null +++ b/examples/pgeocode_demo.py @@ -0,0 +1,5 @@ +import pgeocode + +nomi = pgeocode.Nominatim('us') +response_pgeo = nomi.query_location("Santa Cruz", top_k=3) +print(response_pgeo) \ No newline at end of file diff --git a/examples/pipeline_demo.py b/examples/pipeline_demo.py new file mode 100644 index 00000000..dbad305c --- /dev/null +++ b/examples/pipeline_demo.py @@ -0,0 +1,27 @@ +"""Manual, end-to-end demo of the fill pipeline (not part of the package). + +Run from the repo root with a real PDF path. This is a developer convenience +for exercising the services layer without the API; it is not imported anywhere. +""" + +from commonforms import prepare_form +from pypdf import PdfReader + +from app.services.controller import Controller + +if __name__ == "__main__": + file = "./data/inputs/file.pdf" # update to a real input PDF + user_input = ( + "Hi. The employee's name is John Doe. His job title is managing director. " + "His department supervisor is Jane Doe. His phone number is 123456. " + "His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" + ) + prepared_pdf = "temp_outfile.pdf" + prepare_form(file, prepared_pdf) + + reader = PdfReader(prepared_pdf) + fields = reader.get_fields() + num_fields = len(fields) if fields else 0 + + controller = Controller() + controller.fill_form(user_input, fields, file) diff --git a/frontend/app.js b/frontend/app.js deleted file mode 100644 index 4fd15ad5..00000000 --- a/frontend/app.js +++ /dev/null @@ -1,1362 +0,0 @@ -const STORAGE_TEMPLATES_KEY = "fireform.templates.v1"; -const STORAGE_LAST_OUTPUT_KEY = "fireform.lastOutputPath.v1"; -// Where uploaded template PDFs are copied. Fixed for now; longer term this -// should be user-configurable behind a Settings button (see note below). -const DEFAULT_TEMPLATE_DIRECTORY = "src/inputs"; -const API_BASE_URL = "http://127.0.0.1:8000"; - -// UI label <-> stored type-string mapping. The stored values stay backward -// compatible with the existing default "string" type. -const FIELD_TYPES = [ - { label: "Text", value: "string" }, - { label: "Long Text", value: "long_text" }, - { label: "Number", value: "number" }, - { label: "Date", value: "date" }, - { label: "Time", value: "time" }, - { label: "Email", value: "email" }, - { label: "Phone", value: "phone" }, - { label: "Signature", value: "signature" }, - { label: "Checkbox", value: "checkbox" }, - { label: "List", value: "list" }, -]; -const TYPE_VALUE_TO_LABEL = Object.fromEntries(FIELD_TYPES.map((t) => [t.value, t.label])); -const DEFAULT_FIELD_ROWS = [{ name: "", type: "string" }]; - -const elements = { - tabs: Array.from(document.querySelectorAll(".tab")), - panels: Array.from(document.querySelectorAll(".panel")), - templateForm: document.getElementById("templateForm"), - templateName: document.getElementById("templateName"), - templatePdfFile: document.getElementById("templatePdfFile"), - pdfDropZone: document.getElementById("pdfDropZone"), - selectedFileMeta: document.getElementById("selectedFileMeta"), - changePdfBtn: document.getElementById("changePdfBtn"), - makeFillableBtn: document.getElementById("makeFillableBtn"), - makeFillableHelpBtn: document.getElementById("makeFillableHelpBtn"), - makeFillableHelp: document.getElementById("makeFillableHelp"), - fieldsBuilder: document.getElementById("fieldsBuilder"), - fieldCountBadge: document.getElementById("fieldCountBadge"), - addFieldBtn: document.getElementById("addFieldBtn"), - templateFormMessage: document.getElementById("templateFormMessage"), - templateFormResponse: document.getElementById("templateFormResponse"), - fillForm: document.getElementById("fillForm"), - fillModel: document.getElementById("fillModel"), - fillTemplateTiles: document.getElementById("fillTemplateTiles"), - fillSelectionHint: document.getElementById("fillSelectionHint"), - fillSubmitBtn: document.getElementById("fillSubmitBtn"), - inputText: document.getElementById("inputText"), - sttControls: document.getElementById("sttControls"), - sttRecordBtn: document.getElementById("sttRecordBtn"), - sttPauseBtn: document.getElementById("sttPauseBtn"), - sttStopBtn: document.getElementById("sttStopBtn"), - sttStatus: document.getElementById("sttStatus"), - fillFormMessage: document.getElementById("fillFormMessage"), - fillFormResponse: document.getElementById("fillFormResponse"), - templatesEmpty: document.getElementById("templatesEmpty"), - templatesList: document.getElementById("templatesList"), - localPdfFile: document.getElementById("localPdfFile"), - serverPdfPath: document.getElementById("serverPdfPath"), - previewPathBtn: document.getElementById("previewPathBtn"), - previewStatus: document.getElementById("previewStatus"), - pdfFrame: document.getElementById("pdfFrame"), -}; - -let templates = loadTemplates(); -let activeObjectUrl = null; -let selectedTemplateFile = null; -// Field rows are scratch state for building one template — they start empty -// each session and are not persisted. -let fieldRows = DEFAULT_FIELD_ROWS.map((row) => ({ ...row })); -let dragSourceIndex = null; -let uploadedPath = null; -let uploadedFieldCount = null; -// Template ids currently selected in the Fill Form tab (multi-select). -let selectedFillIds = new Set(); - -// Speech-to-text recording state. The MediaRecorder captures compressed audio -// in the renderer; on stop we POST it straight to /forms/transcribe (the local -// Whisper service handles decoding). -let mediaRecorder = null; -let recordedChunks = []; -let recordingStream = null; - -waitForBackend().then(initialize); - -async function waitForBackend() { - const loadingScreen = document.getElementById("loadingScreen"); - let isReady = false; - - while (!isReady) { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - if (response.ok) { - isReady = true; - } - } catch (e) { - // Ignore error and try again - } - - if (!isReady) { - await new Promise(r => setTimeout(r, 500)); - } - } - - if (loadingScreen) { - loadingScreen.classList.add("hidden"); - } -} - -async function initialize() { - bindEvents(); - renderFieldRows(); - renderTemplates(); - renderFillTemplates(); - restorePreviewState(); - updateSelectedFileMeta(); - loadModels(); - await refreshTemplatesFromApi(); -} - -function bindEvents() { - elements.tabs.forEach((tab) => { - tab.addEventListener("click", () => activateSection(tab.dataset.target)); - }); - - elements.templateForm.addEventListener("submit", handleTemplateSubmit); - elements.templatePdfFile.addEventListener("change", handleTemplateFileInput); - elements.pdfDropZone.addEventListener("click", () => elements.templatePdfFile.click()); - elements.pdfDropZone.addEventListener("keydown", handleDropZoneKeyDown); - elements.changePdfBtn.addEventListener("click", () => elements.templatePdfFile.click()); - elements.addFieldBtn.addEventListener("click", handleAddFieldClick); - elements.makeFillableBtn.addEventListener("click", handleMakeFillableClick); - elements.makeFillableHelpBtn.addEventListener("click", toggleMakeFillableHelp); - bindDropZoneDragEvents(); - elements.fillForm.addEventListener("submit", handleFillSubmit); - elements.fillTemplateTiles.addEventListener("click", handleTileClick); - elements.fillTemplateTiles.addEventListener("keydown", handleTileKeydown); - elements.sttRecordBtn.addEventListener("click", startRecording); - elements.sttPauseBtn.addEventListener("click", togglePauseRecording); - elements.sttStopBtn.addEventListener("click", stopRecording); - elements.templatesList.addEventListener("click", handleTemplateActionClick); - elements.localPdfFile.addEventListener("change", handleLocalFilePreview); - elements.previewPathBtn.addEventListener("click", () => - previewFromPath(elements.serverPdfPath.value, { switchToPreview: true }) - ); -} - -function activateSection(targetId) { - switchSection(targetId); -} - -async function refreshTemplatesFromApi() { - try { - const response = await fetch(`${API_BASE_URL}/templates`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - if (Array.isArray(body)) { - templates = body.map((template) => ({ - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - })); - saveTemplates(); - // Drop selections for templates that no longer exist. - const liveIds = new Set(templates.map((t) => Number(t.id))); - selectedFillIds.forEach((id) => { if (!liveIds.has(id)) selectedFillIds.delete(id); }); - renderTemplates(); - renderFillTemplates(); - } - } catch (error) { - setStatus( - elements.templateFormMessage, - `Could not refresh templates from API: ${error.message}`, - "error" - ); - } -} - -function bindDropZoneDragEvents() { - ["dragenter", "dragover"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.add("active"); - }); - }); - - ["dragleave", "dragend", "drop"].forEach((eventName) => { - elements.pdfDropZone.addEventListener(eventName, (event) => { - event.preventDefault(); - event.stopPropagation(); - elements.pdfDropZone.classList.remove("active"); - }); - }); - - elements.pdfDropZone.addEventListener("drop", (event) => { - const file = event.dataTransfer?.files?.[0]; - setSelectedTemplateFile(file); - }); -} - -function handleDropZoneKeyDown(event) { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - elements.templatePdfFile.click(); - } -} - -function handleTemplateFileInput(event) { - const file = event.target.files && event.target.files[0]; - setSelectedTemplateFile(file); -} - -function setSelectedTemplateFile(file) { - if (!file) { - return; - } - - if (!isPdfFile(file)) { - selectedTemplateFile = null; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - setStatus(elements.templateFormMessage, "Please select a PDF file.", "error"); - updateSelectedFileMeta(); - return; - } - - selectedTemplateFile = file; - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - updateSelectedFileMeta(); - // Eager upload so the user gets a live field-count comparison while building rows. - uploadSelectedFileSilently(); -} - -async function uploadSelectedFileSilently() { - if (!selectedTemplateFile) return; - const directory = DEFAULT_TEMPLATE_DIRECTORY; - - const fileAtUploadStart = selectedTemplateFile; - try { - const upload = await uploadTemplatePdf(fileAtUploadStart, directory); - // Guard against the user picking a different file mid-upload. - if (fileAtUploadStart !== selectedTemplateFile) return; - uploadedPath = upload.pdf_path; - uploadedFieldCount = - typeof upload.field_count === "number" ? upload.field_count : null; - maybeSeedFieldRows(upload.fields); - renderFieldCountBadge(); - } catch (_error) { - // Silent failure — the explicit Create / Make Fillable paths surface errors. - } -} - -// Auto-add a row per field the PDF already defines — same as clicking "+ Add -// Field" for each — filling in its description and type so the user can edit. -// If the list already has rows the user typed, warn before replacing them. -function maybeSeedFieldRows(fields) { - if (!Array.isArray(fields) || !fields.length) return; - syncFieldRowsFromDom(); - - if (fieldRows.some((row) => row.name.trim())) { - const replace = window.confirm( - `This PDF has ${fields.length} fillable field${fields.length === 1 ? "" : "s"}.\n\n` + - "Replace your current form fields with them? Your existing entries will be lost." - ); - if (!replace) { - setStatus(elements.templateFormMessage, "Kept your existing form fields.", "info"); - return; - } - } - - fieldRows = fields.map((f) => ({ - name: f.description || f.name || "", - type: normalizeFieldType(f.type), - })); - renderFieldRows(); - setStatus( - elements.templateFormMessage, - `Loaded ${fieldRows.length} field${fieldRows.length === 1 ? "" : "s"} from the PDF — edit the descriptions as needed.`, - "info" - ); -} - -function setMakeFillableButtonState() { - if (!elements.makeFillableBtn) return; - elements.makeFillableBtn.disabled = !selectedTemplateFile; - elements.makeFillableBtn.textContent = "Make this PDF fillable"; -} - -function renderFieldCountBadge() { - const badge = elements.fieldCountBadge; - if (!badge) return; - - if (!selectedTemplateFile || uploadedFieldCount === null) { - badge.classList.add("hidden"); - badge.classList.remove("match", "mismatch"); - badge.textContent = ""; - return; - } - - const expected = uploadedFieldCount; - const actual = fieldRows.length; - const noun = (n) => `${n} fillable field${n === 1 ? "" : "s"}`; - const rowNoun = (n) => `${n} row${n === 1 ? "" : "s"}`; - - badge.classList.remove("hidden", "match", "mismatch"); - if (expected === actual) { - badge.classList.add("match"); - badge.textContent = `PDF has ${noun(expected)} — your ${rowNoun(actual)} match.`; - } else { - badge.classList.add("mismatch"); - badge.textContent = `PDF has ${noun(expected)} — you have ${rowNoun(actual)}.`; - } -} - -function isPdfFile(file) { - const name = String(file?.name || "").toLowerCase(); - return name.endsWith(".pdf"); -} - -function updateSelectedFileMeta() { - // Once a file is chosen, swap the drop zone for a compact "change" control. - const hasFile = !!selectedTemplateFile; - elements.pdfDropZone.classList.toggle("hidden", hasFile); - elements.changePdfBtn.classList.toggle("hidden", !hasFile); - - if (!hasFile) { - elements.selectedFileMeta.textContent = "No PDF selected."; - return; - } - - const destinationPath = `${DEFAULT_TEMPLATE_DIRECTORY}/${selectedTemplateFile.name}`; - - elements.selectedFileMeta.textContent = `Selected: ${selectedTemplateFile.name} (${formatBytes( - selectedTemplateFile.size - )}) - destination: ${destinationPath}`; -} - -function formatBytes(bytes) { - if (!Number.isFinite(bytes) || bytes <= 0) { - return "0 B"; - } - - const units = ["B", "KB", "MB", "GB"]; - let value = bytes; - let unitIndex = 0; - - while (value >= 1024 && unitIndex < units.length - 1) { - value /= 1024; - unitIndex += 1; - } - - return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; -} - -function switchSection(targetId) { - elements.panels.forEach((panel) => { - panel.classList.toggle("hidden", panel.id !== targetId); - }); - elements.tabs.forEach((tab) => { - tab.classList.toggle("active", tab.dataset.target === targetId); - }); -} - -function setStatus(target, message, type = "info") { - target.textContent = message || ""; - target.className = "status"; - if (type) { - target.classList.add(type); - } -} - -function showJson(preElement, payload) { - preElement.textContent = JSON.stringify(payload, null, 2); - preElement.classList.remove("hidden"); -} - -function clearJson(preElement) { - preElement.textContent = ""; - preElement.classList.add("hidden"); -} - -function collectFieldRows() { - syncFieldRowsFromDom(); - - if (fieldRows.length === 0) { - return { error: "Add at least one field before creating the template." }; - } - - const dict = {}; - const seen = new Set(); - for (const row of fieldRows) { - const name = row.name.trim(); - if (!name) { - return { error: "Every field needs a name." }; - } - const key = name.toLowerCase(); - if (seen.has(key)) { - return { error: `Field names must be unique ("${name}" appears more than once).` }; - } - seen.add(key); - dict[name] = row.type || "string"; - } - return { value: dict }; -} - -async function handleTemplateSubmit(event) { - event.preventDefault(); - clearJson(elements.templateFormResponse); - setStatus(elements.templateFormMessage, ""); - - const name = elements.templateName.value.trim(); - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - const collected = collectFieldRows(); - - if (!name || !selectedTemplateFile) { - setStatus( - elements.templateFormMessage, - "Name and PDF file are required.", - "error" - ); - return; - } - - if (collected.error) { - setStatus(elements.templateFormMessage, collected.error, "error"); - return; - } - - try { - let activePdfPath = uploadedPath; - if (!activePdfPath) { - setStatus(elements.templateFormMessage, "Copying PDF into project directory...", "info"); - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - activePdfPath = upload.pdf_path; - uploadedPath = upload.pdf_path; - } - - const payload = { - name, - pdf_path: activePdfPath, - fields: collected.value, - }; - - setStatus(elements.templateFormMessage, "Creating template...", "info"); - const response = await fetch(`${API_BASE_URL}/templates/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - upsertTemplate(body); - if (body.id != null) { - selectedFillIds.add(Number(body.id)); - } - await refreshTemplatesFromApi(); - elements.serverPdfPath.value = body.pdf_path || ""; - - const expected = body.field_count; - const actual = Object.keys(collected.value).length; - let mismatchNote = ""; - let statusLevel = "success"; - if (typeof expected === "number" && expected !== actual) { - mismatchNote = ` Heads up — the PDF has ${expected} fillable field${expected === 1 ? "" : "s"}, but you added ${actual} row${actual === 1 ? "" : "s"}. Fills may be incomplete or misaligned.`; - statusLevel = "error"; - } - - setStatus( - elements.templateFormMessage, - `Template created (id: ${body.id}). PDF saved at ${activePdfPath}.${mismatchNote}`, - statusLevel - ); - showJson(elements.templateFormResponse, body); - uploadedPath = null; - uploadedFieldCount = null; - setMakeFillableButtonState(); - renderFieldCountBadge(); - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - } -} - -async function uploadTemplatePdf(file, directory) { - const formData = new FormData(); - formData.append("file", file, file.name); - formData.append("directory", directory); - - const response = await fetch(`${API_BASE_URL}/templates/upload`, { - method: "POST", - body: formData, - }); - - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - return body; -} - -// ───────────────────────── Fill Form: model + template tiles ────────────── - -// "1 field" / "3 forms" — keeps the count-and-label logic in one place. -function pluralize(count, noun) { - return `${count} ${noun}${count === 1 ? "" : "s"}`; -} - -// Look up a template by id (ids may arrive as strings from dataset attributes). -function findTemplate(id) { - return templates.find((template) => Number(template.id) === Number(id)); -} - -// Populate the model picker from the local Ollama models the API reports. -async function loadModels() { - const select = elements.fillModel; - try { - const response = await fetch(`${API_BASE_URL}/forms/models`); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - select.innerHTML = ""; - const models = body.models || []; - models.forEach((name) => { - const isDefault = name === body.default; - const option = document.createElement("option"); - option.value = name; - option.textContent = isDefault ? `${name} (default)` : name; - option.selected = isDefault; - select.append(option); - }); - } catch (_error) { - // Ollama unreachable — leave one placeholder so the picker isn't empty. - if (!select.options.length) { - const option = document.createElement("option"); - option.value = ""; - option.textContent = "(default model)"; - select.append(option); - } - } -} - -// Build one selectable tile. Whether it's selected is shown purely through the -// tile's highlighted styling (.selected) — there's no separate checkbox. -function createTemplateTile(template) { - const id = Number(template.id); - const selected = selectedFillIds.has(id); - - const tile = document.createElement("div"); - tile.className = selected ? "template-tile selected" : "template-tile"; - tile.dataset.templateId = String(id); - // Behaves like a toggle button for keyboard and screen-reader users. - tile.setAttribute("role", "button"); - tile.setAttribute("tabindex", "0"); - tile.setAttribute("aria-pressed", String(selected)); - - const title = document.createElement("span"); - title.className = "tile-title"; - title.textContent = template.name || "Untitled"; - - const fieldCount = template.fields ? Object.keys(template.fields).length : 0; - const meta = document.createElement("span"); - meta.className = "tile-meta"; - meta.textContent = pluralize(fieldCount, "field"); - - const body = document.createElement("div"); - body.className = "tile-body"; - body.append(title, meta); - - // Preview must not toggle selection, so it carries its own id and the click - // handler stops the event from bubbling up to the tile. - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.className = "tile-preview-btn"; - previewButton.dataset.previewId = String(id); - previewButton.textContent = "Preview"; - - tile.append(body, previewButton); - return tile; -} - -function renderFillTemplates() { - const container = elements.fillTemplateTiles; - container.innerHTML = ""; - - if (!templates.length) { - const empty = document.createElement("p"); - empty.className = "empty-state"; - empty.textContent = "No templates yet — create one in the Create Template tab."; - container.append(empty); - updateFillButtonState(); - return; - } - - templates.forEach((template) => container.append(createTemplateTile(template))); - updateFillButtonState(); -} - -function handleTileClick(event) { - // A click on the Preview button previews the PDF without toggling selection. - const previewButton = event.target.closest(".tile-preview-btn"); - if (previewButton) { - event.stopPropagation(); - const template = findTemplate(previewButton.dataset.previewId); - if (template) { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - } - return; - } - - // A click anywhere else on the tile toggles it on/off for filling. - const tile = event.target.closest(".template-tile"); - if (tile) { - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function handleTileKeydown(event) { - // Enter/Space activate the focused tile, matching its role="button". - if (event.key !== "Enter" && event.key !== " ") { - return; - } - const tile = event.target.closest(".template-tile"); - if (tile) { - event.preventDefault(); - toggleFillSelection(Number(tile.dataset.templateId)); - } -} - -function toggleFillSelection(id) { - if (selectedFillIds.has(id)) { - selectedFillIds.delete(id); - } else { - selectedFillIds.add(id); - } - renderFillTemplates(); -} - -function updateFillButtonState() { - const count = selectedFillIds.size; - const nothingSelected = count === 0; - - // Greyed out (but still clickable) until at least one form is chosen. - elements.fillSubmitBtn.classList.toggle("is-disabled", nothingSelected); - elements.fillSubmitBtn.textContent = count > 1 ? `Fill ${count} Forms` : "Fill Form"; - - elements.fillSelectionHint.classList.remove("error"); - elements.fillSelectionHint.textContent = nothingSelected - ? "Select one or more forms to fill." - : `${pluralize(count, "form")} selected.`; -} - -// A human-readable label for a template, used in the success/error summary. -function templateLabel(id) { - const template = findTemplate(id); - return template && template.name ? template.name : `id ${id}`; -} - -// Fill a single template and return its submission. Throws on failure so the -// caller can note which form failed and still continue with the others. -async function fillOneTemplate(id, inputText, model) { - const response = await fetch(`${API_BASE_URL}/forms/fill`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ template_id: id, input_text: inputText, model }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return body; -} - -// Summarize "N filled, M failed" into the status line, choosing the right tone: -// all-good = success, some failed but some worked = info, nothing worked = error. -function reportFillOutcome(results, errors) { - const parts = []; - if (results.length) parts.push(`${results.length} filled`); - if (errors.length) parts.push(`${errors.length} failed`); - - let level = "success"; - if (errors.length) { - level = results.length ? "info" : "error"; - } - - const detail = errors.length ? ` ${errors.join("; ")}` : ""; - setStatus(elements.fillFormMessage, `${parts.join(", ")}.${detail}`, level); -} - -async function handleFillSubmit(event) { - event.preventDefault(); - clearJson(elements.fillFormResponse); - setStatus(elements.fillFormMessage, ""); - - const ids = Array.from(selectedFillIds); - if (!ids.length) { - // The button looks disabled but stays clickable, so prompt the user here. - elements.fillSelectionHint.classList.add("error"); - elements.fillSelectionHint.textContent = "Select at least one form to fill."; - setStatus(elements.fillFormMessage, "Select at least one form to fill.", "error"); - return; - } - - const inputText = elements.inputText.value.trim(); - if (!inputText) { - setStatus(elements.fillFormMessage, "Input text is required.", "error"); - return; - } - - // An empty picker value means "let the server use its default model". - const model = elements.fillModel.value || undefined; - setStatus(elements.fillFormMessage, `Filling ${pluralize(ids.length, "form")}…`, "info"); - - // Fill each selected form independently so one failure doesn't stop the rest. - const results = []; - const errors = []; - for (const id of ids) { - try { - results.push(await fillOneTemplate(id, inputText, model)); - } catch (error) { - errors.push(`${templateLabel(id)}: ${error.message}`); - } - } - - const lastResult = results[results.length - 1]; - if (lastResult) { - showJson(elements.fillFormResponse, results.length === 1 ? lastResult : results); - if (lastResult.output_pdf_path) { - localStorage.setItem(STORAGE_LAST_OUTPUT_KEY, lastResult.output_pdf_path); - elements.serverPdfPath.value = lastResult.output_pdf_path; - } - } - - reportFillOutcome(results, errors); - - // Preview the most recently filled PDF. - if (lastResult && lastResult.output_pdf_path) { - await previewFromPath(lastResult.output_pdf_path, { switchToPreview: true }); - } -} - -// ───────────────────────── Speech-to-text (local Whisper) ───────────────── - -function setSttStatus(message) { - if (elements.sttStatus) { - elements.sttStatus.textContent = message || ""; - } -} - -async function startRecording() { - if (mediaRecorder) { - return; - } - if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { - setSttStatus("Microphone capture is not available in this environment."); - return; - } - - try { - recordingStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - } catch (error) { - setSttStatus("Microphone permission denied."); - return; - } - - recordedChunks = []; - mediaRecorder = new MediaRecorder(recordingStream); - mediaRecorder.addEventListener("dataavailable", (event) => { - if (event.data && event.data.size > 0) { - recordedChunks.push(event.data); - } - }); - mediaRecorder.addEventListener("stop", handleRecordingStop); - mediaRecorder.start(); - - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttRecordBtn.disabled = true; - elements.sttPauseBtn.disabled = false; - elements.sttStopBtn.disabled = false; - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); -} - -function togglePauseRecording() { - if (!mediaRecorder) { - return; - } - if (mediaRecorder.state === "recording") { - mediaRecorder.pause(); - elements.sttControls.classList.add("is-paused"); - elements.sttControls.classList.remove("is-recording"); - elements.sttPauseBtn.textContent = "Resume"; - setSttStatus("Paused."); - } else if (mediaRecorder.state === "paused") { - mediaRecorder.resume(); - elements.sttControls.classList.add("is-recording"); - elements.sttControls.classList.remove("is-paused"); - elements.sttPauseBtn.textContent = "Pause"; - setSttStatus("Recording…"); - } -} - -function stopRecording() { - if (!mediaRecorder) { - return; - } - // Lock the controls while we finalize capture and transcribe. - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - setSttStatus("Finishing capture…"); - mediaRecorder.stop(); -} - -async function handleRecordingStop() { - elements.sttControls.classList.remove("is-recording", "is-paused"); - stopRecordingStream(); - - const chunks = recordedChunks; - const recorder = mediaRecorder; - recordedChunks = []; - mediaRecorder = null; - - const blob = new Blob(chunks, { type: (recorder && recorder.mimeType) || "audio/webm" }); - if (!blob.size) { - resetSttControls(); - setSttStatus("Nothing was recorded."); - return; - } - - try { - setSttStatus("Transcribing…"); - const text = await transcribeAudio(blob); - appendTranscribedText(text); - setSttStatus(text ? "Transcription added." : "No speech detected."); - } catch (error) { - setSttStatus(`Transcription failed: ${error.message}`); - } finally { - resetSttControls(); - } -} - -function resetSttControls() { - elements.sttRecordBtn.disabled = false; - elements.sttPauseBtn.disabled = true; - elements.sttStopBtn.disabled = true; - elements.sttPauseBtn.textContent = "Pause"; - elements.sttControls.classList.remove("is-recording", "is-paused"); -} - -function stopRecordingStream() { - if (recordingStream) { - recordingStream.getTracks().forEach((track) => track.stop()); - recordingStream = null; - } -} - -function appendTranscribedText(text) { - if (!text) { - return; - } - const existing = elements.inputText.value.trim(); - elements.inputText.value = existing ? `${existing} ${text}` : text; - // Let any listeners (and the required-field check) see the new value. - elements.inputText.dispatchEvent(new Event("input")); -} - -// "audio/webm;codecs=opus" -> "webm". Just gives the upload a sensible filename; -// the server decodes by content, not extension. -function audioExtension(mimeType) { - const subtype = (mimeType || "").split("/")[1] || ""; - const withoutCodecs = subtype.split(";")[0].trim(); - return withoutCodecs || "webm"; -} - -// The Whisper ASR service decodes audio with ffmpeg, so we post the recording -// as-is (typically webm/opus) — no client-side transcoding needed. -async function transcribeAudio(blob) { - const formData = new FormData(); - formData.append("audio", blob, `recording.${audioExtension(blob.type)}`); - - const response = await fetch(`${API_BASE_URL}/forms/transcribe`, { - method: "POST", - body: formData, - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - return (body.text || "").trim(); -} - -function handleTemplateActionClick(event) { - const button = event.target.closest("button[data-action]"); - if (!button) { - return; - } - - const id = Number(button.dataset.templateId); - const template = templates.find((item) => Number(item.id) === id); - if (!template) { - return; - } - - if (button.dataset.action === "preview") { - elements.serverPdfPath.value = template.pdf_path || ""; - previewFromPath(template.pdf_path || "", { switchToPreview: true }); - return; - } - - if (button.dataset.action === "use-fill") { - selectedFillIds.add(Number(template.id)); - renderFillTemplates(); - activateSection("fillFormSection"); - setStatus( - elements.fillFormMessage, - `"${template.name || "Template"}" selected for filling.`, - "info" - ); - } -} - -function handleLocalFilePreview(event) { - const file = event.target.files && event.target.files[0]; - if (!file) { - return; - } - - if (activeObjectUrl) { - URL.revokeObjectURL(activeObjectUrl); - } - - activeObjectUrl = URL.createObjectURL(file); - elements.pdfFrame.src = activeObjectUrl; - switchSection("pdfPreviewerSection"); - setStatus(elements.previewStatus, `Previewing local file: ${file.name}`, "success"); -} - -function resolvePreviewCandidates(pathInput) { - const raw = String(pathInput || "").trim(); - if (!raw) { - return []; - } - - if (/^https?:\/\//i.test(raw)) { - return [raw]; - } - - return [`${API_BASE_URL}/templates/preview?path=${encodeURIComponent(raw)}`]; -} - -async function previewFromPath(pathInput, options = {}) { - if (options.switchToPreview) { - switchSection("pdfPreviewerSection"); - } - - const raw = String(pathInput || "").trim(); - if (!raw) { - setStatus(elements.previewStatus, "Enter a PDF path or URL first.", "error"); - return false; - } - - const candidates = resolvePreviewCandidates(raw); - if (!candidates.length) { - setStatus(elements.previewStatus, "Unable to parse preview path.", "error"); - return false; - } - - setStatus(elements.previewStatus, "Attempting to preview path...", "info"); - let lastReason = "unknown error"; - - for (const candidate of candidates) { - try { - const response = await fetch(candidate, { method: "HEAD" }); - if (response.ok || response.status === 405) { - elements.pdfFrame.src = candidate; - setStatus(elements.previewStatus, `Previewing path: ${candidate}`, "success"); - return true; - } - lastReason = `${response.status} ${response.statusText}`.trim(); - } catch (error) { - lastReason = error.message; - } - } - - const likelyServerLocal = - !/^https?:\/\//i.test(raw) && !raw.startsWith("/"); - - if (likelyServerLocal) { - setStatus( - elements.previewStatus, - `Could not preview "${raw}". It looks like a server-local path and may not be web-accessible.`, - "error" - ); - } else { - setStatus( - elements.previewStatus, - `Could not preview path. Last error: ${lastReason}`, - "error" - ); - } - - return false; -} - -function renderTemplates() { - elements.templatesList.innerHTML = ""; - - if (!templates.length) { - elements.templatesEmpty.classList.remove("hidden"); - return; - } - - elements.templatesEmpty.classList.add("hidden"); - templates.forEach((template) => { - const card = document.createElement("article"); - card.className = "template-card"; - - const title = document.createElement("h3"); - title.textContent = `${template.name || "Untitled"} (id: ${template.id ?? "n/a"})`; - - const path = document.createElement("p"); - path.className = "template-meta"; - path.textContent = `pdf_path: ${template.pdf_path || ""}`; - - const fields = buildFieldsTable(template.fields || {}); - - const actions = document.createElement("div"); - actions.className = "card-actions"; - - const previewButton = document.createElement("button"); - previewButton.type = "button"; - previewButton.dataset.action = "preview"; - previewButton.dataset.templateId = String(template.id); - previewButton.textContent = "Preview This Template"; - - const useFillButton = document.createElement("button"); - useFillButton.type = "button"; - useFillButton.dataset.action = "use-fill"; - useFillButton.dataset.templateId = String(template.id); - useFillButton.textContent = "Use in Fill Form"; - - actions.append(previewButton, useFillButton); - card.append(title, path, fields, actions); - elements.templatesList.append(card); - }); -} - -function buildFieldsTable(fieldsDict) { - const table = document.createElement("table"); - table.className = "fields-table"; - - const thead = document.createElement("thead"); - thead.innerHTML = "FieldType"; - table.appendChild(thead); - - const tbody = document.createElement("tbody"); - const entries = Object.entries(fieldsDict || {}); - if (!entries.length) { - const row = document.createElement("tr"); - const cell = document.createElement("td"); - cell.colSpan = 2; - cell.textContent = "No fields."; - row.appendChild(cell); - tbody.appendChild(row); - } else { - for (const [name, type] of entries) { - const row = document.createElement("tr"); - const nameCell = document.createElement("td"); - nameCell.textContent = name; - const typeCell = document.createElement("td"); - typeCell.textContent = TYPE_VALUE_TO_LABEL[type] || "Text"; - row.append(nameCell, typeCell); - tbody.appendChild(row); - } - } - table.appendChild(tbody); - return table; -} - -function normalizeFieldType(value) { - return TYPE_VALUE_TO_LABEL[value] ? value : "string"; -} - -function syncFieldRowsFromDom() { - const rowEls = Array.from(elements.fieldsBuilder.querySelectorAll(".field-row")); - fieldRows = rowEls.map((rowEl) => ({ - name: rowEl.querySelector(".field-name").value, - type: rowEl.querySelector(".field-type").value, - })); -} - -function renderFieldRows() { - const fragment = document.createDocumentFragment(); - fieldRows.forEach((row, index) => { - fragment.appendChild(buildFieldRow(row, index)); - }); - elements.fieldsBuilder.innerHTML = ""; - elements.fieldsBuilder.appendChild(fragment); - renderFieldCountBadge(); -} - -function buildFieldRow(row, index) { - const rowEl = document.createElement("div"); - rowEl.className = "field-row"; - rowEl.draggable = true; - rowEl.dataset.index = String(index); - - const handle = document.createElement("span"); - handle.className = "field-drag-handle"; - handle.setAttribute("aria-hidden", "true"); - handle.textContent = "⋮⋮"; // two-column dots — reads as a grip handle - - const nameInput = document.createElement("input"); - nameInput.type = "text"; - nameInput.className = "field-name"; - nameInput.placeholder = "Give description here"; - nameInput.value = row.name || ""; - nameInput.addEventListener("input", () => { - syncFieldRowsFromDom(); - }); - - const typeSelect = document.createElement("select"); - typeSelect.className = "field-type"; - FIELD_TYPES.forEach((t) => { - const opt = document.createElement("option"); - opt.value = t.value; - opt.textContent = t.label; - typeSelect.appendChild(opt); - }); - typeSelect.value = normalizeFieldType(row.type); - typeSelect.addEventListener("change", () => { - syncFieldRowsFromDom(); - }); - - const deleteBtn = document.createElement("button"); - deleteBtn.type = "button"; - deleteBtn.className = "field-delete-btn"; - deleteBtn.setAttribute("aria-label", "Remove field"); - deleteBtn.textContent = "✕"; // ✕ - deleteBtn.addEventListener("click", () => { - syncFieldRowsFromDom(); - const rowIndex = Number(rowEl.dataset.index); - fieldRows.splice(rowIndex, 1); - renderFieldRows(); - }); - - rowEl.addEventListener("dragstart", handleRowDragStart); - rowEl.addEventListener("dragover", handleRowDragOver); - rowEl.addEventListener("dragleave", handleRowDragLeave); - rowEl.addEventListener("drop", handleRowDrop); - rowEl.addEventListener("dragend", handleRowDragEnd); - - rowEl.append(handle, nameInput, typeSelect, deleteBtn); - return rowEl; -} - -function toggleMakeFillableHelp() { - const willShow = elements.makeFillableHelp.classList.contains("hidden"); - elements.makeFillableHelp.classList.toggle("hidden", !willShow); - elements.makeFillableHelpBtn.setAttribute("aria-expanded", String(willShow)); -} - -async function handleMakeFillableClick() { - if (!selectedTemplateFile) { - setStatus(elements.templateFormMessage, "Select a PDF first.", "error"); - return; - } - - const templateDirectory = DEFAULT_TEMPLATE_DIRECTORY; - - elements.makeFillableBtn.disabled = true; - const previousLabel = elements.makeFillableBtn.textContent; - elements.makeFillableBtn.textContent = "Working..."; - setStatus( - elements.templateFormMessage, - "Uploading PDF and running fillable-field detection (this can take a minute)...", - "info" - ); - - try { - if (!uploadedPath) { - const upload = await uploadTemplatePdf(selectedTemplateFile, templateDirectory); - uploadedPath = upload.pdf_path; - } - - const response = await fetch(`${API_BASE_URL}/templates/make-fillable`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ pdf_path: uploadedPath }), - }); - const body = await parseJsonResponse(response); - if (!response.ok) { - throw new Error(extractErrorMessage(body, response.status)); - } - - uploadedPath = body.pdf_path; - const count = typeof body.field_count === "number" ? body.field_count : null; - uploadedFieldCount = count; - renderFieldCountBadge(); - setStatus( - elements.templateFormMessage, - count !== null - ? `Fillable PDF created — ${count} field${count === 1 ? "" : "s"} detected.` - : "Fillable PDF created.", - "success" - ); - elements.makeFillableBtn.textContent = "Re-detect fields"; - elements.makeFillableBtn.disabled = false; - } catch (error) { - setStatus(elements.templateFormMessage, error.message, "error"); - elements.makeFillableBtn.textContent = previousLabel; - elements.makeFillableBtn.disabled = false; - } -} - -function handleAddFieldClick() { - syncFieldRowsFromDom(); - fieldRows.push({ name: "", type: "string" }); - renderFieldRows(); - const rows = elements.fieldsBuilder.querySelectorAll(".field-row .field-name"); - if (rows.length) { - rows[rows.length - 1].focus(); - } -} - -function handleRowDragStart(event) { - const rowEl = event.currentTarget; - dragSourceIndex = Number(rowEl.dataset.index); - rowEl.classList.add("is-dragging"); - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", String(dragSourceIndex)); - } -} - -function handleRowDragOver(event) { - event.preventDefault(); - if (event.dataTransfer) { - event.dataTransfer.dropEffect = "move"; - } - event.currentTarget.classList.add("drag-over"); -} - -function handleRowDragLeave(event) { - event.currentTarget.classList.remove("drag-over"); -} - -function handleRowDrop(event) { - event.preventDefault(); - const rowEl = event.currentTarget; - rowEl.classList.remove("drag-over"); - const targetIndex = Number(rowEl.dataset.index); - if (dragSourceIndex === null || dragSourceIndex === targetIndex) { - return; - } - syncFieldRowsFromDom(); - const [moved] = fieldRows.splice(dragSourceIndex, 1); - fieldRows.splice(targetIndex, 0, moved); - dragSourceIndex = null; - renderFieldRows(); -} - -function handleRowDragEnd(event) { - event.currentTarget.classList.remove("is-dragging"); - elements.fieldsBuilder - .querySelectorAll(".field-row.drag-over") - .forEach((el) => el.classList.remove("drag-over")); - dragSourceIndex = null; -} - -function loadTemplates() { - try { - const raw = localStorage.getItem(STORAGE_TEMPLATES_KEY); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : []; - } catch (_error) { - return []; - } -} - -function saveTemplates() { - localStorage.setItem(STORAGE_TEMPLATES_KEY, JSON.stringify(templates)); -} - -function upsertTemplate(template) { - const normalized = { - id: template.id, - name: template.name || "", - pdf_path: template.pdf_path || "", - fields: template.fields || {}, - }; - - const index = templates.findIndex((item) => Number(item.id) === Number(template.id)); - if (index >= 0) { - templates[index] = normalized; - } else { - templates.unshift(normalized); - } - - saveTemplates(); -} - -function restorePreviewState() { - const lastPath = localStorage.getItem(STORAGE_LAST_OUTPUT_KEY); - if (lastPath) { - elements.serverPdfPath.value = lastPath; - } -} - -async function parseJsonResponse(response) { - const text = await response.text(); - if (!text) { - return {}; - } - try { - return JSON.parse(text); - } catch (_error) { - return { raw: text }; - } -} - -function extractErrorMessage(responseBody, statusCode) { - if (responseBody && typeof responseBody === "object") { - if (typeof responseBody.error === "string") { - return responseBody.error; - } - if (Array.isArray(responseBody.detail)) { - const first = responseBody.detail[0]; - if (first && typeof first.msg === "string") { - return first.msg; - } - } - if (typeof responseBody.detail === "string") { - return responseBody.detail; - } - if (typeof responseBody.raw === "string") { - return responseBody.raw; - } - } - return `Request failed with status ${statusCode}.`; -} diff --git a/frontend/electron.js b/frontend/electron.js deleted file mode 100644 index 9fc7e198..00000000 --- a/frontend/electron.js +++ /dev/null @@ -1,64 +0,0 @@ -const { app, BrowserWindow } = require("electron"); -const path = require("path"); -const { spawn } = require("child_process"); - -let backendProcess = null; - -function startBackend() { - if (app.isPackaged) { - const ext = process.platform === 'win32' ? '.exe' : ''; - const backendPath = path.join(process.resourcesPath, "bin", `api-backend${ext}`); - - backendProcess = spawn(backendPath, [], { - stdio: 'ignore' - }); - - backendProcess.on('error', (err) => { - console.error('Failed to start backend process.', err); - }); - } else { - console.log("Running in development mode. Assuming backend is running via Docker Desktop."); - } -} - -function createWindow() { - const win = new BrowserWindow({ - width: 1100, - height: 800, - webPreferences: { - preload: path.join(__dirname, "preload.js"), - }, - }); - - // Allow microphone capture for the local speech-to-text recorder. Audio is - // only ever sent to the local Whisper service — nothing leaves the machine. - win.webContents.session.setPermissionRequestHandler( - (webContents, permission, callback) => { - callback(permission === "media"); - } - ); - - win.loadFile("index.html"); - if (!app.isPackaged) { - win.webContents.openDevTools(); - } -} - -app.whenReady().then(() => { - startBackend(); - createWindow(); -}); - -app.on("will-quit", () => { - if (backendProcess) { - backendProcess.kill(); - } -}); - -app.on("window-all-closed", () => { - if (process.platform !== "darwin") app.quit(); -}); - -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); -}); diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index d525015e..00000000 --- a/frontend/index.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - FireForm - - - -
-
-

Starting up...

-

Initializing FireForm backend

-
-
-
-

FireForm

-

- Create templates, fill forms, and preview PDFs from one place. -

-
- - - -
-

Create Template

-
- - - - - -
- Drag and drop a PDF here - or click to select a file -
-

No PDF selected.

- - -
- - -
- - - -

- What information should be filled in? Add one row per field. Fields are filled - into your PDF in the order shown — drag to reorder. -

- -
- - - -
-

- -
- - - - - - -
- - - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 86ed6800..00000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4025 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fireform", - "version": "1.1.0", - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - } - }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/fuses/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", - "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", - "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.8.1", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.8.1", - "electron-builder-squirrel-windows": "26.8.1" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", - "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", - "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "35.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz", - "integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", - "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.8.1", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", - "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.8.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-abi": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.29.0.tgz", - "integrity": "sha512-bGc7hHz6lrdpMqH3XqfiHc5PKzEhjgUj6OLpTXynkLi9JZKyMByI/tdpm4Liu6O2BjtE1lakBWXjOQS1EnSQLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", - "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", - "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 468e34ce..00000000 --- a/frontend/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "fireform", - "version": "1.1.0", - "description": "FireForm — report once, file everywhere", - "repository": "fireform-core/FireForm", - "main": "electron.js", - "scripts": { - "start": "electron .", - "dist": "electron-builder --publish never" - }, - "devDependencies": { - "electron": "^35.0.0", - "electron-builder": "^26.0.0" - }, - "build": { - "appId": "com.fireform.app", - "productName": "FireForm", - "files": [ - "index.html", - "app.js", - "styles.css", - "electron.js", - "preload.js" - ], - "extraResources": [ - { - "from": "bin", - "to": "bin", - "filter": [ - "**/*" - ] - } - ], - "mac": { - "target": "dmg" - }, - "win": { - "target": "nsis" - }, - "linux": { - "target": "AppImage" - } - } -} diff --git a/frontend/preload.js b/frontend/preload.js deleted file mode 100644 index 4e99198a..00000000 --- a/frontend/preload.js +++ /dev/null @@ -1,3 +0,0 @@ -// Preload script — placeholder for future Node.js bridge APIs. -// Use contextBridge.exposeInMainWorld() here to safely expose -// Node functionality to the renderer process when needed. diff --git a/frontend/styles.css b/frontend/styles.css deleted file mode 100644 index c5b4a694..00000000 --- a/frontend/styles.css +++ /dev/null @@ -1,658 +0,0 @@ -:root { - --bg: linear-gradient(145deg, #f7f4ec 0%, #eef4f7 100%); - --panel: #ffffff; - --panel-border: #d7dde3; - --text: #1f2a36; - --muted: #576779; - --primary: #125a86; - --primary-strong: #0b4568; - --success: #1a7f4b; - --error: #a43434; - --radius: 14px; - --shadow: 0 10px 28px rgba(16, 44, 70, 0.09); -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - font-family: "Avenir Next", "Trebuchet MS", "Segoe UI", sans-serif; - color: var(--text); - background: var(--bg); -} - -.app-shell { - max-width: 980px; - margin: 0 auto; - padding: 24px 18px 48px; -} - -.app-header { - margin-bottom: 16px; -} - -.eyebrow { - margin: 0 0 8px; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--muted); - font-size: 0.78rem; -} - -h1 { - margin: 0; - font-size: clamp(1.8rem, 3vw, 2.5rem); - line-height: 1.15; -} - -h2 { - margin: 0 0 14px; - font-size: 1.25rem; -} - -.subtitle { - margin: 8px 0 0; - color: var(--muted); -} - -.card { - background: var(--panel); - border: 1px solid var(--panel-border); - border-radius: var(--radius); - box-shadow: var(--shadow); - padding: 18px; -} - -.api-config { - display: grid; - gap: 8px; - margin-bottom: 14px; -} - -.tabs { - display: flex; - gap: 8px; - margin-bottom: 14px; - flex-wrap: wrap; -} - -.tab { - border: 1px solid var(--panel-border); - background: #f9fbfc; - border-radius: 999px; - padding: 8px 14px; - cursor: pointer; - font-weight: 600; - color: var(--text); -} - -.tab.active { - border-color: var(--primary); - color: #fff; - background: var(--primary); -} - -.panel { - display: grid; - gap: 14px; -} - -.stacked-form { - display: grid; - gap: 10px; -} - -.dropzone { - border: 2px dashed #9ab1c7; - border-radius: 12px; - padding: 20px 14px; - display: grid; - gap: 4px; - text-align: center; - cursor: pointer; - background: #f7fbff; - color: #2f4a63; - transition: border-color 0.2s ease, background-color 0.2s ease; -} - -.dropzone strong { - font-size: 1.02rem; - color: #1f3a53; -} - -.dropzone span { - font-size: 0.92rem; - color: #4e657b; -} - -.dropzone:hover, -.dropzone:focus-visible, -.dropzone.active { - border-color: var(--primary); - background: #eef7ff; - outline: none; -} - -label { - font-weight: 600; -} - -input, -textarea, -select, -button { - font: inherit; -} - -input, -textarea, -select { - width: 100%; - border: 1px solid #bcc8d6; - border-radius: 10px; - padding: 10px 12px; - background: #fff; - color: var(--text); -} - -select { - appearance: none; - -webkit-appearance: none; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 32px; -} - -input:focus, -textarea:focus, -select:focus { - outline: 2px solid var(--primary); - outline-offset: 1px; - border-color: var(--primary); -} - -textarea { - resize: vertical; -} - -button { - border: 0; - border-radius: 10px; - background: var(--primary); - color: #fff; - cursor: pointer; - padding: 10px 14px; - font-weight: 700; -} - -button:hover { - background: var(--primary-strong); -} - -.helper { - margin: 0; - color: var(--muted); - font-size: 0.95rem; -} - -.status { - margin: 0; - min-height: 1.4em; - color: var(--muted); -} - -.status.success { - color: var(--success); -} - -.status.error { - color: var(--error); -} - -.json-output { - margin: 0; - white-space: pre-wrap; - word-break: break-word; - border-radius: 10px; - border: 1px solid #d7dde3; - background: #f7fafc; - padding: 10px 12px; - max-height: 280px; - overflow: auto; - font-family: "IBM Plex Mono", "Menlo", "Consolas", monospace; - font-size: 0.88rem; -} - -.hidden { - display: none; -} - -.secondary-btn { - background: #f1f5f9; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - font-weight: 600; - padding: 8px 14px; - justify-self: start; -} - -.secondary-btn:hover { - background: #e5ecf3; -} - -.stt-controls { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.stt-btn { - display: inline-flex; - align-items: center; - gap: 7px; - padding: 8px 14px; -} - -.stt-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.stt-dot { - width: 9px; - height: 9px; - border-radius: 50%; - background: #c2ccd6; - flex-shrink: 0; -} - -.stt-controls.is-recording .stt-dot { - background: var(--error); - animation: stt-pulse 1.1s ease-in-out infinite; -} - -.stt-controls.is-paused .stt-dot { - background: var(--primary); -} - -@keyframes stt-pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.35; transform: scale(0.78); } -} - -.stt-status { - color: var(--muted); - font-size: 0.92rem; - min-height: 1.2em; -} - -/* Fill Form: model picker, template tiles, view toggle */ -.helper.error { - color: var(--error); - font-weight: 600; -} - -.template-tiles { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); - gap: 14px; -} - -.template-tile { - position: relative; - display: flex; - flex-direction: column; - gap: 6px; - min-height: 112px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; - padding: 16px; - cursor: pointer; - transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; -} - -.template-tile:hover { - border-color: #9ab1c7; -} - -.template-tile:focus-visible { - outline: 2px solid var(--primary); - outline-offset: 1px; -} - -.template-tile.selected { - border-color: var(--primary); - background: #eef7ff; - box-shadow: inset 0 0 0 1px var(--primary); -} - -.tile-body { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.tile-title { - font-weight: 700; - color: var(--text); - /* Uniform tiles: clamp long titles to two lines, then ellipsis. */ - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - word-break: break-word; -} - -.tile-meta { - font-size: 0.85rem; - color: var(--muted); -} - -.tile-preview-btn { - margin-top: auto; /* pin to the bottom so every tile's button lines up */ - align-self: flex-start; - background: transparent; - color: var(--primary-strong); - border: 1px solid var(--panel-border); - border-radius: 8px; - padding: 5px 10px; - font-size: 0.82rem; - font-weight: 600; -} - -.tile-preview-btn:hover { - background: #e5ecf3; -} - -/* Greyed-but-clickable: looks disabled, still fires click to prompt the user. */ -button.is-disabled, -button.is-disabled:hover { - background: #c2ccd6; - cursor: not-allowed; -} - -.help-trigger { - background: #e5ecf3; - color: var(--primary-strong); - border: 1px solid #c8d2dd; - border-radius: 50%; - width: 24px; - height: 24px; - padding: 0; - font-weight: 700; - font-size: 0.9rem; - line-height: 1; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - flex-shrink: 0; -} - -.help-trigger:hover { - background: #d8e2eb; -} - -.help-trigger[aria-expanded="true"] { - background: var(--primary); - color: #fff; - border-color: var(--primary); -} - -.field-count-badge { - margin: 0; - padding: 6px 10px; - border-radius: 8px; - background: #f1f5f9; - border: 1px solid var(--panel-border); - color: var(--muted); - font-size: 0.92rem; - display: inline-block; - justify-self: start; -} - -.field-count-badge.hidden { - display: none; -} - -.field-count-badge.match { - background: #e8f3ec; - border-color: #b9d8c4; - color: var(--success); -} - -.field-count-badge.mismatch { - background: #fbe9e9; - border-color: #f3d3d3; - color: var(--error); -} - -.fields-builder { - display: grid; - gap: 8px; -} - -.field-row { - display: grid; - grid-template-columns: 24px 1fr 170px 36px; - gap: 10px; - align-items: center; - padding: 8px 10px; - border: 1px solid var(--panel-border); - border-radius: 12px; - background: #fbfcfe; -} - -.field-row.is-dragging { - opacity: 0.4; -} - -.field-row.drag-over { - border-color: var(--primary); - background: #eef7ff; -} - -.field-drag-handle { - cursor: grab; - color: var(--muted); - text-align: center; - user-select: none; - font-size: 1rem; - line-height: 1; -} - -.field-drag-handle:active { - cursor: grabbing; -} - -.field-delete-btn { - background: transparent; - color: var(--muted); - border: 1px solid transparent; - padding: 0; - font-size: 1rem; - font-weight: 600; - line-height: 1; - border-radius: 8px; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.field-delete-btn:hover { - background: #fbe9e9; - color: var(--error); - border-color: #f3d3d3; -} - -.fields-table { - width: 100%; - border-collapse: collapse; - border: 1px solid var(--panel-border); - border-radius: 10px; - overflow: hidden; - background: #fff; - font-size: 0.92rem; -} - -.fields-table th, -.fields-table td { - text-align: left; - padding: 8px 10px; - border-bottom: 1px solid var(--panel-border); -} - -.fields-table tr:last-child td { - border-bottom: 0; -} - -.fields-table th { - background: #f1f5f9; - font-weight: 600; - color: var(--muted); -} - -@media (max-width: 540px) { - .field-row { - grid-template-columns: 22px 1fr 36px; - grid-template-areas: - "handle name delete" - ". select select"; - } - - .field-drag-handle { grid-area: handle; } - .field-row input[type="text"] { grid-area: name; } - .field-row select { grid-area: select; } - .field-delete-btn { grid-area: delete; } -} - -.divider { - width: 100%; - border: 0; - border-top: 1px solid #d7dde3; - margin: 4px 0; -} - -.template-list { - display: grid; - gap: 12px; -} - -.template-card { - border: 1px solid #d7dde3; - border-radius: 12px; - padding: 14px; - background: #fbfcfe; -} - -.template-meta { - margin: 0; -} - -.card-actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 10px; -} - -.card-actions button { - padding: 8px 12px; - font-weight: 600; -} - -.empty-state { - padding: 14px; - border-radius: 10px; - border: 1px dashed #bcc8d6; - color: var(--muted); - background: #f8fafb; -} - -.preview-controls { - display: grid; - gap: 8px; -} - -.inline-actions { - display: grid; - grid-template-columns: 1fr auto; - gap: 8px; - align-items: center; -} - -#pdfFrame { - width: 100%; - min-height: 560px; - border: 1px solid #d7dde3; - border-radius: 10px; - background: #fff; -} - -@media (max-width: 720px) { - .app-shell { - padding: 16px 12px 28px; - } - - .card { - padding: 14px; - } - - .inline-actions { - grid-template-columns: 1fr; - } - - #pdfFrame { - min-height: 420px; - } -} - -.loading-screen { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: var(--bg); - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - z-index: 9999; - transition: opacity 0.5s ease, visibility 0.5s ease; -} - -.loading-screen.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.loading-screen h2 { - margin-top: 24px; - color: var(--primary); -} - -.spinner { - width: 50px; - height: 50px; - border: 5px solid rgba(18, 90, 134, 0.2); - border-top-color: var(--primary); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} diff --git a/requirements.txt b/requirements.txt index a22229d3..0a800f01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,18 @@ fastapi uvicorn pydantic sqlmodel +psycopg2-binary pytest httpx numpy<2 ollama pypdf python-multipart +celery[redis] +redis +openmeteo-requests +requests-cache +retry-requests +pandas +geopy +alembic diff --git a/scripts/check-deps.sh b/scripts/check-deps.sh new file mode 100755 index 00000000..3c4484e7 --- /dev/null +++ b/scripts/check-deps.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -e + +PASS=0 +FAIL=1 +errors=0 + +check() { + label="$1" + shift + if "$@" > /dev/null 2>&1; then + echo " [ok] $label" + else + echo " [!!] $label" + errors=$((errors + 1)) + fi +} + +echo "" +echo "Checking dependencies..." +echo "========================" + +# Docker daemon running +check "Docker daemon is running" docker info + +# docker compose v2 (plugin form, not legacy docker-compose) +check "docker compose v2 available" docker compose version + +# Minimum Docker version: 24 (BuildKit cache mounts stable) +DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1) +if [ -n "$DOCKER_VERSION" ] && [ "$DOCKER_VERSION" -ge 24 ] 2>/dev/null; then + echo " [ok] Docker version >= 24 (found $(docker version --format '{{.Server.Version}}' 2>/dev/null))" +else + echo " [!!] Docker version >= 24 required (found $(docker version --format '{{.Server.Version}}' 2>/dev/null || echo 'unknown'))" + echo " BuildKit cache mounts in docker/dev/Dockerfile require Docker 24+." + errors=$((errors + 1)) +fi + +echo "" + +if [ "$errors" -gt 0 ]; then + echo "$errors check(s) failed. Fix the above before continuing." + echo "" + echo " Install Docker: https://docs.docker.com/get-docker/" + echo " Upgrade Docker Desktop: https://docs.docker.com/desktop/release-notes/" + echo "" + exit 1 +fi + +echo "All checks passed." +echo "" diff --git a/container-init.sh b/scripts/container-init.sh similarity index 100% rename from container-init.sh rename to scripts/container-init.sh diff --git a/scripts/init-env.sh b/scripts/init-env.sh new file mode 100755 index 00000000..c2c08040 --- /dev/null +++ b/scripts/init-env.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e + +ENV_EXAMPLE="docker/.env.example" +ENV_DEV="docker/.env.dev" + +echo "" +echo "Setting up environment..." +echo "=========================" + +if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: $ENV_EXAMPLE not found. Are you running from the repo root?" + exit 1 +fi + +if [ -f "$ENV_DEV" ]; then + printf " %s already exists. Overwrite? [y/N] " "$ENV_DEV" + read -r answer + case "$answer" in + [yY]*) + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Overwritten." + ;; + *) + echo " Kept existing $ENV_DEV." + ;; + esac +else + cp "$ENV_EXAMPLE" "$ENV_DEV" + echo " Created $ENV_DEV from $ENV_EXAMPLE." +fi + +echo "" +echo " Review docker/.env.dev and adjust values if needed before running 'make fireform'." +echo "" diff --git a/scripts/select-model.sh b/scripts/select-model.sh new file mode 100755 index 00000000..4b3c9d36 --- /dev/null +++ b/scripts/select-model.sh @@ -0,0 +1,107 @@ +#!/bin/sh +set -e + +ENV_DEV="docker/.env.dev" +COMPOSE="docker compose -f docker/dev/compose.yml --env-file $ENV_DEV" + +# model name | approx size +MODELS="qwen2.5:1.5b|~1GB qwen2.5:3b|~2GB qwen2.5:7b|~4GB llama3.2:3b|~2GB mistral:7b|~4GB" + +current_model="" +if [ -f "$ENV_DEV" ]; then + current_model=$(grep -E '^OLLAMA_MODEL=' "$ENV_DEV" | cut -d= -f2 | tr -d '[:space:]') +fi + +echo "" +echo "Select Ollama model" +echo "===================" +[ -n "$current_model" ] && echo " Current: $current_model" +echo "" + +i=1 +for entry in $MODELS; do + name=$(echo "$entry" | cut -d'|' -f1) + size=$(echo "$entry" | cut -d'|' -f2) + if [ "$name" = "${current_model}" ]; then + echo " $i) $name $size [current]" + else + echo " $i) $name $size" + fi + i=$((i + 1)) +done +echo " $i) Enter custom model name" +i=$((i + 1)) +echo " $i) Keep current" +echo "" +printf "Choice [1-$i]: " +read -r choice + +total=$(echo "$MODELS" | wc -w | tr -d '[:space:]') +keep_choice=$((total + 2)) +custom_choice=$((total + 1)) + +if [ "$choice" = "$keep_choice" ] || [ -z "$choice" ]; then + echo " Keeping current model: ${current_model:-qwen2.5:1.5b}" + echo "" + exit 0 +fi + +if [ "$choice" = "$custom_choice" ]; then + printf " Enter model name (e.g. phi3:mini): " + read -r selected + if [ -z "$selected" ]; then + echo " No model entered. Keeping current." + echo "" + exit 0 + fi + selected_size="unknown size" +else + # Validate numeric choice in range + if ! echo "$choice" | grep -qE '^[0-9]+$' || [ "$choice" -lt 1 ] || [ "$choice" -gt "$total" ]; then + echo " Invalid choice. Keeping current model." + echo "" + exit 0 + fi + i=1 + for entry in $MODELS; do + if [ "$i" = "$choice" ]; then + selected=$(echo "$entry" | cut -d'|' -f1) + selected_size=$(echo "$entry" | cut -d'|' -f2) + fi + i=$((i + 1)) + done +fi + +# Patch OLLAMA_MODEL in .env.dev +tmp=$(mktemp) +sed "s|^OLLAMA_MODEL=.*|OLLAMA_MODEL=$selected|" "$ENV_DEV" > "$tmp" +mv "$tmp" "$ENV_DEV" +echo "" +echo " OLLAMA_MODEL set to: $selected" + +# If container is running, check and optionally pull immediately. +# If not running, the caller (make init) handles the pull prompt. +if $COMPOSE ps ollama 2>/dev/null | grep -q "running"; then + if $COMPOSE exec -T ollama ollama list 2>/dev/null | grep -q "^$selected"; then + echo " Model already pulled. Nothing to download." + echo "" + exit 0 + fi + + echo "" + printf " Model not yet downloaded ($selected_size). Pull now? [y/N] " + read -r pull_answer + case "$pull_answer" in + [yY]*) + echo "" + $COMPOSE exec -T ollama ollama pull "$selected" + echo "" + echo " Model ready." + ;; + *) + echo " Skipped. Run 'make pull-model' when ready." + ;; + esac +fi + +echo "" diff --git a/scripts/setup-dockers-env.sh b/scripts/setup-dockers-env.sh new file mode 100644 index 00000000..17418ad9 --- /dev/null +++ b/scripts/setup-dockers-env.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source venv/bin/activate +if command -v uv > /dev/null 2>&1; then + uv pip install -r requirements.txt +else + pip install -r requirements.txt +fi diff --git a/setup-dockers-env.sh b/setup-dockers-env.sh deleted file mode 100644 index 90adb5c4..00000000 --- a/setup-dockers-env.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -source venv/bin/activate -pip install -r requirements.txt diff --git a/src/controller.py b/src/controller.py deleted file mode 100644 index 0e19290a..00000000 --- a/src/controller.py +++ /dev/null @@ -1,11 +0,0 @@ -from src.file_manipulator import FileManipulator - -class Controller: - def __init__(self): - self.file_manipulator = FileManipulator() - - def fill_form(self, user_input: str, fields: list, pdf_form_path: str, model: str = None): - return self.file_manipulator.fill_form(user_input, fields, pdf_form_path, model=model) - - def prepare_fillable(self, pdf_path: str): - return self.file_manipulator.prepare_fillable(pdf_path) \ No newline at end of file diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 630d262a..00000000 --- a/src/main.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -os.environ["CUDA_VISIBLE_DEVICES"] = "" - -# Monkey patch rfdetr to force CPU usage on Mac Silicon / Docker -try: - import rfdetr.detr - original_ensure = rfdetr.detr._ensure_model_on_device - def patched_ensure(model_ctx): - model_ctx.device = "cpu" - original_ensure(model_ctx) - rfdetr.detr._ensure_model_on_device = patched_ensure -except ImportError: - pass - -from commonforms import prepare_form -from pypdf import PdfReader -from controller import Controller - -if __name__ == "__main__": - file = "./src/inputs/file.pdf" - user_input = "Hi. The employee's name is John Doe. His job title is managing director. His department supervisor is Jane Doe. His phone number is 123456. His email is jdoe@ucsc.edu. The signature is , and the date is 01/02/2005" - fields = [ - "Employee's name", - "Employee's job title", - "Employee's department supervisor", - "Employee's phone number", - "Employee's email", - "Signature", - "Date", - ] - prepared_pdf = "temp_outfile.pdf" - prepare_form(file, prepared_pdf) - - reader = PdfReader(prepared_pdf) - fields = reader.get_fields() - if fields: - num_fields = len(fields) - else: - num_fields = 0 - - controller = Controller() - controller.fill_form(user_input, fields, file) diff --git a/tests/conftest.py b/tests/conftest.py index 5f1eb3fa..ecdc4e9b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,16 +5,25 @@ """ import io -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from sqlalchemy.pool import StaticPool -from sqlmodel import SQLModel, Session, create_engine - -from api.main import app -from api.deps import get_db -from api.db.models import Template, FormSubmission # noqa: F401 — registers tables +from sqlmodel import Session, SQLModel, create_engine + +from app.api.deps import get_db +from app.main import app +from app.models import ( # noqa: F401 — registers tables + Extraction, + Form, + FormSubmission, + Incident, + Input, + Job, + Report, + Template, +) # --------------------------------------------------------------------------- # In-memory database @@ -54,6 +63,12 @@ def db(): yield session +@pytest.fixture +def test_engine(): + """Expose the shared in-memory engine for tests that need to open extra sessions.""" + return _engine + + # --------------------------------------------------------------------------- # Minimal PDF bytes (valid 1-page blank PDF) # --------------------------------------------------------------------------- @@ -85,8 +100,8 @@ def pdf_upload(pdf_bytes): @pytest.fixture def mock_controller(): """Patch Controller so create_template / fill_form don't touch the FS or LLM.""" - with patch("api.routes.templates.Controller") as tpl_cls, \ - patch("api.routes.forms.Controller") as form_cls: + with patch("app.api.routes.templates.Controller") as tpl_cls, \ + patch("app.api.routes.forms.Controller") as form_cls: tpl_instance = MagicMock() tpl_instance.create_template.return_value = "src/inputs/test_template.pdf" tpl_cls.return_value = tpl_instance diff --git a/tests/test_api.py b/tests/test_api.py index 89a9b25a..c794431a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,11 +4,10 @@ All heavy dependencies (LLM, commonforms, filesystem) are mocked via conftest. """ -import pytest from sqlmodel import select -from api.db.models import Template, FormSubmission - +from app.core.config import API_PREFIX +from app.models import FormSubmission, Template # ═══════════════════════════════════════════════════════════════════════════ # DB model sanity @@ -84,7 +83,7 @@ def test_list_templates_ordering(self, db): class TestTemplateEndpoints: def test_list_templates_empty(self, client): - resp = client.get("/templates") + resp = client.get(f"{API_PREFIX}/templates") assert resp.status_code == 200 assert resp.json() == [] @@ -98,7 +97,7 @@ def test_create_template(self, client, mock_controller): "Location": "string", }, } - resp = client.post("/templates/create", json=payload) + resp = client.post(f"{API_PREFIX}/templates/create", json=payload) assert resp.status_code == 200 data = resp.json() @@ -111,12 +110,12 @@ def test_create_template(self, client, mock_controller): def test_create_then_list(self, client, mock_controller): """Creating a template should make it appear in the list.""" - client.post("/templates/create", json={ + client.post(f"{API_PREFIX}/templates/create", json={ "name": "T1", "pdf_path": "a.pdf", "fields": {"f": "string"}, }) - resp = client.get("/templates") + resp = client.get(f"{API_PREFIX}/templates") assert resp.status_code == 200 assert len(resp.json()) == 1 assert resp.json()[0]["name"] == "T1" @@ -126,11 +125,11 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): # Point the upload directory inside tmp_path (which is inside the project # for the path-safety check — we monkeypatch the check). monkeypatch.setattr( - "api.routes.templates.PROJECT_ROOT", + "app.api.routes.templates.PROJECT_ROOT", tmp_path, ) resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf_upload], data={"directory": str(tmp_path)}, ) @@ -142,19 +141,19 @@ def test_upload_pdf(self, client, pdf_upload, tmp_path, monkeypatch): def test_upload_non_pdf_rejected(self, client): import io bad_file = ("file", ("notes.txt", io.BytesIO(b"hello"), "text/plain")) - resp = client.post("/templates/upload", files=[bad_file]) + resp = client.post(f"{API_PREFIX}/templates/upload", files=[bad_file]) assert resp.status_code == 400 assert "PDF" in resp.json()["detail"] def test_preview_missing_file(self, client): - resp = client.get("/templates/preview", params={"path": "src/inputs/nonexistent.pdf"}) + resp = client.get(f"{API_PREFIX}/templates/preview", params={"path": "src/inputs/nonexistent.pdf"}) assert resp.status_code == 404 def test_directory_traversal_blocked(self, client): import io pdf = ("file", ("evil.pdf", io.BytesIO(b"%PDF-1.4"), "application/pdf")) resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf], data={"directory": "/etc"}, ) @@ -170,7 +169,7 @@ class TestFormEndpoints: def _seed_template(self, client, mock_controller): """Helper: create a template and return its ID.""" - resp = client.post("/templates/create", json={ + resp = client.post(f"{API_PREFIX}/templates/create", json={ "name": "Employee Form", "pdf_path": "src/inputs/employee.pdf", "fields": { @@ -183,7 +182,7 @@ def _seed_template(self, client, mock_controller): def test_fill_form_success(self, client, mock_controller): tpl_id = self._seed_template(client, mock_controller) - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "The employee is John Doe, email jdoe@ucsc.edu", }) @@ -196,7 +195,7 @@ def test_fill_form_success(self, client, mock_controller): mock_controller["form_ctrl"].fill_form.assert_called_once() def test_fill_form_missing_template(self, client, mock_controller): - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": 9999, "input_text": "some text", }) @@ -206,17 +205,23 @@ def test_fill_form_template_file_not_found(self, client, mock_controller): tpl_id = self._seed_template(client, mock_controller) mock_controller["form_ctrl"].fill_form.side_effect = FileNotFoundError("PDF template not found") - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "some text", }) assert resp.status_code == 500 - assert "PDF template not found" in resp.json()["error"] + assert resp.json()["error_code"] == "FORM_FILL_ERROR" + assert "PDF template not found" in resp.json()["message"] def test_fill_form_validates_body(self, client): - """Missing required fields → 422.""" - resp = client.post("/forms/fill", json={}) + """Missing required fields → 422 with contract envelope.""" + resp = client.post(f"{API_PREFIX}/forms/fill", json={}) assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert len(body["validation_errors"]) >= 1 + assert body["validation_errors"][0]["field"] is not None + assert body["validation_errors"][0]["issue"] is not None def test_transcribe_success(self, client, monkeypatch): """Audio is forwarded to the whisper sidecar and its text returned.""" @@ -235,10 +240,10 @@ def fake_post(url, params=None, files=None, timeout=None): captured["files"] = files return fake_response - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"RIFFfake"), "audio/wav")) - resp = client.post("/forms/transcribe", files=[audio]) + resp = client.post(f"{API_PREFIX}/forms/transcribe", files=[audio]) assert resp.status_code == 200 assert resp.json()["text"] == "structure fire on main street" @@ -247,16 +252,16 @@ def fake_post(url, params=None, files=None, timeout=None): assert captured["params"]["output"] == "json" def test_list_models(self, client, monkeypatch): - """/forms/models lists Ollama models and always includes the default.""" + ""f"{API_PREFIX}/forms/models lists Ollama models and always includes the default.""" from unittest.mock import MagicMock fake_response = MagicMock() fake_response.json.return_value = {"models": [{"name": "qwen2.5:3b"}, {"name": "mistral:latest"}]} fake_response.raise_for_status.return_value = None - monkeypatch.setattr("api.routes.forms.requests.get", lambda *a, **k: fake_response) + monkeypatch.setattr("app.api.routes.forms.requests.get", lambda *a, **k: fake_response) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") - resp = client.get("/forms/models") + resp = client.get(f"{API_PREFIX}/forms/models") assert resp.status_code == 200 body = resp.json() assert body["default"] == "qwen2.5:1.5b" @@ -270,17 +275,17 @@ def test_list_models_ollama_down(self, client, monkeypatch): def boom(*a, **k): raise requests.exceptions.ConnectionError("down") - monkeypatch.setattr("api.routes.forms.requests.get", boom) + monkeypatch.setattr("app.api.routes.forms.requests.get", boom) monkeypatch.setenv("OLLAMA_MODEL", "qwen2.5:1.5b") - resp = client.get("/forms/models") + resp = client.get(f"{API_PREFIX}/forms/models") assert resp.status_code == 200 assert resp.json()["models"] == ["qwen2.5:1.5b"] def test_fill_form_passes_model_override(self, client, mock_controller): """A `model` in the request reaches Controller.fill_form but isn't persisted.""" tpl_id = self._seed_template(client, mock_controller) - resp = client.post("/forms/fill", json={ + resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": tpl_id, "input_text": "John Doe", "model": "qwen2.5:3b", @@ -292,15 +297,16 @@ def test_fill_form_passes_model_override(self, client, mock_controller): def test_transcribe_service_unavailable(self, client, monkeypatch): """A down whisper service surfaces as a 503, not a 500.""" import io + import requests def fake_post(*args, **kwargs): raise requests.exceptions.ConnectionError("no service") - monkeypatch.setattr("api.routes.forms.requests.post", fake_post) + monkeypatch.setattr("app.api.routes.forms.requests.post", fake_post) audio = ("audio", ("recording.wav", io.BytesIO(b"data"), "audio/wav")) - resp = client.post("/forms/transcribe", files=[audio]) + resp = client.post(f"{API_PREFIX}/forms/transcribe", files=[audio]) assert resp.status_code == 503 @@ -316,9 +322,9 @@ class TestE2EPipeline: def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypatch, db): # -- Step 1: Upload a PDF -- - monkeypatch.setattr("api.routes.templates.PROJECT_ROOT", tmp_path) + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) upload_resp = client.post( - "/templates/upload", + f"{API_PREFIX}/templates/upload", files=[pdf_upload], data={"directory": str(tmp_path)}, ) @@ -327,7 +333,7 @@ def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypa assert uploaded_path.endswith(".pdf") # -- Step 2: Create a template from the uploaded PDF -- - create_resp = client.post("/templates/create", json={ + create_resp = client.post(f"{API_PREFIX}/templates/create", json={ "name": "Incident Report", "pdf_path": uploaded_path, "fields": { @@ -343,13 +349,13 @@ def test_full_flow(self, client, mock_controller, pdf_upload, tmp_path, monkeypa assert template_id is not None # -- Step 3: Verify template appears in list -- - list_resp = client.get("/templates") + list_resp = client.get(f"{API_PREFIX}/templates") assert list_resp.status_code == 200 templates = list_resp.json() assert any(t["id"] == template_id for t in templates) # -- Step 4: Fill the form -- - fill_resp = client.post("/forms/fill", json={ + fill_resp = client.post(f"{API_PREFIX}/forms/fill", json={ "template_id": template_id, "input_text": ( "Officer Jane Smith, badge 4521. On January 15 2025 at " diff --git a/tests/test_deletion.py b/tests/test_deletion.py new file mode 100644 index 00000000..7dcffc40 --- /dev/null +++ b/tests/test_deletion.py @@ -0,0 +1,253 @@ +"""Tests for DELETE /api/v1/templates/{id}, DELETE /api/v1/forms/{id}, +POST /api/v1/forms/purge, and API-key access control. +""" + +from datetime import datetime, timedelta, timezone + +import pytest + +from app.core.config import API_PREFIX +from app.models import FormSubmission, Template + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _seed_template(client, name="T1", pdf_path="src/inputs/t.pdf"): + resp = client.post(f"{API_PREFIX}/templates/create", json={ + "name": name, + "pdf_path": pdf_path, + "fields": {"name": "string"}, + }) + assert resp.status_code == 200, resp.json() + return resp.json()["id"] + + +def _seed_submission(db, template_id, output_pdf_path="src/outputs/out.pdf"): + sub = FormSubmission( + template_id=template_id, + input_text="John Doe, firefighter", + output_pdf_path=output_pdf_path, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return sub.id + + +# =========================================================================== +# DELETE /api/v1/templates/{template_id} +# =========================================================================== + +class TestDeleteTemplate: + + def test_delete_template_no_key_required_when_unconfigured(self, client): + """No API key needed when FIREFORM_API_KEY is empty (default).""" + tpl_id = _seed_template(client) + resp = client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "success" + + def test_delete_template_removes_from_db(self, client, db): + tpl_id = _seed_template(client) + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert db.get(Template, tpl_id) is None + + def test_delete_template_not_found(self, client): + resp = client.delete(f"{API_PREFIX}/templates/99999") + assert resp.status_code == 404 + + def test_delete_template_cascades_submissions(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + + assert db.get(FormSubmission, sub_id) is None + assert db.get(Template, tpl_id) is None + + def test_delete_template_deletes_pdf_file(self, client, tmp_path, monkeypatch): + """Verify the template PDF file is removed from disk on delete.""" + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) + pdf_file = tmp_path / "myform.pdf" + pdf_file.write_bytes(b"%PDF-1.4 fake") + + relative_path = "myform.pdf" + tpl_id = _seed_template(client, pdf_path=relative_path) + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert not pdf_file.exists() + + def test_delete_template_deletes_submission_output_pdfs(self, client, db, tmp_path, monkeypatch): + """Output PDFs of related submissions should be wiped on template deletion.""" + monkeypatch.setattr("app.api.routes.templates.PROJECT_ROOT", tmp_path) + + out_pdf = tmp_path / "filled.pdf" + out_pdf.write_bytes(b"%PDF-1.4 filled") + + tpl_id = _seed_template(client, pdf_path="tpl.pdf") + _seed_submission(db, tpl_id, output_pdf_path="filled.pdf") + + client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert not out_pdf.exists() + + +# =========================================================================== +# DELETE /api/v1/forms/{submission_id} +# =========================================================================== + +class TestDeleteSubmission: + + def test_delete_submission_no_key_when_unconfigured(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert resp.status_code == 200 + assert resp.json()["status"] == "success" + + def test_delete_submission_removes_from_db(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert db.get(FormSubmission, sub_id) is None + + def test_delete_submission_not_found(self, client): + resp = client.delete(f"{API_PREFIX}/forms/99999") + assert resp.status_code == 404 + + def test_delete_submission_removes_output_pdf(self, client, db, tmp_path, monkeypatch): + monkeypatch.setattr("app.api.routes.forms.PROJECT_ROOT", tmp_path) + out_pdf = tmp_path / "filled_out.pdf" + out_pdf.write_bytes(b"%PDF-1.4") + + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id, output_pdf_path="filled_out.pdf") + + client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert not out_pdf.exists() + + +# =========================================================================== +# POST /api/v1/forms/purge +# =========================================================================== + +class TestPurgeSubmissions: + + def _seed_old_submission(self, db, tpl_id, days_old=40): + old_ts = datetime.now(timezone.utc) - timedelta(days=days_old) + sub = FormSubmission( + template_id=tpl_id, + input_text="old data", + output_pdf_path="src/outputs/old.pdf", + created_at=old_ts, + ) + db.add(sub) + db.commit() + db.refresh(sub) + return sub.id + + def test_purge_removes_old_submissions(self, client, db): + tpl_id = _seed_template(client) + old_id = self._seed_old_submission(db, tpl_id, days_old=40) + new_id = _seed_submission(db, tpl_id) # recent + + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert resp.json()["purged_count"] == 1 + + assert db.get(FormSubmission, old_id) is None + assert db.get(FormSubmission, new_id) is not None + + def test_purge_nothing_to_remove(self, client, db): + tpl_id = _seed_template(client) + _seed_submission(db, tpl_id) # recent + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert resp.json()["purged_count"] == 0 + + def test_purge_removes_output_pdf_file(self, client, db, tmp_path, monkeypatch): + monkeypatch.setattr("app.api.routes.forms.PROJECT_ROOT", tmp_path) + out_pdf = tmp_path / "old_filled.pdf" + out_pdf.write_bytes(b"%PDF-1.4") + + tpl_id = _seed_template(client) + old_ts = datetime.now(timezone.utc) - timedelta(days=50) + sub = FormSubmission( + template_id=tpl_id, + input_text="old", + output_pdf_path="old_filled.pdf", + created_at=old_ts, + ) + db.add(sub) + db.commit() + + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 200 + assert not out_pdf.exists() + + +# =========================================================================== +# API-Key Access Control +# =========================================================================== + +class TestApiKeyAccessControl: + + @pytest.fixture(autouse=True) + def _set_api_key(self, monkeypatch): + monkeypatch.setattr("app.api.deps.FIREFORM_API_KEY", "secret-test-key") + + def test_delete_template_requires_key_when_set(self, client): + tpl_id = _seed_template(client) + resp = client.delete(f"{API_PREFIX}/templates/{tpl_id}") + assert resp.status_code == 401 + + def test_delete_template_with_valid_x_api_key(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_delete_template_with_bearer_token(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"Authorization": "Bearer secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_delete_template_wrong_key_rejected(self, client): + tpl_id = _seed_template(client) + resp = client.delete( + f"{API_PREFIX}/templates/{tpl_id}", + headers={"X-API-Key": "wrong-key"}, + ) + assert resp.status_code == 401 + + def test_delete_submission_requires_key_when_set(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete(f"{API_PREFIX}/forms/{sub_id}") + assert resp.status_code == 401 + + def test_delete_submission_with_valid_key(self, client, db): + tpl_id = _seed_template(client) + sub_id = _seed_submission(db, tpl_id) + resp = client.delete( + f"{API_PREFIX}/forms/{sub_id}", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 + + def test_purge_requires_key_when_set(self, client): + resp = client.post(f"{API_PREFIX}/forms/purge?days=30") + assert resp.status_code == 401 + + def test_purge_with_valid_key(self, client, db): + resp = client.post( + f"{API_PREFIX}/forms/purge?days=30", + headers={"X-API-Key": "secret-test-key"}, + ) + assert resp.status_code == 200 diff --git a/tests/test_jobs.py b/tests/test_jobs.py new file mode 100644 index 00000000..5c6e06b5 --- /dev/null +++ b/tests/test_jobs.py @@ -0,0 +1,113 @@ +"""Tests for async job submission and status endpoints.""" + +from unittest.mock import MagicMock, patch + +from app.core.config import API_PREFIX + + +class TestJobEndpoints: + + def _seed_template(self, client): + resp = client.post(f"{API_PREFIX}/templates/create", json={ + "name": "Test Template", + "pdf_path": "test.pdf", + "fields": {"name": "string"}, + }) + return resp.json()["id"] + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_single(self, mock_task, client): + mock_result = MagicMock() + mock_result.id = "celery-task-id-1" + mock_task.delay.return_value = mock_result + + tpl_id = self._seed_template(client) + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "John Doe firefighter", + }) + assert resp.status_code == 200 + data = resp.json() + assert len(data["jobs"]) == 1 + assert data["jobs"][0]["status"] == "queued" + assert "job_id" in data["jobs"][0] + assert data["jobs"][0]["poll_url"].startswith(f"{API_PREFIX}/jobs/") + mock_task.delay.assert_called_once_with(tpl_id, "John Doe firefighter", None) + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_batch(self, mock_task, client): + mock_task.delay.side_effect = [ + MagicMock(id="task-1"), + MagicMock(id="task-2"), + ] + + t1 = self._seed_template(client) + t2 = self._seed_template(client) + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [t1, t2], + "input_text": "batch input", + }) + assert resp.status_code == 200 + jobs = resp.json()["jobs"] + assert len(jobs) == 2 + assert jobs[0]["job_id"] != jobs[1]["job_id"] + assert mock_task.delay.call_count == 2 + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_async_missing_template(self, mock_task, client): + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [9999], + "input_text": "some text", + }) + assert resp.status_code == 404 + mock_task.delay.assert_not_called() + + @patch("app.api.routes.jobs.fill_form_task") + def test_get_job_status(self, mock_task, client): + mock_task.delay.return_value = MagicMock(id="celery-abc") + + tpl_id = self._seed_template(client) + submit_resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "test input", + }) + job_id = submit_resp.json()["jobs"][0]["job_id"] + + resp = client.get(f"{API_PREFIX}/jobs/{job_id}") + assert resp.status_code == 200 + data = resp.json() + assert data["job_id"] == job_id + assert data["job_type"] == "form_generation" + assert data["status"] == "queued" + assert data["progress_percent"] == 0 + + def test_get_job_not_found(self, client): + resp = client.get(f"{API_PREFIX}/jobs/00000000-0000-0000-0000-000000000000") + assert resp.status_code == 404 + + @patch("app.api.routes.jobs.fill_form_task") + def test_submit_with_model_override(self, mock_task, client): + mock_task.delay.return_value = MagicMock(id="celery-xyz") + + tpl_id = self._seed_template(client) + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [tpl_id], + "input_text": "test", + "model": "mistral:latest", + }) + assert resp.status_code == 200 + mock_task.delay.assert_called_once_with(tpl_id, "test", "mistral:latest") + + def test_submit_empty_template_ids(self, client): + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [], + "input_text": "test", + }) + assert resp.status_code == 422 + + def test_submit_empty_input_text(self, client): + resp = client.post(f"{API_PREFIX}/forms/jobs", json={ + "template_ids": [1], + "input_text": "", + }) + assert resp.status_code == 422 diff --git a/tests/test_migrations.py b/tests/test_migrations.py new file mode 100644 index 00000000..25c10dc3 --- /dev/null +++ b/tests/test_migrations.py @@ -0,0 +1,310 @@ +"""Tests for Alembic migration infrastructure. + +Runs migrations against an in-memory SQLite database to verify: +- upgrade to head works +- downgrade to base works +- round-trip (up → down → up) works +- schema matches expected columns and foreign keys +""" + +import pytest +from alembic.config import Config +from sqlalchemy import create_engine, inspect + +from alembic import command + +ALEMBIC_INI = "alembic.ini" + + +@pytest.fixture() +def alembic_engine(): + return create_engine("sqlite://") + + +@pytest.fixture() +def alembic_cfg(alembic_engine): + cfg = Config(ALEMBIC_INI) + cfg.set_main_option("sqlalchemy.url", "sqlite://") + cfg.attributes["connection"] = alembic_engine.connect() + return cfg + + +def test_upgrade_head(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" in tables + assert "formsubmission" in tables + assert "job" in tables + assert "inputs" in tables + assert "extractions" in tables + assert "incidents" in tables + assert "forms" in tables + assert "reports" in tables + assert "alembic_version" in tables + + +def test_downgrade_base(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "base") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" not in tables + assert "formsubmission" not in tables + assert "job" not in tables + + +def test_round_trip(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "-1") + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "template" in tables + assert "formsubmission" in tables + + +def test_template_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("template")} + assert columns == {"id", "name", "fields", "pdf_path", "created_at"} + + +def test_formsubmission_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("formsubmission")} + assert columns == {"id", "template_id", "input_text", "output_pdf_path", "created_at"} + + +def test_formsubmission_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("formsubmission") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "template" + assert fks[0]["referred_columns"] == ["id"] + + +def test_job_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("job")} + assert columns == { + "id", + "job_id", + "celery_task_id", + "job_type", + "template_id", + "input_text", + "status", + "progress_percent", + "result_url", + "error", + "model", + "created_at", + "updated_at", + } + + +def test_job_indexes(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + indexes = {ix["name"]: ix for ix in inspector.get_indexes("job")} + assert indexes["ix_job_job_id"]["unique"] + assert not indexes["ix_job_celery_task_id"]["unique"] + + +def test_job_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("job") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "template" + assert fks[0]["referred_columns"] == ["id"] + + +# --------------------------------------------------------------------------- +# 002 — v1 model tables +# --------------------------------------------------------------------------- + +def test_inputs_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("inputs")} + assert columns == { + "input_id", + "input_type", + "status", + "transcript", + "original_filename", + "audio_duration_seconds", + "character_count", + "word_count", + "station_id", + "responder_badge", + "incident_date_hint", + "error_detail", + "created_at", + "updated_at", + } + + +def test_extractions_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("extractions")} + assert columns == { + "extract_id", + "input_id", + "status", + "started_at", + "completed_at", + "model_used", + "processing_time_seconds", + "incident_contract", + "corrections", + "error_type", + "error_detail", + "created_at", + "updated_at", + } + + +def test_extractions_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("extractions") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "inputs" + assert fks[0]["referred_columns"] == ["input_id"] + + +def test_incidents_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("incidents")} + assert columns == { + "incident_id", + "extract_id", + "incident_number", + "status", + "incident_name", + "incident_type", + "incident_date", + "tags", + "notes", + "created_at", + "updated_at", + "deleted_at", + } + + +def test_incidents_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("incidents") + assert len(fks) == 1 + assert fks[0]["referred_table"] == "extractions" + assert fks[0]["referred_columns"] == ["extract_id"] + + +def test_forms_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("forms")} + assert columns == { + "form_id", + "form_type", + "status", + "extract_id", + "incident_id", + "job_id", + "completed_at", + "pdf_ready", + "json_ready", + "field_mapping_summary", + "pdf_path", + "json_data", + "created_at", + "updated_at", + } + + +def test_forms_fk(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = {fk["referred_table"]: fk for fk in inspector.get_foreign_keys("forms")} + # extract_id → extractions and incident_id → incidents; job_id has NO FK constraint + assert "extractions" in fks + assert fks["extractions"]["referred_columns"] == ["extract_id"] + assert "incidents" in fks + assert fks["incidents"]["referred_columns"] == ["incident_id"] + assert len(fks) == 2 + + +def test_reports_columns(alembic_cfg, alembic_engine): + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + columns = {c["name"] for c in inspector.get_columns("reports")} + assert columns == { + "report_id", + "period_type", + "period_label", + "year", + "month", + "quarter", + "output_format", + "status", + "generated_at", + "summary", + "job_id", + "pdf_path", + "json_data", + "created_at", + "updated_at", + } + + +def test_reports_no_fk(alembic_cfg, alembic_engine): + """job_id on reports is a plain UUID column — no FK constraint until contract Job is resolved.""" + command.upgrade(alembic_cfg, "head") + + inspector = inspect(alembic_engine) + fks = inspector.get_foreign_keys("reports") + assert len(fks) == 0 + + +def test_downgrade_002(alembic_cfg, alembic_engine): + """Downgrade by one step removes only the 002 tables, leaving 001 tables intact.""" + command.upgrade(alembic_cfg, "head") + command.downgrade(alembic_cfg, "-1") + + inspector = inspect(alembic_engine) + tables = inspector.get_table_names() + assert "inputs" not in tables + assert "extractions" not in tables + assert "incidents" not in tables + assert "forms" not in tables + assert "reports" not in tables + assert "template" in tables + assert "formsubmission" in tables + assert "job" in tables diff --git a/src/test/test_model.py b/tests/test_model.py similarity index 100% rename from src/test/test_model.py rename to tests/test_model.py diff --git a/tests/test_v1_input.py b/tests/test_v1_input.py new file mode 100644 index 00000000..a73f0487 --- /dev/null +++ b/tests/test_v1_input.py @@ -0,0 +1,164 @@ +"""Tests for POST /api/v1/input/text and GET /api/v1/input/{input_id}. + +Uses the shared in-memory SQLite engine from conftest.py. No Ollama or +Whisper involvement — text input is fully synchronous. +""" + + +TEXT_URL = "/api/v1/input/text" +INPUT_URL = "/api/v1/input" + +# Narrative that passes all validation: > 20 chars, >= 10 words, << 50,000 chars. +VALID_NARRATIVE = ( + "Responded to a wildfire at Bear Creek Trailhead around 1:45 PM on July 10th. " + "Lightning strike ignited timber litter in mixed conifer and chaparral." +) + + +# --------------------------------------------------------------------------- +# POST /api/v1/input/text +# --------------------------------------------------------------------------- + +class TestSubmitTextInput: + + def test_201_returns_required_fields(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + assert resp.status_code == 201 + body = resp.json() + assert body["status"] == "ready" + assert body["input_type"] == "text" + assert "input_id" in body + assert "created_at" in body + + def test_201_character_and_word_counts_match_narrative(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + assert resp.status_code == 201 + body = resp.json() + assert body["character_count"] == len(VALID_NARRATIVE) + assert body["word_count"] == len(VALID_NARRATIVE.split()) + + def test_201_optional_fields_accepted_and_visible_via_get(self, client): + resp = client.post(TEXT_URL, json={ + "narrative": VALID_NARRATIVE, + "station_id": "STA-045", + "responder_badge": "FD-7842", + "incident_date_hint": "2024-07-10", + }) + assert resp.status_code == 201 + input_id = resp.json()["input_id"] + + get_resp = client.get(f"{INPUT_URL}/{input_id}") + assert get_resp.status_code == 200 + body = get_resp.json() + assert body["station_id"] == "STA-045" + assert body["responder_badge"] == "FD-7842" + assert body["incident_date_hint"] == "2024-07-10" + + def test_narrative_stored_in_transcript_field(self, client): + resp = client.post(TEXT_URL, json={"narrative": VALID_NARRATIVE}) + input_id = resp.json()["input_id"] + get_resp = client.get(f"{INPUT_URL}/{input_id}") + assert get_resp.json()["transcript"] == VALID_NARRATIVE + + def test_413_narrative_over_50000_chars(self, client): + too_long = "x" * 50_001 + resp = client.post(TEXT_URL, json={"narrative": too_long}) + assert resp.status_code == 413 + body = resp.json() + assert body["error_code"] == "NARRATIVE_TOO_LONG" + assert body["detail"]["max_characters"] == 50_000 + assert body["detail"]["received_characters"] == 50_001 + + def test_413_boundary_exactly_50000_chars_is_accepted(self, client): + # Exactly at the limit: should succeed (> 50_000 is rejected, not >=). + # Build a narrative at exactly 50,000 chars that has >= 10 words. + phrase = "fire incident response " # 23 chars + narrative = (phrase * 2175)[:50_000] # 2175*23=50025, sliced to 50000 + resp = client.post(TEXT_URL, json={"narrative": narrative}) + assert resp.status_code == 201 + + def test_422_fewer_than_10_words_matches_request_validation_shape(self, client): + # >= 20 chars so Pydantic minLength passes, but only 6 words. + short = "Fire happened at the scene today here" # 7 words, > 20 chars + resp = client.post(TEXT_URL, json={"narrative": short}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert body["message"] == "Request validation failed" + errs = body["validation_errors"] + assert len(errs) == 1 + assert errs[0]["field"] == "narrative" + assert errs[0]["issue"] == "Must contain at least 10 words" + assert errs[0]["value"] == short + + def test_422_narrative_under_20_chars_triggers_pydantic_validation(self, client): + resp = client.post(TEXT_URL, json={"narrative": "Too short"}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + assert "validation_errors" in body + + def test_422_missing_narrative_field(self, client): + resp = client.post(TEXT_URL, json={}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + + def test_422_empty_string_narrative(self, client): + resp = client.post(TEXT_URL, json={"narrative": ""}) + assert resp.status_code == 422 + body = resp.json() + assert body["error_code"] == "VALIDATION_ERROR" + + +# --------------------------------------------------------------------------- +# GET /api/v1/input/{input_id} +# --------------------------------------------------------------------------- + +class TestGetInput: + + def _create(self, client, **kwargs): + payload = {"narrative": VALID_NARRATIVE, **kwargs} + resp = client.post(TEXT_URL, json=payload) + assert resp.status_code == 201 + return resp.json()["input_id"] + + def test_200_returns_full_record(self, client): + input_id = self._create(client) + resp = client.get(f"{INPUT_URL}/{input_id}") + assert resp.status_code == 200 + body = resp.json() + assert body["input_id"] == input_id + assert body["input_type"] == "text" + assert body["status"] == "ready" + assert body["transcript"] == VALID_NARRATIVE + assert body["character_count"] == len(VALID_NARRATIVE) + assert body["word_count"] == len(VALID_NARRATIVE.split()) + assert body["created_at"] is not None + assert body["updated_at"] is not None + + def test_200_voice_only_fields_are_null_for_text_input(self, client): + input_id = self._create(client) + body = client.get(f"{INPUT_URL}/{input_id}").json() + assert body["original_filename"] is None + assert body["audio_duration_seconds"] is None + assert body["error_detail"] is None + + def test_200_retry_after_is_null_for_ready_input(self, client): + input_id = self._create(client) + body = client.get(f"{INPUT_URL}/{input_id}").json() + assert body.get("retry_after_seconds") is None + + def test_404_unknown_uuid_returns_app_error_envelope(self, client): + fake_id = "00000000-0000-0000-0000-000000000000" + resp = client.get(f"{INPUT_URL}/{fake_id}") + assert resp.status_code == 404 + body = resp.json() + assert body["error_code"] == "INPUT_NOT_FOUND" + assert fake_id in body["message"] + + def test_404_message_contains_the_requested_id(self, client): + target = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + resp = client.get(f"{INPUT_URL}/{target}") + assert resp.status_code == 404 + assert target in resp.json()["message"] diff --git a/tests/test_v1_models.py b/tests/test_v1_models.py new file mode 100644 index 00000000..396f537e --- /dev/null +++ b/tests/test_v1_models.py @@ -0,0 +1,407 @@ +"""Direct SQLModel instantiation tests for the five v1 models. + +Uses the same in-memory SQLite engine and _reset_tables fixture from conftest +(autouse=True) so every test starts with a clean schema. Tests stay at the ORM +layer — no routes, no Celery, no Ollama. +""" + +from datetime import date, datetime, timezone +from uuid import UUID, uuid4 + +import pytest +from sqlmodel import Session, select + +from app.api.schemas.enums import ( + ExtractionStatus, + FormStatus, + FormType, + InputStatus, + InputType, + JobStatus, + OutputFormat, + PeriodType, + ReportStatus, +) +from app.models import Extraction, Form, Incident, Input, Report + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _input(db: Session, **kwargs) -> Input: + defaults = dict(input_type=InputType.text) + row = Input(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + +def _extraction(db: Session, input_id: UUID, **kwargs) -> "Extraction": + row = Extraction(input_id=input_id, **kwargs) + db.add(row) + db.commit() + db.refresh(row) + return row + + +# --------------------------------------------------------------------------- +# Input +# --------------------------------------------------------------------------- + +class TestInputModel: + + def test_defaults_on_create(self, db): + row = _input(db) + assert isinstance(row.input_id, UUID) + assert row.status == InputStatus.queued + assert row.transcript is None + assert row.audio_duration_seconds is None + assert row.character_count is None + assert isinstance(row.created_at, datetime) + assert isinstance(row.updated_at, datetime) + + def test_voice_fields_roundtrip(self, db): + row = _input( + db, + input_type=InputType.voice, + original_filename="call_recording.mp3", + audio_duration_seconds=47.3, + station_id="STA-12", + responder_badge="B-4421", + incident_date_hint=date(2026, 6, 1), + ) + fetched = db.get(Input, row.input_id) + assert fetched.input_type == InputType.voice + assert fetched.original_filename == "call_recording.mp3" + assert fetched.audio_duration_seconds == pytest.approx(47.3) + assert fetched.station_id == "STA-12" + assert fetched.incident_date_hint == date(2026, 6, 1) + + def test_status_transitions_persist(self, db): + row = _input(db) + row.status = InputStatus.transcribing + db.add(row) + db.commit() + fetched = db.get(Input, row.input_id) + assert fetched.status == InputStatus.transcribing + + def test_error_detail_on_failed(self, db): + row = _input(db, status=InputStatus.failed, error_detail="Whisper timeout") + fetched = db.get(Input, row.input_id) + assert fetched.status == InputStatus.failed + assert fetched.error_detail == "Whisper timeout" + + def test_word_and_char_counts(self, db): + row = _input( + db, + status=InputStatus.ready, + transcript="Structure fire at 4th and Main", + character_count=30, + word_count=6, + ) + fetched = db.get(Input, row.input_id) + assert fetched.character_count == 30 + assert fetched.word_count == 6 + + def test_unique_primary_keys(self, db): + a = _input(db) + b = _input(db) + assert a.input_id != b.input_id + + +# --------------------------------------------------------------------------- +# Extraction +# --------------------------------------------------------------------------- + +class TestExtractionModel: + + def test_defaults_on_create(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + assert isinstance(ext.extract_id, UUID) + assert ext.input_id == inp.input_id + assert ext.status == ExtractionStatus.processing + assert ext.incident_contract is None + assert ext.corrections is None + assert ext.started_at is None + + def test_incident_contract_json_roundtrip(self, db): + inp = _input(db) + contract = { + "schema_version": "1.1.0", + "incident": {"name": "Structure Fire Main St", "types": []}, + "location": {"address": "123 Main St", "state": "CA"}, + } + ext = _extraction(db, inp.input_id, incident_contract=contract) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.incident_contract["schema_version"] == "1.1.0" + assert fetched.incident_contract["location"]["state"] == "CA" + + def test_corrections_json_roundtrip(self, db): + inp = _input(db) + corrections = [ + { + "field_path": "incident.name", + "original_value": "Wildland Fire", + "corrected_value": "Structure Fire", + "corrected_at": "2026-06-26T10:00:00Z", + "corrected_by": "dispatcher_01", + } + ] + ext = _extraction(db, inp.input_id, corrections=corrections) + fetched = db.get(Extraction, ext.extract_id) + assert len(fetched.corrections) == 1 + assert fetched.corrections[0]["field_path"] == "incident.name" + + def test_completed_status_fields(self, db): + inp = _input(db) + now = datetime.now(timezone.utc) + ext = _extraction( + db, + inp.input_id, + status=ExtractionStatus.completed, + started_at=now, + completed_at=now, + model_used="llama3:8b", + processing_time_seconds=38.4, + ) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.status == ExtractionStatus.completed + assert fetched.model_used == "llama3:8b" + assert fetched.processing_time_seconds == pytest.approx(38.4) + + def test_failed_status_with_error_fields(self, db): + inp = _input(db) + ext = _extraction( + db, + inp.input_id, + status=ExtractionStatus.failed, + error_type="LLM_TIMEOUT", + error_detail="Ollama did not respond within 120s", + ) + fetched = db.get(Extraction, ext.extract_id) + assert fetched.status == ExtractionStatus.failed + assert fetched.error_type == "LLM_TIMEOUT" + + +# --------------------------------------------------------------------------- +# Incident +# --------------------------------------------------------------------------- + +class TestIncidentModel: + + def _make(self, db, **kwargs): + inp = _input(db) + ext = _extraction(db, inp.input_id) + defaults = dict(extract_id=ext.extract_id) + row = Incident(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.incident_id, UUID) + assert row.status == ReportStatus.draft + assert row.deleted_at is None + assert row.tags is None + + def test_tags_json_roundtrip(self, db): + row = self._make(db, tags=["wildland", "high-priority", "multi-agency"]) + fetched = db.get(Incident, row.incident_id) + assert fetched.tags == ["wildland", "high-priority", "multi-agency"] + + def test_soft_delete(self, db): + row = self._make(db) + assert row.deleted_at is None + ts = datetime.now(timezone.utc) + row.deleted_at = ts + db.add(row) + db.commit() + fetched = db.get(Incident, row.incident_id) + assert fetched.deleted_at is not None + + def test_incident_date_field(self, db): + row = self._make(db, incident_date=date(2026, 5, 15)) + fetched = db.get(Incident, row.incident_id) + assert fetched.incident_date == date(2026, 5, 15) + + def test_all_nullable_fields_default_none(self, db): + row = self._make(db) + assert row.incident_number is None + assert row.incident_name is None + assert row.incident_type is None + assert row.incident_date is None + assert row.notes is None + + +# --------------------------------------------------------------------------- +# Form +# --------------------------------------------------------------------------- + +class TestFormModel: + + def _make(self, db, **kwargs): + inp = _input(db) + ext = _extraction(db, inp.input_id) + defaults = dict( + extract_id=ext.extract_id, + form_type=FormType.nfirs_basic, + ) + row = Form(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.form_id, UUID) + assert row.status == FormStatus.queued + assert row.pdf_ready is False + assert row.json_ready is False + assert row.incident_id is None + assert row.job_id is None + assert row.completed_at is None + + def test_field_mapping_summary_roundtrip(self, db): + summary = { + "total_form_fields": 42, + "fields_filled": 38, + "fields_blank": 4, + "coverage_percent": 90.5, + } + row = self._make(db, field_mapping_summary=summary) + fetched = db.get(Form, row.form_id) + assert fetched.field_mapping_summary["total_form_fields"] == 42 + assert fetched.field_mapping_summary["coverage_percent"] == pytest.approx(90.5) + + def test_json_data_roundtrip(self, db): + agency_json = {"FDID": "CA99901", "INC_NO": "2026-0042", "ALARMS": 2} + row = self._make(db, json_data=agency_json, json_ready=True) + fetched = db.get(Form, row.form_id) + assert fetched.json_ready is True + assert fetched.json_data["FDID"] == "CA99901" + + def test_incident_fk_links_correctly(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + incident = Incident(extract_id=ext.extract_id) + db.add(incident) + db.commit() + db.refresh(incident) + + form = Form( + extract_id=ext.extract_id, + form_type=FormType.neris, + incident_id=incident.incident_id, + ) + db.add(form) + db.commit() + db.refresh(form) + assert form.incident_id == incident.incident_id + + def test_job_id_stored_without_fk(self, db): + """job_id is a plain UUID — can store any UUID without a FK constraint.""" + arbitrary_uuid = uuid4() + row = self._make(db, job_id=arbitrary_uuid) + fetched = db.get(Form, row.form_id) + assert fetched.job_id == arbitrary_uuid + + def test_all_form_types_accepted(self, db): + inp = _input(db) + ext = _extraction(db, inp.input_id) + for ft in FormType: + form = Form(extract_id=ext.extract_id, form_type=ft) + db.add(form) + db.commit() + results = db.exec(select(Form)).all() + stored_types = {f.form_type for f in results} + assert stored_types == set(FormType) + + +# --------------------------------------------------------------------------- +# Report +# --------------------------------------------------------------------------- + +class TestReportModel: + + def _make(self, db, **kwargs): + defaults = dict( + period_type=PeriodType.monthly, + year=2026, + month=6, + output_format=OutputFormat.pdf, + ) + row = Report(**{**defaults, **kwargs}) + db.add(row) + db.commit() + db.refresh(row) + return row + + def test_defaults_on_create(self, db): + row = self._make(db) + assert isinstance(row.report_id, UUID) + assert row.status == JobStatus.queued + assert row.generated_at is None + assert row.summary is None + assert row.pdf_path is None + assert row.json_data is None + assert row.job_id is None + + def test_summary_json_roundtrip(self, db): + summary = { + "total_incidents": 23, + "by_type": {"fire": 10, "ems": 13}, + "forms_generated": 71, + "avg_completeness_score": 88.3, + } + row = self._make(db, summary=summary) + fetched = db.get(Report, row.report_id) + assert fetched.summary["total_incidents"] == 23 + assert fetched.summary["avg_completeness_score"] == pytest.approx(88.3) + + def test_quarterly_report(self, db): + row = self._make( + db, + period_type=PeriodType.quarterly, + year=2026, + month=None, + quarter=2, + output_format=OutputFormat.json, + ) + fetched = db.get(Report, row.report_id) + assert fetched.period_type == PeriodType.quarterly + assert fetched.quarter == 2 + assert fetched.month is None + + def test_annual_report(self, db): + row = self._make( + db, + period_type=PeriodType.annual, + year=2025, + month=None, + quarter=None, + output_format=OutputFormat.both, + ) + fetched = db.get(Report, row.report_id) + assert fetched.period_type == PeriodType.annual + assert fetched.year == 2025 + assert fetched.output_format == OutputFormat.both + + def test_job_id_stored_without_fk(self, db): + arbitrary_uuid = uuid4() + row = self._make(db, job_id=arbitrary_uuid) + fetched = db.get(Report, row.report_id) + assert fetched.job_id == arbitrary_uuid + + def test_status_progression(self, db): + row = self._make(db) + assert row.status == JobStatus.queued + row.status = JobStatus.processing + db.add(row) + db.commit() + fetched = db.get(Report, row.report_id) + assert fetched.status == JobStatus.processing diff --git a/tests/test_v1_system.py b/tests/test_v1_system.py new file mode 100644 index 00000000..d8851213 --- /dev/null +++ b/tests/test_v1_system.py @@ -0,0 +1,311 @@ +"""Tests for GET /api/v1/health, /schema/incident, /schema/incident/versions. + +External dependencies (Ollama, Whisper, disk, DB) are mocked at the route-module +boundary. The health check uses engine.connect() directly (not the get_db +dependency) so we mock system_mod.engine explicitly in every test. +""" + +from unittest.mock import MagicMock + +import pytest +import requests as requests_lib + +import app.api.routes.system as system_mod + +# --------------------------------------------------------------------------- +# Low-level helpers +# --------------------------------------------------------------------------- + +def _mock_engine_healthy(): + """Engine whose connect() succeeds — supports the 'with engine.connect()' pattern.""" + mock = MagicMock() + # MagicMock already supports __enter__/__exit__ automatically + return mock + + +def _mock_engine_down(): + mock = MagicMock() + mock.connect.side_effect = Exception("Connection refused") + return mock + + +def _mock_engine_execute_fails(): + """connect() succeeds (context manager enters) but execute() raises — simulates + a connection acquired then lost, or a query timeout: the more realistic outage path. + """ + mock = MagicMock() + conn = MagicMock() + conn.execute.side_effect = Exception("query failed: timeout") + mock.connect.return_value.__enter__.return_value = conn + return mock + + +def _mock_shutil(free_bytes: int = 120 * 1024 ** 3, raise_oserror: bool = False): + """Return a shutil-shaped object whose disk_usage behaves as configured.""" + mock = MagicMock() + if raise_oserror: + mock.disk_usage.side_effect = OSError("disk error") + else: + usage = MagicMock() + usage.free = free_bytes + mock.disk_usage.return_value = usage + return mock + + +def _make_mock_response(json_data=None, status_code=200): + m = MagicMock() + m.status_code = status_code + m.json.return_value = json_data or {} + m.raise_for_status.return_value = None + return m + + +# --------------------------------------------------------------------------- +# Canned Ollama / Whisper responses +# --------------------------------------------------------------------------- + +_TAGS_RESPONSE = { + "models": [ + {"name": "llama3:8b", "size": 5_000_000_000, "details": {"quantization_level": "Q4_K_M"}}, + {"name": "mistral:7b", "size": 4_000_000_000, "details": {"quantization_level": "Q4_0"}}, + ] +} +_VERSION_RESPONSE = {"version": "0.3.0"} +_PS_RESPONSE = {"models": [{"name": "llama3:8b"}]} +_PS_EMPTY = {"models": []} + + +def _fake_get_all_healthy(url, timeout=None): + """All external services reachable and healthy.""" + if "/api/tags" in url: + return _make_mock_response(_TAGS_RESPONSE) + if "/api/version" in url: + return _make_mock_response(_VERSION_RESPONSE) + if "/api/ps" in url: + return _make_mock_response(_PS_RESPONSE) + if url.endswith("/docs"): + return _make_mock_response() + raise ValueError(f"Unexpected URL in test: {url}") + + +def _fake_get_ollama_down(url, timeout=None): + """Ollama is unreachable; Whisper is healthy.""" + if "/api/tags" in url or "/api/version" in url or "/api/ps" in url: + raise requests_lib.exceptions.ConnectionError("Ollama down") + if url.endswith("/docs"): + return _make_mock_response() + raise ValueError(f"Unexpected URL in test: {url}") + + +def _fake_get_whisper_down(url, timeout=None): + """Whisper is unreachable; Ollama is healthy.""" + if url.endswith("/docs"): + raise requests_lib.exceptions.ConnectionError("Whisper down") + if "/api/tags" in url: + return _make_mock_response(_TAGS_RESPONSE) + if "/api/version" in url: + return _make_mock_response(_VERSION_RESPONSE) + if "/api/ps" in url: + return _make_mock_response(_PS_EMPTY) + raise ValueError(f"Unexpected URL in test: {url}") + + +# --------------------------------------------------------------------------- +# Health endpoint +# --------------------------------------------------------------------------- + +class TestHealthEndpoint: + + def test_all_healthy(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "healthy" + assert body["version"] is not None + assert body["uptime_seconds"] >= 0 + + comps = body["components"] + assert comps["database"]["status"] == "healthy" + assert comps["ollama"]["status"] == "healthy" + assert comps["whisper"]["status"] == "healthy" + assert comps["storage"]["status"] == "healthy" + + def test_ollama_down_returns_200_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_ollama_down) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["ollama"]["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "healthy" + assert body["components"]["whisper"]["status"] == "healthy" + + def test_whisper_down_returns_200_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_whisper_down) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["whisper"]["status"] == "unhealthy" + assert body["components"]["ollama"]["status"] == "healthy" + + def test_db_down_returns_503_unhealthy(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_down()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 503 + body = resp.json() + assert body["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "unhealthy" + assert "Connection refused" in body["components"]["database"]["detail"] + + def test_db_execute_fails_returns_503_unhealthy(self, client, monkeypatch): + """connect() succeeds but execute() raises — the realistic outage path where + a connection is acquired then the query times out or the server drops it. + """ + monkeypatch.setattr(system_mod, "engine", _mock_engine_execute_fails()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + + assert resp.status_code == 503 + body = resp.json() + assert body["status"] == "unhealthy" + assert body["components"]["database"]["status"] == "unhealthy" + assert "query failed" in body["components"]["database"]["detail"] + + def test_current_load_not_fabricated(self, client, monkeypatch): + """current_load must be absent or null — never a made-up value.""" + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama_comp = resp.json()["components"]["ollama"] + assert ollama_comp.get("current_load") is None + + def test_ollama_slow_returns_degraded(self, client, monkeypatch): + """Patch _SLOW_MS to -1 so any measured elapsed time triggers degraded, + without depending on wall-clock timing in CI. + """ + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + monkeypatch.setattr(system_mod, "_SLOW_MS", -1) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["ollama"]["status"] == "degraded" + + def test_models_available_and_loaded_flag(self, client, monkeypatch): + """models_available is built from /api/tags; loaded=True only for models in /api/ps.""" + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + models = resp.json()["components"]["ollama"]["models_available"] + assert len(models) == 2 + llama = next(m for m in models if m["name"] == "llama3:8b") + mistral = next(m for m in models if m["name"] == "mistral:7b") + assert llama["loaded"] is True + assert mistral["loaded"] is False + assert llama["quantization"] == "Q4_K_M" + assert llama["size_gb"] == pytest.approx(4.66, abs=0.1) + + def test_model_loaded_reflects_running_model(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama = resp.json()["components"]["ollama"] + assert ollama["model_loaded"] == "llama3:8b" + + def test_ollama_version_present(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + ollama = resp.json()["components"]["ollama"] + assert ollama["ollama_version"] == "0.3.0" + + def test_storage_disk_free_present(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil(free_bytes=200 * 1024 ** 3)) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + storage = resp.json()["components"]["storage"] + assert storage["status"] == "healthy" + assert storage["disk_free_gb"] == pytest.approx(200.0, abs=0.1) + + def test_storage_unavailable_is_degraded(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil(raise_oserror=True)) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + body = resp.json() + assert body["status"] == "degraded" + assert body["components"]["storage"]["status"] == "unhealthy" + + def test_uptime_seconds_is_non_negative(self, client, monkeypatch): + monkeypatch.setattr(system_mod, "engine", _mock_engine_healthy()) + monkeypatch.setattr(system_mod, "shutil", _mock_shutil()) + monkeypatch.setattr("app.api.routes.system.requests.get", _fake_get_all_healthy) + + resp = client.get("/api/v1/health") + assert resp.status_code == 200 + assert resp.json()["uptime_seconds"] >= 0 + + def test_existing_routes_not_displaced(self, client): + """Sanity: adding v1_router must not break the existing /api/v1/templates endpoint.""" + resp = client.get("/api/v1/templates") + assert resp.status_code == 200 + + +# --------------------------------------------------------------------------- +# Schema stub endpoints +# --------------------------------------------------------------------------- + +class TestSchemaStubEndpoints: + + def test_schema_incident_returns_501(self, client): + resp = client.get("/api/v1/schema/incident") + assert resp.status_code == 501 + body = resp.json() + assert body["error_code"] == "NOT_IMPLEMENTED" + assert "555" in body["message"] + + def test_schema_versions_returns_501(self, client): + resp = client.get("/api/v1/schema/incident/versions") + assert resp.status_code == 501 + body = resp.json() + assert body["error_code"] == "NOT_IMPLEMENTED" diff --git a/tests/test_v1_voice.py b/tests/test_v1_voice.py new file mode 100644 index 00000000..c911db42 --- /dev/null +++ b/tests/test_v1_voice.py @@ -0,0 +1,268 @@ +"""Tests for POST /api/v1/input/voice (endpoint) and transcribe_audio_task (task unit). + +Endpoint tests: dispatch is mocked — no broker, no Whisper, no filesystem writes. +Task tests: called directly with an injected test session and mocked call_whisper_asr. +""" + +import io +from pathlib import Path +from unittest.mock import MagicMock, patch + +from sqlmodel import Session + +from app.api.schemas.enums import InputStatus, InputType +from app.db.repositories import get_input, get_job_by_uuid +from app.models import Input, Job +from app.tasks.transcribe import transcribe_audio_task + +VOICE_URL = "/api/v1/input/voice" + +# Minimal valid WAV bytes (44-byte header + silence) — passes extension check. +_WAV_BYTES = b"RIFF$\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00\x80>\x00\x00\x00}\x00\x00\x02\x00\x10\x00data\x00\x00\x00\x00" + + +# --------------------------------------------------------------------------- +# POST /api/v1/input/voice — endpoint tests (dispatch mocked) +# --------------------------------------------------------------------------- + +class TestSubmitVoiceInput: + + def _post(self, client, filename="memo.wav", content=None, fields=None, whisper_up=True): + mock_result = MagicMock() + mock_result.id = "celery-task-uuid-001" + # Dispatch is owned by InputService.process_voice_upload — patch there. + with patch("app.api.routes.input.check_whisper_available", return_value=whisper_up), \ + patch("app.services.input.transcribe_audio_task") as mock_task, \ + patch("pathlib.Path.write_bytes"): + mock_task.delay.return_value = mock_result + resp = client.post( + VOICE_URL, + files={"audio_file": (filename, io.BytesIO(content or _WAV_BYTES), "audio/wav")}, + data=fields or {}, + ) + return resp, mock_task + + def test_201_returns_required_fields(self, client): + resp, _ = self._post(client) + assert resp.status_code == 201 + body = resp.json() + assert body["status"] == "queued" + assert body["input_type"] == "voice" + assert "input_id" in body + assert "job_id" in body + assert "poll_url" in body + assert "estimated_processing_seconds" in body + + def test_201_poll_url_points_to_input_record(self, client): + resp, _ = self._post(client) + body = resp.json() + assert body["poll_url"] == f"/api/v1/input/{body['input_id']}" + + def test_201_creates_queued_input_row(self, client, db): + resp, _ = self._post(client) + input_id = resp.json()["input_id"] + from uuid import UUID + record = get_input(db, UUID(input_id)) + assert record is not None + assert record.status == InputStatus.queued + assert record.input_type == InputType.voice + assert record.original_filename == "memo.wav" + + def test_201_creates_transcription_job_row(self, client, db): + resp, _ = self._post(client) + job_id = resp.json()["job_id"] + job = get_job_by_uuid(db, job_id) + assert job is not None + assert job.job_type == "transcription" + assert job.status == "queued" + assert job.celery_task_id == "celery-task-uuid-001" + + def test_201_dispatch_called_with_input_id_and_job_id(self, client, db): + resp, mock_task = self._post(client) + body = resp.json() + mock_task.delay.assert_called_once() + call_args = mock_task.delay.call_args[0] + assert call_args[0] == body["input_id"] # input_id_str + assert call_args[2] == body["job_id"] # job_id_str + # call_args[1] is the audio_path (filesystem path string) + assert body["input_id"] in call_args[1] + + def test_201_optional_metadata_stored_on_input(self, client, db): + resp, _ = self._post(client, fields={ + "station_id": "STA-045", + "responder_badge": "FD-7842", + "incident_date_hint": "2024-07-10", + }) + assert resp.status_code == 201 + from uuid import UUID + record = get_input(db, UUID(resp.json()["input_id"])) + assert record.station_id == "STA-045" + assert record.responder_badge == "FD-7842" + assert str(record.incident_date_hint) == "2024-07-10" + + def test_415_unsupported_format_rejected(self, client): + resp, _ = self._post(client, filename="memo.pdf") + assert resp.status_code == 415 + body = resp.json() + assert body["error_code"] == "UNSUPPORTED_FORMAT" + assert "accepted_formats" in body["detail"] + assert "wav" in body["detail"]["accepted_formats"] + + def test_415_no_extension_rejected(self, client): + resp, _ = self._post(client, filename="audiofile") + assert resp.status_code == 415 + assert resp.json()["error_code"] == "UNSUPPORTED_FORMAT" + + def test_413_oversize_file_rejected(self, client, monkeypatch): + monkeypatch.setattr("app.api.routes.input._MAX_AUDIO_BYTES", 5) + resp, _ = self._post(client, content=b"toolong") # 7 bytes > 5 + assert resp.status_code == 413 + body = resp.json() + assert body["error_code"] == "FILE_TOO_LARGE" + assert body["detail"]["max_size_bytes"] == 5 + assert body["detail"]["received_size_bytes"] == 7 + + def test_503_when_whisper_unavailable(self, client): + resp, _ = self._post(client, whisper_up=False) + assert resp.status_code == 503 + body = resp.json() + assert body["error_code"] == "STT_UNAVAILABLE" + + def test_422_missing_audio_file(self, client): + with patch("app.api.routes.input.check_whisper_available", return_value=True): + resp = client.post(VOICE_URL, data={}) + assert resp.status_code == 422 + + +# --------------------------------------------------------------------------- +# transcribe_audio_task — unit tests (no broker, direct call, mocked Whisper) +# --------------------------------------------------------------------------- + +def _seed_input_and_job(session: Session): + """Insert a queued Input and Job, return both refreshed.""" + from datetime import datetime, timezone + now = datetime.now(timezone.utc) + inp = Input( + input_type=InputType.voice, + status=InputStatus.queued, + original_filename="test.wav", + created_at=now, + updated_at=now, + ) + session.add(inp) + job = Job(celery_task_id="celery-test-id", job_type="transcription", status="queued") + session.add(job) + session.commit() + session.refresh(inp) + session.refresh(job) + return inp, job + + +class TestTranscribeAudioTask: + + def _run_task(self, inp, job, test_engine, whisper_side_effect=None, whisper_return="Transcribed text here."): + """ + Call the task function directly against the in-memory DB. + Mocks get_session (so the task uses the test engine) and call_whisper_asr. + Also mocks Path.read_bytes so no filesystem access is needed. + """ + def _gen(): + with Session(test_engine) as s: + yield s + + with patch("app.tasks.transcribe.get_session", side_effect=_gen), \ + patch("app.tasks.transcribe.call_whisper_asr") as mock_whisper, \ + patch.object(Path, "read_bytes", return_value=b"fake_audio_bytes"): + if whisper_side_effect is not None: + mock_whisper.side_effect = whisper_side_effect + else: + mock_whisper.return_value = whisper_return + try: + transcribe_audio_task(str(inp.input_id), "/data/audio/test.wav", job.job_id) + except (ConnectionError, RuntimeError): + pass # expected on failure tests + + def test_success_input_becomes_ready_with_transcript(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_return="Incident at Main St, two casualties.") + + db.refresh(inp) + assert inp.status == InputStatus.ready + assert inp.transcript == "Incident at Main St, two casualties." + assert inp.character_count == len("Incident at Main St, two casualties.") + assert inp.word_count == len(["Incident", "at", "Main", "St,", "two", "casualties."]) + + def test_success_job_becomes_completed_with_result_url(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_return="Incident at Main St.") + + db.refresh(job) + assert job.status == "completed" + assert job.result_url == f"/api/v1/input/{inp.input_id}" + + def test_connection_error_input_failed_error_detail(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=ConnectionError("Cannot reach Whisper")) + + db.refresh(inp) + assert inp.status == InputStatus.failed + assert "Cannot reach Whisper" in inp.error_detail + + def test_connection_error_job_failed_with_stt_unavailable_code(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=ConnectionError("Cannot reach Whisper")) + + db.refresh(job) + assert job.status == "failed" + assert job.error["error_code"] == "STT_UNAVAILABLE" + assert "Cannot reach Whisper" in job.error["message"] + + def test_runtime_error_input_failed_error_detail(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=RuntimeError("HTTP 500 from Whisper")) + + db.refresh(inp) + assert inp.status == InputStatus.failed + assert "HTTP 500 from Whisper" in inp.error_detail + + def test_runtime_error_job_failed_with_transcription_failed_code(self, db, test_engine): + inp, job = _seed_input_and_job(db) + + self._run_task(inp, job, test_engine, whisper_side_effect=RuntimeError("HTTP 500 from Whisper")) + + db.refresh(job) + assert job.status == "failed" + assert job.error["error_code"] == "TRANSCRIPTION_FAILED" + assert "HTTP 500 from Whisper" in job.error["message"] + + def test_success_input_transitions_through_transcribing(self, db, test_engine): + """The task sets transcribing before calling Whisper and ready after.""" + inp, job = _seed_input_and_job(db) + observed_statuses: list[str] = [] + + original_update = __import__( + "app.db.repositories", fromlist=["update_input"] + ).update_input + + def tracking_update(session, input_obj): + observed_statuses.append(input_obj.status.value) + return original_update(session, input_obj) + + def _gen(): + with Session(test_engine) as s: + yield s + + with patch("app.tasks.transcribe.get_session", side_effect=_gen), \ + patch("app.tasks.transcribe.call_whisper_asr", return_value="Hello world text."), \ + patch("app.tasks.transcribe.update_input", side_effect=tracking_update), \ + patch.object(Path, "read_bytes", return_value=b"fake"): + transcribe_audio_task(str(inp.input_id), "/data/audio/test.wav", job.job_id) + + assert "transcribing" in observed_statuses + assert "ready" in observed_statuses + assert observed_statuses.index("transcribing") < observed_statuses.index("ready")