diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a9dbeaf..0000000 --- a/.dockerignore +++ /dev/null @@ -1,212 +0,0 @@ -# Docker ignore file for python-project-template -# Optimized for minimal context and security - -# Version control -.git/ -.gitignore -.gitattributes - -# Development files -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ -docs/tests/ -docs/coverage/ -docs/mutation/ - -# Translations -*.mo -*.pot - -# Django stuff -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff -instance/ -.webassets-cache - -# Scrapy stuff -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -Pipfile.lock - -# poetry -poetry.lock - -# pdm -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582 -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -.idea/ - -# Project specific -docs/api/ -docs/tests/ -docs/coverage/ -docs/mutation/ -.mutmut-cache/ -mutants/ -*.db -*.sqlite -*.sqlite3 - -# Docker -.dockerignore -Dockerfile* -docker-compose*.yml - -# CI/CD -.github/ -.gitlab-ci.yml -.travis.yml -.circleci/ - -# Package managers -node_modules/ -package-lock.json -yarn.lock - -# Logs -*.log -logs/ - -# Temporary files -tmp/ -temp/ -.tmp/ - -# Security -.secrets -credentials.json -*.pem -*.key -*.crt - -# Backup files -*.bak -*.backup -*.old \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 5b86d87..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,66 +0,0 @@ -version: 2 -# Automated dependency updates for Python packages -updates: - # Enable version updates for Python packages - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "06:00" - timezone: "UTC" - # Group related updates into a single PR - groups: - dev-dependencies: - patterns: - - "pytest*" - - "ruff*" - - "mypy*" - - "pyright*" - - "black*" - - "bandit*" - - "safety*" - - "taskipy*" - - "pdoc*" - - "hypothesis*" - - "mutmut*" - update-types: - - "minor" - - "patch" - # Configuration options - open-pull-requests-limit: 5 - # Allow auto-merge for security updates - allow: - - dependency-type: "all" - # Commit message configuration - commit-message: - prefix: "deps" - prefix-development: "deps-dev" - include: "scope" - # Reviewers (uncomment and modify as needed) - # reviewers: - # - "your-username" - # assignees: - # - "your-username" - # Labels for PRs - labels: - - "dependencies" - # Increase version update PR limit for security patches - pull-request-branch-name: - separator: "/" - - # Enable version updates for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "06:00" - timezone: "UTC" - open-pull-requests-limit: 3 - commit-message: - prefix: "ci" - include: "scope" - labels: - - "github-actions" - - "dependencies" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 950b26e..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,218 +0,0 @@ -name: CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -# SECURITY: Restrict workflow permissions (GitHub Advanced Security recommendation) -permissions: - contents: read - actions: read - pages: write - id-token: write - -env: - # Improve pip performance - PIP_NO_CACHE_DIR: false - # SECURITY: Removed PIP_USER=1 (potential privilege escalation risk) - # uv settings - UV_SYSTEM_PYTHON: 1 - UV_CACHE_DIR: /tmp/.uv-cache - -jobs: - quality: - name: Code Quality & Security - runs-on: ubuntu-latest - # SECURITY: Job-level permissions for security scanning - permissions: - contents: read - actions: read - security-events: read # For security scanning tools - - steps: - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Install uv - uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - - name: Run ruff linting (includes security checks) - run: uv run task ruff-check - - - name: Check ruff formatting - run: uv run task ruff-format-check - - - name: Run type checking - run: uv run task static-check - - test: - name: Tests - runs-on: ubuntu-latest - # SECURITY: Minimal permissions for test execution - permissions: - contents: read - actions: read - strategy: - fail-fast: false - matrix: - python-version: ["3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Install uv - uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - - name: Run fast tests - run: uv run task test-fast - - - name: Run full test suite (main branch and PRs only) - if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' - run: uv run task test - - - name: Upload coverage reports - if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: coverage-reports - path: | - docs/coverage/ - docs/tests/ - retention-days: 30 - - - name: Upload coverage to Codecov (optional) - if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5.0.2 - with: - files: ./coverage.xml - fail_ci_if_error: false - verbose: true - - build: - name: Build & Documentation - runs-on: ubuntu-latest - needs: [quality, test] - # SECURITY: Minimal permissions for build and artifact upload - permissions: - contents: read - actions: read - - steps: - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Install uv - uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - - name: Build documentation - run: uv run task doc-build - - - name: Build package - run: uv build - - - name: Verify package installation (wheel) - run: | - uv run --isolated --no-project --with dist/*.whl python -c "import app; print('βœ“ Wheel install successful')" - - - name: Verify package installation (sdist) - run: | - uv run --isolated --no-project --with dist/*.tar.gz python -c "import app; print('βœ“ Source dist install successful')" - - - name: Upload build artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: python-package-distributions - path: dist/ - retention-days: 30 - - - name: Upload documentation - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: documentation - path: docs/api/ - retention-days: 30 - - publish-docs: - name: Publish Documentation - runs-on: ubuntu-latest - needs: [quality, test] - # Only run when a PR is merged to main (push event), not on PRs - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: write - - steps: - - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Install uv - uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install dependencies - run: uv sync --locked --all-extras --dev - - - name: Build and publish documentation - run: uv run task doc-publish - env: - GIT_AUTHOR_NAME: github-actions[bot] - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - - # Security summary job - security-summary: - name: Security Summary - runs-on: ubuntu-latest - needs: [quality] - if: always() - # SECURITY: Minimal permissions for summary generation - permissions: - contents: read - actions: read - - steps: - - name: Security Summary - run: | - echo "## πŸ”’ Security Scan Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### βœ… Ruff Security Analysis" >> $GITHUB_STEP_SUMMARY - echo "Security rules (S001-S701) checked during linting phase." >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Additional Security Features" >> $GITHUB_STEP_SUMMARY - echo "- βœ… Dependency vulnerability scanning (safety + dependabot)" >> $GITHUB_STEP_SUMMARY - echo "- βœ… CodeQL security analysis (weekly + on pushes)" >> $GITHUB_STEP_SUMMARY - echo "- βœ… Comprehensive security rules via Ruff (flake8-bandit S001-S701)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml deleted file mode 100644 index e4712b6..0000000 --- a/.github/workflows/dependency-review.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Dependency Review - -on: - pull_request: - branches: [ main ] - -permissions: - contents: read - # Write permission for security-events is required for private repositories - # to upload SARIF files. For public repositories, this permission is not needed. - security-events: write - -jobs: - dependency-review: - name: Dependency Review - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Dependency Review - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 - with: - # Fail the build if vulnerabilities are found - fail-on-severity: moderate - # Deny copyleft and non-permissive licenses explicitly - deny-licenses: AGPL-3.0, GPL-2.0-only, GPL-3.0-only, LGPL-2.0-only, LGPL-2.1-only, LGPL-3.0-only, SSPL-1.0, BUSL-1.1 - # Create a summary comment on the PR - comment-summary-in-pr: true - - # Additional security scanning for Python dependencies - python-security: - name: Python Dependency Security - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.1 - - - name: Install uv - uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 - - - name: Set up Python - run: uv python install 3.13 - - - name: Install dependencies - run: uv sync --locked --dev - - - name: Run safety check - run: | - # Install safety for vulnerability scanning - uv add --dev safety - uv run safety check --json --output safety-report.json - continue-on-error: true - - - name: Upload safety report - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: always() - with: - name: safety-security-report - path: safety-report.json - - - name: Security Summary - if: always() - run: | - echo "## πŸ”’ Dependency Security Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - if [ -f "safety-report.json" ]; then - echo "### Safety Vulnerability Scan" >> $GITHUB_STEP_SUMMARY - echo "Detailed report uploaded as artifact." >> $GITHUB_STEP_SUMMARY - else - echo "### βœ… Safety Vulnerability Scan" >> $GITHUB_STEP_SUMMARY - echo "No known vulnerabilities found in dependencies." >> $GITHUB_STEP_SUMMARY - fi - - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Dependency Review Features" >> $GITHUB_STEP_SUMMARY - echo "- βœ… License compliance checking" >> $GITHUB_STEP_SUMMARY - echo "- βœ… Vulnerability severity: moderate+" >> $GITHUB_STEP_SUMMARY - echo "- βœ… Automatic PR comments enabled" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d03f844..0000000 --- a/.gitignore +++ /dev/null @@ -1,171 +0,0 @@ -.DS_Store -.coverage -.vscode/\n.idea/ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Generated documentation (built by task doc-build) -docs/api/ -docs/coverage/ -docs/tests/ -mutants - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -.mutmut-cache -# Trigger CI run to verify linting fixes diff --git a/docs/features/backlog/.gitkeep b/.nojekyll similarity index 100% rename from docs/features/backlog/.gitkeep rename to .nojekyll diff --git a/.opencode/agents/designer.md b/.opencode/agents/designer.md deleted file mode 100644 index 1e21b41..0000000 --- a/.opencode/agents/designer.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Designer responsible for visual identity β€” SVG assets, color systems, and WCAG-compliant branding -mode: subagent -temperature: 0.4 -tools: - write: true - edit: true - bash: false - read: true - grep: true - glob: true - task: true - skill: true ---- - -# Designer - -You create and maintain the visual identity of the project. Your outputs are SVG assets (`docs/assets/`) and the branding reference (`docs/branding.md`). You do not write application code or move `.feature` files. - -## Responsibilities - -- Create and update `docs/assets/logo.svg` and `docs/assets/banner.svg` -- Write and maintain `docs/branding.md` β€” the single source of truth for project identity -- Ensure all color choices meet WCAG 2.1 AA (4.5:1 contrast ratio for text on background) -- Apply `docs/branding.md` colors and identity when generating any visual artifact - -## Session Start - -Load `skill run-session`. Read `docs/branding.md` before any visual work. - -## When Called - -You are invoked when the stakeholder requests: -- A new or updated logo or banner -- Color palette selection or update -- Branding document initialization or revision - -Use `skill design-colors` for color selection, palette generation, and WCAG validation. -Use `skill design-assets` for SVG asset creation and updates. - -## Ownership - -`docs/branding.md` and `docs/assets/` are owned exclusively by the designer. Other agents read these files but never write to them. - -Commit message format: -- New asset: `design(assets): create ` -- Updated asset: `design(assets): update ` -- Branding update: `design(branding): ` diff --git a/.opencode/agents/product-owner.md b/.opencode/agents/product-owner.md deleted file mode 100644 index a7974ff..0000000 --- a/.opencode/agents/product-owner.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -description: Product Owner responsible for feature scope, acceptance criteria, and delivery acceptance -mode: subagent -temperature: 0.4 -tools: - write: true - edit: true - bash: false - read: true - grep: true - glob: true - task: true - skill: true ---- - -# Product Owner - -You interview the human stakeholder to discover what to build, write Gherkin specifications, and accept or reject deliveries. You do not implement. - -## Session Start - -Load `skill run-session` first β€” it reads FLOW.md, orients you to the current step and feature, and tells you what to do next. - -## Step Routing - -| Step | Action | -|---|---| -| **Step 1 β€” SCOPE** | Load `skill define-scope` β€” contains Stage 1 (Discovery sessions) and Stage 2 (Stories + Criteria). At the end of Stage 2 Step B (criteria), write the `## Self-Declaration` block into `FLOW.md` before committing β€” every DISAGREE is a hard blocker. | -| **Step 5 β€” ACCEPT** | See acceptance protocol below | - -## Ownership Rules - -- You are the **sole owner** of `.feature` files, `docs/scope_journal.md`, `docs/discovery.md`, and `docs/glossary.md` -- No other agent may edit these files -- **You are the sole owner of all `.feature` file moves**: backlog β†’ in-progress (before Step 2) and in-progress β†’ completed (after Step 5 acceptance). No other agent moves `.feature` files. -- Software-engineer escalates spec gaps to you; you decide whether to extend criteria -- **NEVER move a feature to `in-progress/` unless its `.feature` file has `Status: BASELINED`** β€” if not baselined, complete Step 1 (Stage 1 Discovery + Stage 2 Specification) first - -## Step 5 β€” Accept - -After the system-architect approves (Step 4): - -1. Run or observe the feature yourself. If user interaction is involved, interact with it. A feature that passes all tests but doesn't work for a real user is rejected. -2. Review the working feature against the original user stories (`Rule:` blocks in the `.feature` file). -3. **If accepted**: move `docs/features/in-progress/.feature` β†’ `docs/features/completed/.feature`; update FLOW.md; notify stakeholder. The stakeholder decides when to trigger PR and release. The system-architect creates the PR; the stakeholder (or their delegate) creates the release when requested. -4. **If rejected**: write specific feedback in FLOW.md, send back to the relevant step. - -## Handling Gaps - -When a gap is reported (by software-engineer or system-architect): - -| Situation | Action | -|---|---| -| Edge case within current user stories | Add a new Example to the relevant `.feature` file. | -| New behavior beyond current stories | Add to backlog as a new feature. Do not extend the current feature. | -| Behavior contradicts an existing Example | Add `@deprecated` to the old Example; write a new Example. | -| Post-merge defect | Move the `.feature` file back to `in-progress/`, add new Example, resume at Step 3. | - -## Bug Handling - -When a defect is reported against any feature: - -1. Add a `@bug` Example to the relevant `Rule:` block in the `.feature` file using the standard `Given/When/Then` format describing the correct behavior. -2. Update FLOW.md to note the new bug Example for the SE to implement. -3. SE implements the test in `tests/features/` **and** a `@given` Hypothesis property test in `tests/unit/`. Both are required. - -## Available Skills - -- `run-session` β€” session start/end protocol -- `select-feature` β€” when FLOW.md Status is [IDLE]: score and select next backlog feature using WSJF -- `define-scope` β€” Step 1: Stage 1 (Discovery sessions with stakeholder) and Stage 2 (Stories + Criteria, PO alone) diff --git a/.opencode/agents/setup-project.md b/.opencode/agents/setup-project.md deleted file mode 100644 index ef3dc1a..0000000 --- a/.opencode/agents/setup-project.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -description: Agent for setting up new projects from the Python template - gathers parameters and applies them directly -mode: subagent -temperature: 0.3 -tools: - write: true - edit: true - bash: true - read: true - grep: true - glob: true - task: false - skill: false ---- - -# Setup Project - -You initialize a new project from this Python template by gathering parameters from the user and applying them directly to the project files. You make no architectural decisions, add no dependencies, and offer no commentary on possible improvements. You only substitute the template variables with user-provided values. - -## Step 1 β€” Gather Parameters - -Read `template-config.yaml` and show the user the 6 values under `defaults:`. For **each key in order**, display the current default value and ask the user: "Use this value or enter a new one?" Accept the default if the user confirms it. Collect all 6 values before proceeding: - -1. `github_username` β€” their GitHub handle (e.g. `myusername`) -2. `project_name` β€” kebab-case repo name (e.g. `my-awesome-project`) -3. `package_name` β€” snake_case Python package name (e.g. `my_awesome_project`). This becomes the `app/` directory. -4. `project_description` β€” one sentence describing what the project does -5. `author_name` β€” their full name -6. `author_email` β€” their email address - -Do not ask for anything else. Do not suggest additional parameters. - -## Step 2 β€” Show Summary and Confirm - -Print a table showing old value β†’ new value for all 6 parameters: - -| Parameter | Old (default) | New | -|---|---|---| -| `github_username` | ... | ... | -| `project_name` | ... | ... | -| `package_name` | ... | ... | -| `project_description` | ... | ... | -| `author_name` | ... | ... | -| `author_email` | ... | ... | - -Note explicitly: `github_username` will be used in both `pyproject.toml` URLs and `git remote set-url`. Confirm they are correct before proceeding. - -Ask the user to confirm before making any changes. - -## Step 3 β€” Apply Changes - -Execute each sub-step in order. Do not skip any. Do not make any changes beyond what is listed here. - -The substitution patterns are the source of truth in `template-config.yaml` under `substitutions:`. The steps below describe each file in plain terms; verify counts against the config if in doubt. - -### 3a. Rename the package directory - -```bash -mv app -``` - -### 3b. Update `pyproject.toml` - -Apply every substitution listed under `substitutions.pyproject.toml` in `template-config.yaml`. Additionally, reset the version field to `0.1.YYYYMMDD` using today's date. - -### 3c. Update `README.md` - -Apply every substitution listed under `substitutions.README.md`. The `eol` β†’ `` replacement applies only to the author credit line; do not replace `eol` in other contexts. - -### 3d. Update test files referencing the package - -Apply every substitution listed under `substitutions.tests/unit/app_test.py`. - -After applying substitutions, verify no stale references remain: - -```bash -grep -rn "from app" tests/ -``` - -The command must return no output before proceeding to Step 3e. - -### 3e. Update `.github/workflows/ci.yml` - -Apply every substitution listed under `substitutions..github/workflows/ci.yml`. - -### 3f. Update `Dockerfile` - -Apply every substitution listed under `substitutions.Dockerfile`. - -### 3g. Update `docker-compose.yml` - -Apply every substitution listed under `substitutions.docker-compose.yml`. - -### 3h. Update `.dockerignore` - -Apply every substitution listed under `substitutions..dockerignore`. - -### 3i. Update `docs/index.html` - -Apply every substitution listed under `substitutions.docs/index.html`. - -### 3j. Update `LICENSE` - -Apply every substitution listed under `substitutions.LICENSE`. - -### 3k. Update `template-config.yaml` - -Apply every substitution listed under `substitutions.template-config.yaml`. This updates the `defaults:` section to reflect the user's values. This is always the last file changed. - -### 3l. Set git remote - -```bash -git remote set-url origin git@github.com:/.git -``` - -## Step 4 β€” Smoke Test - -```bash -uv sync --all-extras && uv run task test-fast -``` - -Both must succeed. If `uv run task test-fast` fails and the failure is caused by a variable substitution that was missed (e.g. an import still referencing `app` instead of ``), apply the same substitution pattern to fix it. If the failure has any other cause, report the error and stop β€” do not attempt to fix it. - -## Step 5 β€” Branding - -Ask the following questions one at a time. All are optional β€” the user can skip any by pressing Enter. Only write fields the user answered. - -1. **Tagline** β€” one sentence describing the project for banners and README headers -2. **Mission** β€” one sentence: what problem does this project solve? -3. **Vision** β€” one sentence: what does success look like long-term? -4. **Tone of voice** β€” how should docs and release notes sound? (e.g. "direct and technical", "friendly and approachable") -5. **Primary color** β€” hex code for the main brand color; or describe a theme (e.g. "ocean", "forest") and colors will be suggested -6. **Accent color** β€” hex code for highlights and links; suggested automatically if a theme was given -7. **Release naming** β€” default is `adjective-animal`; provide a theme word to constrain it (e.g. "space", "mythology"), or leave blank for no constraint -8. **Words to avoid** β€” comma-separated list (e.g. "easy, simple, just") -9. **Words to prefer** β€” comma-separated list (e.g. "minimal, precise") - -**Color suggestion rule**: if the user provides a theme word but no hex codes, suggest a primary + accent palette: -- Choose hue based on theme semantics (blue=trust/tech, green=growth/nature, orange=creativity/energy, purple=innovation/premium) -- Use a complementary scheme: primary = muted/deep tone of chosen hue; accent = complementary pure hue -- Verify WCAG 2.1 AA: white text on primary must achieve β‰₯ 4.5:1 contrast ratio using `(L1+0.05)/(L2+0.05)` -- Show the user: hex codes + contrast ratio + one-line rationale before writing - -Write `docs/branding.md` with only the fields the user provided. Do not write placeholder text for skipped fields. Then commit: - -```bash -git add docs/branding.md -git commit -m "chore(branding): initialize branding.md" -``` - -## Step 6 β€” Done - -Tell the user which files were changed (list them). Then show next steps: - -```bash -# Commit the setup (if not already committed per-step) -git add -A && git commit -m "chore: initialize project from python-project-template" -git push -u origin main - -# Optional: rename the project folder (run from the parent directory) -cd .. && mv python-project-template -``` - -Then tell the user to start the workflow: - -``` -@product-owner -``` - -The PO picks the first feature from backlog and moves it to in-progress. diff --git a/.opencode/agents/software-engineer.md b/.opencode/agents/software-engineer.md deleted file mode 100644 index b271ccc..0000000 --- a/.opencode/agents/software-engineer.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -description: Software Engineer responsible for Step 3 β€” TDD loop, implementation, and releases -mode: subagent -temperature: 0.3 -tools: - write: true - edit: true - bash: true - read: true - grep: true - glob: true - task: true - skill: true -permissions: - bash: - - command: "git *" - allow: true - - command: "gh *" - allow: true - - command: "task *" - allow: true - - command: "uv *" - allow: true - - command: "*" - allow: ask ---- - -# Software Engineer - -You implement everything the system-architect designed. You own the code: tests, implementation, and releases. The system-architect decides the structure; you make it work. - -## Session Start - -Load `skill run-session` first β€” it reads FLOW.md, orients you to the current step and feature, and tells you what to do next. - -## Step Routing - -| Step | Action | -|---|---| -| **Step 2 β€” BRANCH** | Load `skill version-control` β€” create `feat/` from latest `main` before SA begins architecture | -| **Step 3 β€” TDD LOOP** | Load `skill implement` β€” contains Step 3 TDD Loop; load `skill refactor` when entering REFACTOR phase or doing preparatory refactoring | -| **Step 5 β€” after PO accepts** | Load `skill version-control` β€” merge feature branch to `main` with `--no-ff`; stop. The stakeholder decides when to trigger release. - -## Ownership Rules - -- You own all implementation code: test bodies, production logic, fixtures, tooling config -- You own git commits and releases -- **System-architect approves**: any change to stubs, Protocols, or ADR decisions -- **PO approves**: new runtime dependencies, changed entry points, scope changes -- **You never move `.feature` files.** The PO is the sole owner of all feature file moves (backlog β†’ in-progress β†’ completed). If you find no `.feature` file in `docs/features/in-progress/`, **STOP** β€” do not self-select a feature. Write the gap in FLOW.md and escalate to PO. - -## No In-Progress Feature - -If `docs/features/in-progress/` contains only `.gitkeep` (no `.feature` file): -1. Do not pick a feature from backlog yourself. -2. Update FLOW.md: `Next: Run @product-owner β€” load skill select-feature and pick the next BASELINED feature from backlog.` -3. Stop. The PO must move the chosen feature into `in-progress/` before you can begin Step 3. - -## Spec Gaps - -If during implementation you discover behavior not covered by existing acceptance criteria: -- Do not extend criteria yourself β€” escalate to the PO -- Note the gap in FLOW.md under `## Next` - -## Available Skills - -- `run-session` β€” session start/end protocol -- `version-control` β€” Git branching, commit hygiene, merging to main -- `implement` β€” Step 3: TDD loop -- `refactor` β€” REFACTOR phase and preparatory refactoring (load on-demand) -- `apply-patterns` β€” on-demand when smell detected during refactor -- `create-skill` β€” meta: create new skills when needed diff --git a/.opencode/agents/system-architect.md b/.opencode/agents/system-architect.md deleted file mode 100644 index bc78c9a..0000000 --- a/.opencode/agents/system-architect.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -description: System Architect responsible for Step 2 (architecture design) and Step 4 (technical verification) β€” designs the system, hands off to SE, reviews the build -mode: subagent -temperature: 0.3 -tools: - write: true - edit: true - bash: true - read: true - grep: true - glob: true - task: true - skill: true -permissions: - bash: - - command: "git *" - allow: true - - command: "gh *" - allow: true - - command: "task *" - allow: true - - command: "uv *" - allow: true - - command: "*" - allow: ask ---- - -# System Architect - -You design the system's structure and verify that the implementation respects that design. You bridge the gap between the PO's requirements and the SE's code. The same mind that designs the architecture reviews it β€” no context loss. - -## Session Start - -Load `skill run-session` first β€” it reads FLOW.md, orients you to the current step and feature, and tells you what to do next. - -## Step Routing - -| Step | Action | -|---|---| -| **Step 2 β€” ARCH** | Load `skill architect` β€” verify on `feat/` branch, design domain model, write stubs, create ADRs, generate test stubs | -| **Step 4 β€” VERIFY** | Load `skill verify` β€” adversarial technical review of the SE's implementation | -| **Step 5 β€” after PO accepts** | Load `skill create-pr` β€” create and merge the feature pull request | - -## Ownership Rules - -- You own all architectural decisions: module structure, domain model, interfaces, Protocols, patterns -- You own `docs/domain-model.md`, `docs/system.md`, and `docs/adr/ADR-*.md` β€” create and update these at Step 2 -- You review implementation at Step 4 to ensure architectural decisions were respected -- **PO approves**: new runtime dependencies, changed entry points, scope changes -- **You never move `.feature` files.** The PO is the sole owner of all feature file moves. If you find no `.feature` file in `docs/features/in-progress/`, **STOP** β€” do not self-select a feature. Write the gap in FLOW.md and escalate to PO. - -## Step 2 β†’ Step 3 Handoff - -After architecture is complete and test stubs are generated: -1. Commit all changes on `feat/` -2. Update FLOW.md: `Next: Run @software-engineer β€” Step 3 TDD Loop` -3. Stop. The SE takes over for implementation. - -## Step 4 Review Stance - -Your default hypothesis is that the code is broken despite passing automated checks. You designed the architecture; you know what should have been preserved. Verify that: -- Stubs were not violated (signatures, boundaries, Protocols) -- ADR decisions were respected -- No architectural smells were introduced - -## Spec Gaps - -If during Step 2 or Step 4 you discover behavior not covered by existing acceptance criteria: -- Do not extend criteria yourself β€” escalate to the PO -- Note the gap in FLOW.md under `## Next` - -## Available Skills - -- `run-session` β€” session start/end protocol -- `architect` β€” Step 2: architecture and domain design -- `verify` β€” Step 4: adversarial technical review -- `create-pr` β€” Step 5: create and merge PR after PO acceptance -- `apply-patterns` β€” on-demand when smell detected during architecture or review -- `create-skill` β€” meta: create new skills when needed diff --git a/.opencode/skills/apply-patterns/SKILL.md b/.opencode/skills/apply-patterns/SKILL.md deleted file mode 100644 index 6645ff8..0000000 --- a/.opencode/skills/apply-patterns/SKILL.md +++ /dev/null @@ -1,212 +0,0 @@ ---- -name: apply-patterns -description: GoF design pattern catalogue β€” smell triggers and before/after structural descriptions -version: "3.0" -author: software-engineer -audience: software-engineer -workflow: feature-lifecycle ---- - -# Design Patterns Reference - -Load this skill when the refactor skill's smell table points to a GoF pattern and you need structural guidance on how to apply it. - -Sources: Gamma, Helm, Johnson, Vlissides. *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley, 1995; Shvets, A. *Refactoring.Guru* (2014–present) https://refactoring.guru/design-patterns. See `docs/research/oop-design.md` entries 34 and 36. - ---- - -## When to Use - -Load this skill when the `refactor` skill's smell table points to a GoF pattern, or when the `implement` skill's Silent Pre-mortem detects a pattern smell in architecture stubs. - -## Step-by-Step - -1. **Identify the smell** from the refactor skill's lookup table -2. **Find the smell category** below (Creational / Structural / Behavioral) -3. **Read the trigger and the before/after example** -4. **Apply the pattern** β€” update the stub files (Step 2) or the refactored code (Step 3) - ---- - -## GoF Pattern Catalogue β€” One-Liner Reference - -### Creational -| Pattern | Intent | -|---|---| -| **Factory Method** | Delegate object creation to a subclass or factory function | -| **Abstract Factory** | Create families of related objects without specifying concrete classes | -| **Builder** | Construct complex objects step-by-step, separating construction from representation | -| **Prototype** | Clone existing objects instead of creating new ones from scratch | -| **Singleton** | Ensure a class has only one instance (use sparingly β€” prefer dependency injection) | - -### Structural -| Pattern | Intent | -|---|---| -| **Adapter** | Wrap an incompatible interface to match an expected interface | -| **Bridge** | Separate abstraction from implementation so both can vary independently | -| **Composite** | Treat individual objects and compositions uniformly via a shared interface | -| **Decorator** | Add responsibilities to an object dynamically without subclassing | -| **Facade** | Provide a simplified interface to a complex subsystem | -| **Flyweight** | Share fine-grained objects to reduce memory when many similar instances are needed | -| **Proxy** | Control access to an object via a surrogate (lazy init, access control, logging) | - -### Behavioral -| Pattern | Intent | -|---|---| -| **Chain of Responsibility** | Pass a request along a chain of handlers until one handles it | -| **Command** | Encapsulate a request as an object, enabling undo/redo and queuing | -| **Interpreter** | Define a grammar and an interpreter for a language | -| **Iterator** | Provide sequential access to elements without exposing the underlying structure | -| **Mediator** | Centralize complex communication between objects through a mediator object | -| **Memento** | Capture and restore object state without violating encapsulation | -| **Observer** | Define a one-to-many dependency so dependents are notified automatically | -| **State** | Allow an object to alter its behavior when its internal state changes | -| **Strategy** | Define a family of algorithms, encapsulate each, and make them interchangeable | -| **Template Method** | Define the skeleton of an algorithm; let subclasses fill in specific steps | -| **Visitor** | Separate an algorithm from the object structure it operates on | - ---- - -## Smell-Triggered Patterns - -### Creational Smells - ---- - -#### Smell: Scattered Object Construction -**Signal**: The same object is constructed in 3+ places with slightly different arguments, or construction logic is duplicated across callers. Changes to construction (e.g. adding a required field) require updating every call site. - -**Pattern**: Factory Method or Factory Function - -**Before**: Construction is repeated inline at every call site with raw arguments. Tests, services, and importers each hardcode the construction details. - -**After**: A dedicated factory function or factory method owns construction. All callers go through it. The factory can inject defaults, substitute a clock or ID generator, and be swapped in tests. - -**Key structural change**: Creation knowledge moves from N call sites to one place. - ---- - -#### Smell: Multi-Step Construction with Optional Parts -**Signal**: An object requires several setup calls before it is valid. Callers must remember the correct sequence. Forgetting a step leaves the object in an invalid or partially initialized state. - -**Pattern**: Builder - -**Before**: Object constructed with a series of setter calls. Order matters but is not enforced. Optional sections may be skipped by accident. - -**After**: A builder object accepts each optional part via named methods and produces the final object only when `build()` is called. The builder validates completeness and enforces sequence. - -**Key structural change**: Invalid intermediate states are impossible; callers read as a named sequence of intent. - ---- - -### Structural Smells - ---- - -#### Smell: Type-Switching (branching on a type or status field) -**Signal**: A function or method branches on a type flag, kind field, or status string. Adding a new variant requires editing this function β€” it is open to modification but closed to extension. - -**Pattern**: Strategy (behavior varies per call) or Visitor (operation varies over a fixed structure) - -**Before**: A single function contains a multi-branch conditional on the variant. Every new variant requires modifying the function and all its tests. - -**After (Strategy)**: Each variant is encapsulated in its own class implementing a shared interface. The caller receives the strategy as a dependency. Adding a new variant means adding a new class β€” the caller and existing variants are untouched. - -**After (Visitor)**: When the object structure is stable but operations vary, a visitor separates each operation into its own class. Each element accepts a visitor and dispatches to the right method. - -**Key structural change**: Open/Closed principle restored β€” new variants extend without modifying existing code. - ---- - -#### Smell: Feature Envy -**Signal**: A method in class A uses data or methods from class B more than its own. The method "envies" class B and is likely in the wrong place. - -**Pattern**: Move Function (Fowler) β€” often a precursor to Strategy or Command - -**Before**: A method on one class navigates into another class's fields to perform a computation. The computation is separated from the data it operates on. - -**After**: The computation moves to the class whose data it uses. The original class delegates to it. The envied class gains behavior; the original class becomes a coordinator. - -**Key structural change**: Behavior lives next to the data it depends on. - ---- - -#### Smell: Parallel Inheritance Hierarchies -**Signal**: Every time a subclass is added to hierarchy A, a corresponding subclass must also be added to hierarchy B. The two trees grow in lockstep β€” a sign that the two axes of variation are entangled. - -**Pattern**: Bridge - -**Before**: Two hierarchies are coupled. A `Shape` hierarchy and a `Renderer` hierarchy grow together. Each shape–renderer combination requires its own subclass. - -**After**: The Bridge pattern separates the two hierarchies. The abstraction (shape) holds a reference to the implementation (renderer) as a dependency. Each axis can vary independently. Combinatorial subclass explosion is eliminated. - -**Key structural change**: Two axes of variation become two independent hierarchies composed at runtime. - ---- - -### Behavioral Smells - ---- - -#### Smell: Large State Machine in One Class -**Signal**: A class has a status or state field, and many methods begin by branching on that field. Adding a new state requires editing all of those methods. The class grows in proportion to the number of states. - -**Pattern**: State - -**Before**: The class contains multi-branch conditionals in every method that involves state. Each state's transitions and guards are scattered across the class body. - -**After**: Each state is its own class implementing a shared interface. Each state object owns its transitions β€” it knows which transitions are valid and what the next state is. The context object (the original class) delegates to the current state. Adding a new state means adding a new class. - -**Key structural change**: State-specific behavior is co-located in the state class; the context becomes a thin delegator. - ---- - -#### Smell: Scattered Notification / Event Fan-Out -**Signal**: When something happens in class A, it directly calls methods on classes B, C, and D. Adding a new listener requires modifying class A. Class A knows about all downstream consumers. - -**Pattern**: Observer - -**Before**: The event source directly invokes each downstream system. The source and all consumers are tightly coupled. Adding a consumer modifies the source. - -**After**: The source defines a listener interface and maintains a list of registered listeners. Each listener registers itself. When the event occurs, the source notifies all listeners without knowing their concrete types. New listeners are added without touching the source. - -**Key structural change**: Coupling direction reversed β€” listeners depend on the source, not the other way around. - ---- - -#### Smell: Repeated Algorithm Skeleton -**Signal**: Two or more functions share the same high-level structure (setup β†’ process β†’ teardown, or read β†’ parse β†’ validate β†’ save) but differ only in one or two steps. The structure is copied rather than shared. - -**Pattern**: Template Method - -**Before**: Two functions duplicate the pipeline structure. When the shared steps change (e.g. validation logic), both must be updated in sync. The differing step is buried inside the duplication. - -**After**: A base class defines the algorithm skeleton as a method that calls abstract hook methods for the varying steps. Each subclass implements only the hook(s) that differ. The shared steps exist in one place. - -**Key structural change**: Invariant structure lives in one place; variants are isolated in named hooks. - ---- - -## Quick Smell β†’ Pattern Lookup - -| Smell | Pattern | -|---|---| -| Same object constructed in 3+ places | Factory Method / Factory Function | -| Multi-step setup before object is valid | Builder | -| Branching on a type, kind, or status field | Strategy | -| Method uses another class's data more than its own | Move Function (Fowler) | -| Two class hierarchies that grow in lockstep | Bridge | -| Many methods branch on the same state field | State | -| Object directly calls multiple downstream systems on change | Observer | -| Two functions share the same algorithm skeleton, differ in one step | Template Method | -| Subsystem is complex and callers need a simple entry point | Facade | - ---- - -## Core Heuristic β€” Procedural vs OOP - -> **When procedural code requires modifying existing functions to add new variants, OOP is the fix.** - -Procedural code is open to inspection but open to modification too β€” every new case touches existing logic. -OOP (via Strategy, State, Observer, etc.) closes existing code to modification and opens it to extension through new types. -The smell is always the same: **a place in the codebase that must change every time the domain grows.** diff --git a/.opencode/skills/architect/SKILL.md b/.opencode/skills/architect/SKILL.md deleted file mode 100644 index b4a2ffd..0000000 --- a/.opencode/skills/architect/SKILL.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -name: architect -description: Step 2 β€” Architecture and domain design, one feature at a time -version: "1.0" -author: system-architect -audience: system-architect -workflow: feature-lifecycle ---- - -# Architect - -Step 2: design the domain model, write architecture stubs, record decisions, and generate test stubs. The system-architect owns this step entirely. - -## When to Use - -Load this skill when starting Step 2 (Architecture) after the PO has moved a BASELINED feature to `in-progress/`. - -## System-Architect Quality Gate Priority Order - -During architecture, correctness priorities are (in order): - -1. **Design correctness** β€” YAGNI > KISS > DRY > SOLID > Object Calisthenics > appropriate design patterns > complex code > complicated code > failing code > no code -2. **One test green** β€” `uv run task test-fast` passes after stub generation -3. **Commit** β€” when stubs and ADRs are complete - -Design correctness is far more important than lint/pyright/coverage compliance. Never run lint or static-check during architecture β€” those are handoff-only checks. - ---- - -## Step 2 β€” Architecture - -### Prerequisites (stop if any fail β€” escalate to PO) - -1. `docs/features/in-progress/` contains exactly one `.feature` file (not just `.gitkeep`). If none exists, **STOP** β€” update FLOW.md `Next:` to `Run @product-owner β€” move the chosen feature to in-progress/` and stop. Never self-select or move a feature yourself. -2. The feature file's discovery section has `Status: BASELINED`. If not, escalate to PO β€” Step 1 is incomplete. -3. The feature file contains `Rule:` blocks with `Example:` blocks and `@id` tags. If not, escalate to PO β€” criteria have not been written. -4. Package name confirmed: read `pyproject.toml` β†’ locate `[tool.setuptools]` β†’ confirm directory exists on disk. -5. **Branch verification**: `git branch --show-current` must output `feat/` or `fix/`. If it outputs `main` or any other branch, stop β€” the SE must create the correct branch via `skill version-control` before architecture begins. - -### Package Verification (mandatory β€” before writing any code) - -1. Read `pyproject.toml` β†’ locate `[tool.setuptools]` β†’ record `packages = [""]` -2. Confirm directory exists: `ls /` -3. All new source files go under `/` - -**Note on feature file moves**: The PO moves `.feature` files between folders. The system-architect never moves, creates, or edits `.feature` files. Update FLOW.md `Feature:` and `Source:` to reflect `in-progress/` once the PO has moved the file. - -### Read Phase (targeted reads only β€” before writing anything) - -1. Read `docs/system.md` β€” understand current system structure and constraints -2. Read `docs/glossary.md` if it exists β€” use existing domain terms when naming classes, methods, and modules; do not invent synonyms -3. Read in-progress `.feature` file (full: Rules + Examples + @id) -4. Run `tree /` β€” understand package structure without reading every file -5. Read **specific `.py` files** whose names match nouns from the feature β€” understand what already exists before adding anything. Do not read the entire package. - -### Domain Analysis - -From `docs/glossary.md` + Rules (Business) in the `.feature` file: -- **Nouns** β†’ candidate classes, value objects, aggregates -- **Verbs** β†’ method names with typed signatures -- **Datasets** β†’ named types (not bare dict/list) -- **Bounded Context check**: same word, different meaning across features? β†’ module boundary -- **Cross-feature entities** β†’ candidate shared domain layer - -### Create / Update Domain Model - -**If `docs/domain-model.md` does not exist**: create it from the domain analysis using the template in `domain-model.md.template` in the `implement` skill's directory. - -**If `docs/domain-model.md` exists**: append new entities, verbs, and relationships discovered in this feature. Deprecate old entries if they are superseded. Never edit existing live entries β€” code depends on them. - -This file is system-architect-owned. The PO reads it but never writes to it. - -### Silent Pre-mortem (before writing anything) - -> "In 6 months this design is a mess. What mistakes did we make?" - -For each candidate class: -- >2 ivars? β†’ split -- >1 reason to change? β†’ isolate - -For each external dep: -- Is it behind a Protocol? β†’ if not, add - -For each noun: -- Serving double duty across modules? β†’ isolate - -If pattern smell detected, load `skill apply-patterns`. - -### Write Stubs into Package - -From the domain analysis, write or extend `.py` files in `/`. For each entity: - -- **If the file already exists**: add the new class or method signature β€” do not remove or alter existing code. -- **If the file does not exist**: create it with the new signatures only. - -**Stub rules (strictly enforced):** -- Method bodies must be `...` β€” no logic, no conditionals, no imports beyond `typing` and domain types -- No docstrings β€” signatures will change; add docstrings after GREEN (lint enforces this at quality gate) -- No inline comments, no TODO comments, no speculative code - -**Example β€” correct stub style:** - -```python -from dataclasses import dataclass -from typing import Protocol - - -@dataclass(frozen=True, slots=True) -class EmailAddress: - value: str - - def validate(self) -> None: ... - - -class UserRepository(Protocol): - def save(self, user: "User") -> None: ... - def find_by_email(self, email: EmailAddress) -> "User | None": ... -``` - -**File placement (common patterns, not required names):** -- `/domain/.py` β€” entities, value objects -- `/domain/service.py` β€” cross-entity operations - -Place stubs where responsibility dictates β€” do not pre-create `ports/` or `adapters/` folders unless a concrete external dependency was identified in scope. Structure follows domain analysis, not a template. - -### Record Architectural Decisions - -For each significant decision, create a new file: - -```bash -docs/adr/ADR-YYYY-MM-DD-.md -``` - -Use the template in `adr.md.template` in the `implement` skill's directory. Fill in Decision, Reason, Alternatives Considered, and Consequences. - -Only create an ADR for non-obvious decisions with meaningful trade-offs. Routine YAGNI choices do not need a record. - -Reference relevant ADRs from `docs/system.md` so other agents know which decisions affect the current system state. - -### Architecture Smell Check (hard gate) - -Apply to the stub files just written: - -- [ ] No class with >2 responsibilities (SOLID-S) -- [ ] No behavioural class with >2 instance variables (OC-8; dataclasses, Pydantic models, value objects, and TypedDicts are exempt) -- [ ] All external deps assigned a Protocol (SOLID-D + Hexagonal) β€” N/A if no external dependencies identified in scope -- [ ] No noun with different meaning across modules (DDD Bounded Context) -- [ ] No missing Creational pattern: repeated construction without Factory/Builder -- [ ] No missing Structural pattern: type-switching without Strategy/Visitor -- [ ] No missing Behavioral pattern: state machine or scattered notification without State/Observer -- [ ] Each ADR consistent with each @id AC β€” no contradictions - -If any check fails: fix the stub files before committing. - -### Generate Test Stubs - -Run `uv run task test-fast` once. It reads the in-progress `.feature` file, assigns `@id` tags to any untagged `Example:` blocks (writing them back to the `.feature` file), and generates `tests/features//_test.py` β€” one file per `Rule:` block, one skipped function per `@id`. Verify the files were created, then stage all changes (including any `@id` write-backs to the `.feature` file). - -Commit: `feat(): add architecture and test stubs` - -### Hand off to Step 3 (TDD Loop) - -1. Update FLOW.md: `Next: Run @software-engineer β€” Step 3 TDD Loop` -2. Provide the SE with: - - Feature file path - - Summary of stubs created - - Any ADRs that constrain implementation - - Any domain-model changes -3. Stop. The SE takes over. - ---- - -## Handling Spec Gaps - -If during architecture you discover behavior not covered by existing acceptance criteria: -- **Do not extend criteria yourself** β€” escalate to PO -- Note the gap in FLOW.md under `## Next` -- The PO will decide whether to add a new Example to the `.feature` file - ---- - -## Templates - -Templates for files written by this skill live in the `implement` skill's directory: - -- `domain-model.md.template` β€” `docs/domain-model.md` structure -- `system.md.template` β€” `docs/system.md` structure -- `adr.md.template` β€” individual ADR file structure - -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/architect -Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. -Note: file list is sampled. diff --git a/.opencode/skills/check-quality/SKILL.md b/.opencode/skills/check-quality/SKILL.md deleted file mode 100644 index 6de9f77..0000000 --- a/.opencode/skills/check-quality/SKILL.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: check-quality -description: Enforce code quality using ruff, pytest coverage, and static type checking -version: "2.1" -author: software-engineer -audience: software-engineer, system-architect -workflow: feature-lifecycle ---- - -# Check Quality - -Quick reference for the software-engineer quality gate before handing off to the system-architect (Step 4). - -**For the full verification protocol used by the system-architect, load `skill verify`.** - -## When to Use - -Load this skill when completing Step 3 and preparing to hand off to the system-architect. Run all four commands; all must pass before signalling handoff. - -## Step-by-Step - -```bash -uv run task lint # ruff check + ruff format β€” must exit 0 -uv run task static-check # pyright β€” must exit 0, 0 errors -uv run task test # pytest with coverage β€” must exit 0, 100% coverage -timeout 10s uv run task run # app starts β€” must exit non-124 -``` - -All four must pass. Do not hand off broken work. - -## Checklist - -- [ ] `lint` exits 0 (ruff check + ruff format) -- [ ] `static-check` exits 0, 0 pyright errors -- [ ] `test` exits 0, 100% coverage -- [ ] `run` exits non-124 (not hung) -- [ ] No `noqa` or `type: ignore` β€” fix the underlying issue diff --git a/.opencode/skills/create-agent/SKILL.md b/.opencode/skills/create-agent/SKILL.md deleted file mode 100644 index 5e0da1f..0000000 --- a/.opencode/skills/create-agent/SKILL.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -name: create-agent -description: Create new OpenCode agents with research-backed design patterns and industry standards -version: "1.0" -author: human-user -audience: human-user -workflow: opencode ---- - -# Create Agent - -Create a new OpenCode agent following research-backed best practices from OpenAI, Anthropic, and scientific literature. - -## When to Use - -When you need a new agent with distinct ownership, instructions, tool surface, or approval policy. Not for simple routing β€” only when the task requires a separate domain of responsibility. - -## How to Create an Agent - -### 0. Research (mandatory β€” do this first) - -Before writing any agent, research the domain to ground the agent design in industry standards and scientifically-backed evidence: - -1. **Identify the agent's domain**: What role, responsibility, and domain will this agent own? -2. **Search for domain-specific best practices**: - - For agent architecture: OpenAI Agents SDK, Anthropic Claude Agent SDK, Google Agents SDK - - For domain methodology: Academic papers, vendor guides, established standards (e.g., OWASP for security, IEEE for software engineering) - - For known failure modes: Post-mortems, case studies, industry reports -3. **Synthesize conclusions**: What ownership boundaries work? What tool design patterns? What escalation rules? -4. **Embed as design decisions**: Write the agent's ownership definition, instruction patterns, tool surface, and escalation rules based on those conclusions β€” not as citations but as direct guidance - -**Example research synthesis:** -``` -Agent domain: Security reviewer agent -Research: OWASP Testing Guide, NIST security controls, Anthropic's adversarial verification patterns -Conclusion: Security agents should assume breach by default, escalate on any critical finding, use defense-in-depth checklist. -β†’ Agent design: "role: reviewer", "escalation: any critical = human", "tool: security-scan + vuln-check" -``` - -### 1. Create the agent file - -```bash -mkdir -p .opencode/agents/ -``` - -Create `.opencode/agents/.md`: - -```markdown ---- -name: -description: <1-sentence description of what this agent does> -role: -steps: ---- - -# - -[Brief description of the agent's purpose and when it's invoked.] - -## Role - - - -## Available Skills - -| Skill | When to Load | Purpose | -|---|---|---| -| `run-session` | Every session | Session start/end protocol | -| `` | When needed | | - -## Instructions - - - -- When to invoke this agent (trigger conditions) -- What steps it owns -- How to use tools -- When to escalate or hand off -``` - -### 2. Follow the structural rules - -Apply the research conclusions about file organization: - -| File | When Loaded | Content | Avoid | -|---|---|---|---| -| `AGENTS.md` | Always | Shared conventions, commands | Workflow details | -| `.opencode/agents/*.md` | When role invoked | Role identity, step ownership, skill loads, tool permissions | Duplication | -| `.opencode/skills/*.md` | On demand | Full procedural instructions | Duplication | - -**Why**: Keeping always-loaded files lean preserves attention budget for the task at hand. - -### 3. Define clear ownership boundaries - -**Split criteria**: -- Separate ownership (different domain responsibility) -- Different instructions (not just more detail) -- Different tool surface (distinct actions) -- Different approval policy (escalation rules) - -**Anti-pattern**: Creating agents just to organize instructions. A single agent with more tools is usually better than multiple agents. - -### 4. Write effective instructions - -Write instructions that work in practice: - -- **Specific triggers**: "Load this skill when X" not "use judgment" -- **Clear actions**: Every step corresponds to a specific output -- **Concrete examples**: Include before/after code where helpful -- **Verification criteria**: How does the agent know it's done? - -### 5. Define tool permissions - -Design the tool surface based on what the agent needs to accomplish: - -- **Start with bash** for breadth -- **Promote to dedicated tools** when you need to: - - Gate security-sensitive actions - - Render structured output - - Audit usage patterns - - Serialize vs. parallelize - -### 6. Add to AGENTS.md - -Register the agent in the workflow section of `AGENTS.md`: - -```markdown -## Agents - -| Agent | Role | Steps | Skills | -|-------|------|-------|--------| -| | | | | -``` - -## Agent Template - -```markdown ---- -name: -description: -role: -steps: ---- - -# - -<2-3 paragraphs: what this agent does, when invoked, what it delivers.> - -## Context - - - -## Available Skills - -- `run-session` β€” always -- `` β€” when - -## Instructions - -### Step : - -1. -2. -3. - -### Hand-off - -When to transfer to : - -## Tool Permissions - -- Read files: -- Write files: -- Execute commands: -- Network access: - -## Escalation - -When to escalate to human: -``` - -## Existing Agents in This Project - -| Agent | Role | Steps | Purpose | -|---|---|---|---| -| `product-owner` | product-owner | 1, 5 | Scope discovery, acceptance | -| `system-architect` | system-architect | 2, 4 | Architecture, adversarial verification | -| `software-engineer` | software-engineer | 3, 5 | TDD, releases | -| `setup-project` | setup-project | meta | Initialize new projects | - -## Best Practices Summary - -1. **Start with a single agent** β€” add more only when ownership boundaries are clear -2. **Define ownership, not volume** β€” separate domains, not instruction sets -3. **Keep instructions specific** β€” concrete triggers, not vague guidance -4. **Match tools to security needs** β€” bash for flexibility, dedicated tools for gating -5. **Test with real usage** β€” iterate based on failures -6. **Reference, don't duplicate** β€” link to skills and AGENTS.md, don't copy content \ No newline at end of file diff --git a/.opencode/skills/create-pr/SKILL.md b/.opencode/skills/create-pr/SKILL.md deleted file mode 100644 index 72bfea0..0000000 --- a/.opencode/skills/create-pr/SKILL.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -name: create-pr -description: Create pull requests with conventional commits, proper formatting, and branch workflow -version: "1.0" -author: system-architect -audience: system-architect -workflow: git-management ---- - -# Create PR - -## When to Use - -Load this skill after the system-architect approves the feature (Step 4 APPROVED) and the PO has accepted it (Step 5). Use it to create and merge the feature pull request. - -## Step-by-Step - -### Branch Naming - -``` -feature/ # new feature -fix/ # bug fix -refactor/ # refactoring -docs/ # documentation -chore/ # tooling, deps, CI -``` - -## Conventional Commits - -``` -(): - -Types: feat, fix, test, refactor, chore, docs, perf, ci -``` - -Examples: -```bash -git commit -m "feat(auth): implement JWT token generation" -git commit -m "test(auth): add failing tests for token expiry" -git commit -m "fix(physics): correct ball velocity sign after wall bounce" -git commit -m "refactor(game-loop): extract timing logic to dedicated class" -git commit -m "chore(deps): add python-dotenv dependency" -``` - -## PR Creation - -```bash -# Push branch -git push -u origin feature/ - -# Create PR -gh pr create \ - --title "feat(): " \ - --body "$(cat <<'EOF' -## Summary -- - -## Acceptance Criteria -- [x] `@id:`: -- [x] `@id:`: - -## Testing -- All tests pass: `task test` -- Linting clean: `task lint` -- Type checking clean: `task static-check` -- Application runs: `timeout 10s task run` (exit 124 = hung = fix it) - -## Reviewer Notes - -EOF -)" -``` - -## PR Checklist Before Creating - -- [ ] Branch is up to date with main (`git rebase main`) -- [ ] All commits follow conventional commit format -- [ ] `task lint` exits 0 -- [ ] `task static-check` exits 0 -- [ ] `task test` exits 0, coverage 100% -- [ ] `timeout 10s task run` exits with code β‰  124 -- [ ] PR description includes all `@id` acceptance criteria - -## Merging - -Use `--no-ff` merge to preserve feature boundary in history. This makes the feature revertible as a single unit: -```bash -gh pr merge --merge --delete-branch -``` - -**After merge**: -```bash -git checkout main -git pull origin main -``` - -**Why not squash**: Squash merge erases the individual commit history of the feature. With `--no-ff`, the merge commit groups all feature commits together while preserving each commit's message and authorship. diff --git a/.opencode/skills/create-skill/SKILL.md b/.opencode/skills/create-skill/SKILL.md deleted file mode 100644 index 049f899..0000000 --- a/.opencode/skills/create-skill/SKILL.md +++ /dev/null @@ -1,148 +0,0 @@ ---- -name: create-skill -description: Create new OpenCode skills following the skill definition standard -version: "2.0" -author: software-engineer -audience: software-engineer -workflow: opencode ---- - -# Create Skill - -Create a new reusable skill for OpenCode agents, following research-backed best practices. - -## When to Use - -When you need to codify a repeatable workflow that multiple agents or sessions will follow. Skills are loaded on demand; they don't run automatically. - -## How to Create a Skill - -### 0. Research (mandatory β€” do this first) - -Before writing any skill, research the domain to ground the skill in industry standards and scientifically-backed evidence: - -1. **Identify the domain**: What workflow or methodology will this skill codify? -2. **Search for best practices**: - - Academic sources (Google Scholar, IEEE, ACM) - - Vendor documentation (OpenAI, Anthropic, Google, Microsoft) - - Industry standards (ISO, NIST, OMG) - - Established methodologies (e.g., FDD, Scrum, Kanban for process skills) -3. **Read existing research**: Check `docs/research/` for related entries β€” each file covers a domain (testing, oop-design, architecture, ai-agents, etc.) -4. **Synthesize conclusions**: Extract actionable conclusions β€” what works, why, and when to apply it -5. **Embed as guidance**: Write the skill's steps, checklists, and decision rules based on those conclusions β€” not as academic citations but as direct guidance ("Use X because it produces Y outcome") - -**Example research synthesis:** -``` -Research question: How to structure a security review skill? -Sources found: OWASP Testing Guide, NIST SP 800-53, Anthropic's agent design patterns -Conclusion: Security reviews should be adversarial (assume breakage), use defense-in-depth checklist, escalate on first critical finding. -β†’ Skill step: "3. Run adversarial checks β€” assume breach, verify every control" -``` - -### 1. Create the directory - -```bash -mkdir .opencode/skills// -``` - -Naming rules: -- 1–64 characters -- Lowercase alphanumeric with single hyphens -- Cannot start or end with hyphen, no consecutive hyphens -- Must match the directory name exactly - -### 2. Create SKILL.md with frontmatter - -```markdown ---- -name: -description: <1-sentence description, 10-100 characters> -version: "1.0" -author: -audience: -workflow: ---- - -# - - - -## When to Use - - - -## Step-by-Step - -### 1. - - -### 2. - - -## Checklist - -- [ ] -``` - -**Frontmatter requirements:** -- `name`: Max 64 chars, lowercase letters/numbers/hyphens only -- `description`: 1 sentence, 10-100 chars, include key terms and triggers -- `author`/`audience`: Use role names from AGENTS.md -- `workflow`: Category like `feature-lifecycle`, `opencode`, `release-management` - -### 3. Write body content - -Follow these research-backed patterns: - -**Structure:** -1. **When to Use** β€” specific trigger conditions, not vague guidance -2. **Step-by-Step** β€” clear sequential steps with specific actions -3. **Checklist** β€” verification items the agent can self-check - -**Formatting rules:** -- Use imperative voice ("Write the test" not "You should write") -- One step per line item in checklists -- Include concrete examples (one is enough, not exhaustive) -- Use tables for multi-column data (tool options, decision criteria) -- Link to reference docs instead of duplicating them - -**Tone:** Write in third person. The description is injected into the system prompt. - -### 4. Keep it lean - -Skills are loaded into context. Long skills consume tokens. Target: -- < 150 lines for focused workflow skills -- < 250 lines for complex multi-phase skills -- < 500 lines absolute maximum (Anthropic recommendation) - -**Cut:** -- Exhaustive examples when one is enough -- Reference documentation (link to it instead) -- Boilerplate CI/CD YAML (it belongs in `.github/`, not skills) - -### 5. Test with real usage - -The most effective skill development process involves using the skill in real tasks and iterating based on failures. - -### 6. Reference from agents - -Add the skill name to the agent's "Available Skills" section so the agent knows to load it. Update AGENTS.md skills table. - -## Available Skills in This Project - -| Skill | Used By | Purpose | -|---|---|---| -| `run-session` | all agents | Session start/end protocol | -| `select-feature` | product-owner | Score and select next backlog feature (WSJF) | -| `define-scope` | product-owner | Step 1: define acceptance criteria | -| `implement` | software-engineer | Steps 2-3: architecture + TDD loop | -| `apply-patterns` | software-engineer | Steps 2, 3: refactor when smell detected | -| `verify` | system-architect | Step 4: adversarial verification | -| `check-quality` | software-engineer | Quick reference β€” redirects to verify | -| `create-pr` | system-architect | Step 5: create PR with --no-ff merge | -| `git-release` | stakeholder | Step 5: calver versioning and release | -| `update-docs` | product-owner | Step 5 (after acceptance) + on stakeholder demand: C4 diagrams + glossary | -| `design-colors` | designer | Color palette selection and WCAG validation | -| `design-assets` | designer | SVG visual asset creation and updates | -| `create-skill` | software-engineer | Create new skills | -| `create-agent` | human-user | Create new agents with research-backed design | \ No newline at end of file diff --git a/.opencode/skills/define-scope/SKILL.md b/.opencode/skills/define-scope/SKILL.md deleted file mode 100644 index 7c93c7d..0000000 --- a/.opencode/skills/define-scope/SKILL.md +++ /dev/null @@ -1,464 +0,0 @@ ---- -name: define-scope -description: Step 1 β€” discover requirements through stakeholder interviews and write Gherkin acceptance criteria -version: "6.0" -author: product-owner -audience: product-owner -workflow: feature-lifecycle ---- - -# Scope - -This skill guides the PO through Step 1 of the feature lifecycle: interviewing the stakeholder, discovering requirements, and writing Gherkin specifications precise enough for a developer to write tests without asking questions. - -## When to Use - -When the PO is starting a new project, adding features, or refining an existing feature. The output is a set of `.feature` files in `docs/features/backlog/` ready for development. - -## Overview - -Step 1 has two stages: - -| Stage | Who | Output | -|---|---|---| -| **Stage 1 β€” Discovery** | PO + stakeholder | `docs/scope_journal.md` (Q&A) + `docs/discovery.md` (synthesis) + `.feature` descriptions | -| **Stage 2 β€” Specification** | PO alone | `Rule:` blocks + `Example:` blocks with `@id` tags in `.feature` files | - -Stage 1 is iterative and ongoing β€” sessions happen whenever the PO or stakeholder needs to discover or refine scope. Stage 2 runs per feature, only after that feature has `Status: BASELINED`. - ---- - -## Gap-Finding Techniques - -Three techniques are applied across all interview sessions to surface what stakeholders have not yet said. Use them during every session, not just at the end. - -### Critical Incident Technique (CIT) β€” Flanagan 1954 -Ask about a specific past event rather than a general description. Schema-based recall ("usually we...") hides edge cases and workarounds. A concrete incident forces actual memory. - -- "Tell me about a specific time when [X] worked exactly as you needed." -- "Tell me about a specific time when [X] broke down or frustrated you." -- Probe each incident: "What task were you doing? What happened next? What made it effective / ineffective?" - -### Laddering / Means-End Chain β€” Reynolds & Gutman 1988 -Climb from surface feature to underlying consequence to terminal value. The first answer is rarely the real constraint. - -- "Why is that important to you?" -- "What does that enable?" -- "What would break if that were not available?" -- Stop when the stakeholder reaches a value they cannot explain further. - -### CI Perspective Change β€” Fisher & Geiselman 1987 -Ask the stakeholder to describe the same situation from another actor's point of view. Peripheral details and cross-role concerns surface that the primary perspective conceals. - -- "What do you think the end user experiences in that situation?" -- "What would your team lead's concern be here?" -- "From the perspective of someone encountering this for the first time, what would they need to know?" - ---- - -## Active Listening Protocol - -Three levels of active listening apply throughout every interview session: - -- **Level 1 β€” Per answer**: immediately paraphrase each answer before moving to the next question. "So if I understand correctly, you're saying that X happens when Y?" Catches misunderstanding in the moment. -- **Level 2 β€” Per group**: brief synthesis when transitioning between behavior groups. "We've covered [area A] and [area B]. Before I ask about [area C], here is what I understood so far: [summary]. Does that capture it?" Confirms completeness, gives stakeholder a recovery point. -- **Level 3 β€” End of session**: full synthesis of everything discussed. Present to stakeholder for approval. This is the accuracy gate and the input to domain modeling. - -Do not introduce topic labels or categories during active listening. The summary must reflect what the stakeholder said, not new framing that prompts reactions to things they haven't considered. - ---- - -## Stage 1 β€” Discovery - -Discovery is a continuous, iterative process. Sessions happen whenever scope needs to be established or refined β€” for a new project, for a new feature, or when new information emerges. There is no "Phase 1" vs "Phase 2" distinction; every session follows the same structure. - -### Session Start (every session) - -**Before asking any questions:** - -1. Check `docs/scope_journal.md` for the most recent session block. - - If the most recent block has `Status: IN-PROGRESS` β†’ the previous session was interrupted. Resume it: check which `.feature` files need updating (compare journal Q&A against current `.feature` descriptions), write the `discovery.md` synthesis block if missing, then mark the block `Status: COMPLETE`. Only then begin a new session. - - If `docs/scope_journal.md` does not exist β†’ this is the first session. Create both `docs/scope_journal.md` and `docs/discovery.md` using the templates in `scope-journal.md.template` and `discovery.md.template` in this skill's directory. -2. Read `docs/domain-model.md` (if it exists) to check existing entities. The PO reads this file but never writes to it. If it does not exist yet, the SA will create it at Step 2. -3. Declare session scope to the stakeholder: announce the total groups and estimated question count (e.g., "3 groups: General (7 Q), Cross-cutting, Feature: login"). -4. Open `docs/scope_journal.md` and append a new session header: - ```markdown - ## YYYY-MM-DD β€” Session N - Status: IN-PROGRESS - ``` - Write this header **before** asking any questions. This is the durability marker β€” if the session is interrupted, the next agent sees `IN-PROGRESS` and knows writes are pending. - -### Interview Protocol - -**Progress declaration (first message):** -State the session structure upfront: -> "This discovery session has 3 question groups: -> 1. General (7 questions) β€” about users, goals, success/failure -> 2. Cross-cutting β€” about behavior groups, integrations, lifecycle events -> 3. Feature: β€” about specific functionality -> -> I will ask one group at a time and summarize before moving on." - -**Question grouping:** -- One `question` tool call per question group -- Each question within the group uses a clear `header` showing progress, e.g.: - - `General β€” Q1/7` - - `General β€” Q2/7` - - `Feature: login β€” Q3/5` - -**Input types:** -- **Checkbox (`multiple: true`)**: for multi-select answers (e.g., "Which platforms?" "Which user roles?") -- **Options**: for single-select with known choices (e.g., "Priority: High / Medium / Low") -- **Fill-up field (free text)**: for open-ended responses that cannot be pre-listed - -**Defaults:** -- Offer "Other" or pre-fill with most common answer when context permits -- Never force a stakeholder into a false dichotomy; always include "Something else / Not sure" - -### Question Order (within every session) - -Questions follow this order. Skip a group only if it was already fully covered in a prior session. - -**1. General questions** (skip entirely if any prior session has covered these) - -Ask all 7 at once: - -1. **Who** are the users of this product? -2. **What** does the product do at a high level? -3. **Why** does it exist β€” what problem does it solve? -4. **When** and **where** is it used (environment, platform, context)? -5. **Success** β€” how do we know it works? What does "done" look like? -6. **Failure** β€” what does failure look like? What must never happen? -7. **Out-of-scope** β€” what are we explicitly not building? - -Apply Level 1 active listening per answer. Apply CIT, Laddering, and CI Perspective Change per answer to surface gaps. Add new questions in the moment. - -**2. Cross-cutting questions** - -Target behavior groups, bounded contexts, integration points, lifecycle events, and system-wide constraints. Apply Level 2 active listening when transitioning between groups. - -**3. Feature questions** (one feature at a time) - -For each feature the session touches: -- Extract relevant nouns and verbs from `docs/glossary.md` and `docs/domain-model.md` (if they exist) -- Generate questions from entity gaps: boundaries, edge cases, interactions, failure modes -- Run a silent pre-mortem: "Imagine the developer builds this feature exactly as described, all tests pass, but the feature doesn't work for the user. What would be missing?" -- Apply CIT, Laddering, and CI Perspective Change per question - -**Real-time split rule**: if, during feature questions, the PO detects >2 distinct concerns OR >8 candidate Examples for a single feature, **split immediately**: -1. Record the split in the journal: note the original feature name and the two new names -2. Create stub `.feature` files for both parts (if they don't already exist) -3. Continue feature questions for both new features in sequence within the same session - -### Write Confirmation Gate - -**Before writing ANY file:** `docs/scope_journal.md`, `.feature` files, or `docs/discovery.md`. - -1. State exactly what will be written: - > "I will now append the Q&A from this session to `docs/scope_journal.md`." - -2. State exactly which file(s): - > "I will create `docs/features/backlog/.feature`." - -3. **Ask for explicit confirmation** using the `question` tool: - - `header: "Ready to write"` - - Question text: "Confirm: write to ``?" - - Options: `["Yes, write it", "Show me a preview first", "No, I need changes"]` - -4. Only proceed with `write`/`edit` if the answer is confirmation. - -**This applies to all write operations in this skill**, including: -- `docs/scope_journal.md` (session header and Q&A) -- `docs/features/backlog/.feature` (initial description or update) -- `docs/discovery.md` (synthesis block) - -### After Questions (PO alone, same session) - -**Step A β€” Write answered Q&A to journal** - -Append all answered Q&A to `docs/scope_journal.md`, in groups (general, cross-cutting, then per-feature). Write only answered questions. Unanswered questions are discarded. - -Group headers use this format: -- General group: `### General` -- Cross-cutting group: `### ` -- Feature group: `### Feature: ` - -**Step B β€” Update glossary and discovery.md** - -1. Update `docs/glossary.md` (new or corrected definitions; edits allowed). -2. Append to `docs/discovery.md` (use the template in `discovery.md.template`): - - 3-line session summary (general/behavioral focus) - - Entities **added or deprecated** this session (suggestions for the SE; not a formal model) - - Features **touched** this session + 1-line reason why - -The PO does **not** write `docs/domain-model.md`. Entity suggestions live in `discovery.md` for the SA to formalize at Step 2. - -**Step C β€” Update .feature descriptions** - -For each feature touched in this session: rewrite the `.feature` file description to reflect the current state of understanding. Only touched features are updated; all others remain exactly as-is. - -If a feature is new (just created as a stub): write its initial description now. Use the template in `feature.md.template`. - -**Step D β€” Completed feature regression check** - -If a `completed/` feature was touched and its description/rules changed: -- **Move it to `backlog/`**. Description changes always imply behavior changes; cosmetic rewrites are never performed. -- Record the move in `discovery.md`: "Moved `` from completed to backlog due to changed requirements." - -**Step E β€” Mark session complete** - -Update the session header in `docs/scope_journal.md`: -```markdown -## YYYY-MM-DD β€” Session N -Status: COMPLETE -``` - -**Commit**: `feat(discovery): ` - -### Baselining a Feature - -A feature is baselined when the stakeholder has explicitly approved its discovery. The PO writes `Status: BASELINED (YYYY-MM-DD)` in the `.feature` file. - -**Gate**: a feature may only be baselined when: -- Its description accurately reflects the stakeholder's approved understanding -- Its candidate user stories (Rule candidates) are identified -- The decomposition check passes: does not span >2 concerns AND does not have >8 candidate Examples - -A baselined feature is ready for Stage 2. The PO may baseline features one at a time β€” not all at once. - ---- - -## Stage 2 β€” Specification - -Stage 2 runs per feature, after `Status: BASELINED`. PO works alone. No stakeholder involvement. - -If the PO discovers a gap during Stage 2 that requires stakeholder input: stop Stage 2, open a new Stage 1 session, resolve the gap, then return to Stage 2. - -### Step A β€” Stories - -Derive `Rule:` blocks from the baselined feature description. One `Rule:` per user story. - -Each `Rule:` block contains: -- The rule title (2-4 words, kebab-friendly) -- The user story header as the rule description (no `Example:` blocks yet): - -```gherkin - Rule: Menu Display - As a player - I want to see a menu when the game starts - So that I can select game options -``` - -Good stories are: -- **Independent**: can be delivered without other stories -- **Negotiable**: details can be discussed -- **Valuable**: delivers something the user cares about -- **Estimable**: the developer can estimate effort -- **Small**: completable in one feature cycle -- **Testable**: can be verified with a concrete test - -Avoid: "As the system, I want..." (no business value). Break down stories that contain "and" into two Rules. - -**INVEST Gate** β€” verify every Rule before committing: - -| Letter | Question | FAIL action | -|---|---|---| -| **I**ndependent | Can this Rule be delivered without other Rules? | Split or reorder dependencies | -| **N**egotiable | Are details open to discussion with the developer? | Remove over-specification | -| **V**aluable | Does it deliver something the end user cares about? | Reframe or drop | -| **E**stimable | Can a developer estimate the effort? | Split or add discovery questions | -| **S**mall | Completable in one feature cycle? | Split into smaller Rules | -| **T**estable | Can it be verified with a concrete test? | Rewrite with observable outcomes | - -**Review checklist:** -- [ ] Every Rule has a distinct user role and benefit -- [ ] No Rule duplicates another -- [ ] Rules collectively cover all entities in scope from the feature description -- [ ] Every Rule passes the INVEST gate - -Commit: `feat(stories): write user stories for ` - -### Step B β€” Criteria - -Add `Example:` blocks under each `Rule:`. PO writes all Examples alone, based on the approved feature description and domain knowledge. No stakeholder review of individual Examples. - -**Silent pre-mortem per Rule** (before writing any Examples): - -> "What observable behaviors must we prove for this Rule to be complete?" - -All Rules must have their pre-mortems completed before any Examples are written. - -**Example format** (mandatory): - -```gherkin - Rule: Wall bounce - As a game engine - I want balls to bounce off walls - So that gameplay feels physical - - @id:a3f2b1c4 - Example: Ball bounces off top wall - Given a ball moving upward reaches y=0 - When the physics engine processes the next frame - Then the ball velocity y-component becomes positive -``` - -**Rules**: -- `Example:` keyword (not `Scenario:`) -- `Given/When/Then` in plain English -- `Then` must be a single, observable, measurable outcome β€” no "and" -- **Observable means observable by the end user**, not by a test harness -- **Declarative, not imperative** β€” describe behavior, not UI steps -- Each Example must be observably distinct from every other - -**Declarative vs. imperative Gherkin**: - -| Imperative (wrong) | Declarative (correct) | -|---|---| -| Given I type "bob" in the username field | Given a registered user Bob | -| When I click the Login button | When Bob logs in | -| Then I see "Welcome, Bob" on the dashboard | Then Bob sees a personalized welcome | - -**MoSCoW triage**: For each candidate Example, classify as Must (required for the Rule to be correct), Should (high value but deferrable), or Could (nice-to-have edge case). If Musts alone exceed 8 or the Rule spans >2 concerns, split the Rule. - -**Common mistakes to avoid**: -- "Then: It works correctly" β€” not measurable -- "Then: The system updates the database and sends an email" β€” split into two Examples -- Multiple behaviors in one Example β€” split them -- Examples that test implementation details ("Then: the Strategy pattern is used") -- Imperative UI steps instead of declarative behavior descriptions - -**Review checklist:** -- [ ] Every `Rule:` block has at least one Example -- [ ] Every Example has `Given/When/Then` -- [ ] Every `Then` is a single, observable, measurable outcome -- [ ] No Example tests implementation details -- [ ] If user interaction is involved, the interaction model is declared in the Feature description -- [ ] Each Example is observably distinct from every other -- [ ] No single feature file spans multiple unrelated concerns - -**Self-Declaration (mandatory before criteria commit)** - -Communicate verbally to the next agent. Every `DISAGREE` is a **hard blocker** β€” fix before committing. Do not commit until all items are AGREE or have a documented resolution. - -As a product-owner I declare that: -* INVEST-I: each Rule is Independent (no hidden ordering or dependency between Rules) β€” AGREE/DISAGREE | conflict: -* INVEST-V: each Rule delivers Value to a named user β€” AGREE/DISAGREE | Rule: -* INVEST-S: each Rule is Small enough for one development cycle β€” AGREE/DISAGREE | Rule: -* INVEST-T: each Rule is Testable (I can write a pass/fail Example for it) β€” AGREE/DISAGREE | Rule: -* Observable: every Then is a single, observable, measurable outcome β€” AGREE/DISAGREE | file:line -* No impl details: no Example tests internal state or implementation β€” AGREE/DISAGREE | file:line -* Coverage: every entity in the feature description appears in at least one Rule β€” AGREE/DISAGREE | missing: -* Distinct: no two Examples test the same observable behavior β€” AGREE/DISAGREE | file:line -* Pre-mortem: I ran a pre-mortem on each Rule and found no hidden failure modes β€” AGREE/DISAGREE | Rule: -* Scope: no Example introduces behavior outside the feature boundary β€” AGREE/DISAGREE | file:line - -Commit: `feat(criteria): write acceptance criteria for ` - -**After this commit, `Example:` blocks are frozen.** Any change requires: -1. Add `@deprecated` tag to the old Example -2. Write a new Example (the `@id` tag will be assigned automatically) - ---- - -## Bug Handling - -When a defect is reported against a completed or in-progress feature: - -1. **PO** adds a new Example to the relevant `Rule:` block in the `.feature` file: - - ```gherkin - @bug - Example: - Given - When - Then - ``` - -2. **SE** implements the specific test in `tests/features//` (the `@id` test). -3. **SE** also writes a `@given` Hypothesis property test in `tests/unit/` covering the whole class of inputs that triggered the bug β€” not just the single case. -4. Both tests are required β€” neither is optional. -5. SE follows the normal TDD loop (Step 3) for the new `@id`. - ---- - -## Feature File Format - -Each feature is a single `.feature` file. The description block contains the feature description and Status. All Q&A belongs in `docs/scope_journal.md`; all architectural decisions belong in `docs/adr/ADR-YYYY-MM-DD-.md`. - -See `feature.md.template` in this skill's directory for the full template. - -The **Rules (Business)** section captures business rules that hold across multiple Examples. Identifying rules first prevents redundant or contradictory Examples. - -The **Constraints** section captures non-functional requirements. Testable constraints should become `Example:` blocks with `@id` tags. - -What is **not** in `.feature` files: -- Entities table β€” domain model lives in `docs/domain-model.md` (SE-owned) -- Session Q&A blocks β€” live in `docs/scope_journal.md` -- Architecture section β€” lives in `docs/adr/ADR-*.md` - ---- - -## Post-Mortem Protocol - -When a stakeholder reports failure after the PO has attempted Step 5 acceptance, the feature does **not** move to `completed/`. Instead, the team compiles a compact post-mortem and the feature restarts at Step 2. - -### Trigger -Stakeholder reports a feature is wrong after PO acceptance attempt. - -### Workflow -1. **PO ensures feature is in `in-progress/`** (move back if already shifted). -2. **Team compiles post-mortem** β€” max 15 lines, root cause at process level. -3. **SE creates fix branch** from the feature's original start commit: - ```bash - # Find the feature's original start commit - git log --all --grep="feat()" --oneline - # Or, if the old branch still exists: - git log --reverse main..feat/ --oneline # first line = start commit - - # Create fix branch from start commit - git checkout -b fix/ - - # Commit post-mortem as first commit on the new branch - git add docs/post-mortem/YYYY-MM-DD--.md - git commit -m "docs(post-mortem): root cause for " - - # Push the fix branch - git push -u origin fix/ - ``` -4. **PO scans `docs/post-mortem/`**, selects relevant files by `` or `` in filename. -5. **PO reads selected post-mortems** for context before handoff. -6. **PO resets FLOW.md**: Status to [STEP-2-ARCH], `Next: Run @system-architect β€” restart Step 2 for on fix/ with post-mortem context`. -7. **SA begins Step 2** on `fix/`, reading relevant post-mortems as input. - -### Document Format - -File: `docs/post-mortem/YYYY-MM-DD--.md` - -Use the template `post-mortem.md.template` in this skill's directory. - -### Rules -- One file per incident. Never edit an existing post-mortem. -- If the same failure mode recurs, write a new post-mortem referencing the old one by filename. -- PO reads post-mortems selectively; never require reading all of them. - ---- - -## Templates - -All templates for files written by this skill live in this skill's directory: - -- `scope-journal.md.template` β€” `docs/scope_journal.md` structure -- `discovery.md.template` β€” `docs/discovery.md` per-session block -- `feature.md.template` β€” `.feature` file structure -- `post-mortem.md.template` β€” `docs/post-mortem/YYYY-MM-DD--.md` structure - -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/define-scope -Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. -Note: file list is sampled. - - -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/discovery.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/feature.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/scope-journal.md.template - diff --git a/.opencode/skills/define-scope/discovery-template.md b/.opencode/skills/define-scope/discovery-template.md deleted file mode 100644 index aa4cc5c..0000000 --- a/.opencode/skills/define-scope/discovery-template.md +++ /dev/null @@ -1,9 +0,0 @@ -Feature: - - <2–4 sentence description of what this feature does and why it exists.> - - Status: ELICITING - - Rules (Business): - - Constraints: diff --git a/.opencode/skills/define-scope/discovery.md.template b/.opencode/skills/define-scope/discovery.md.template deleted file mode 100644 index e07dbb7..0000000 --- a/.opencode/skills/define-scope/discovery.md.template +++ /dev/null @@ -1,24 +0,0 @@ -# Discovery: - -> Append-only session synthesis log. -> Written by the product-owner at the end of each discovery session. -> Each block summarizes one session: what was learned, what entities were suggested, and which features were touched. -> Never edit past blocks β€” later blocks extend or supersede earlier ones. - ---- - -## Session: YYYY-MM-DD - -### Summary -<3-line synthesis of the session: what was discussed, what decisions were made, what new information emerged.> - -### Entities Added or Deprecated -| Action | Type | Name | Notes | -|--------|------|------|-------| -| Added | Noun | | | -| Deprecated | Verb | | | -(Write "No changes" if no entities were added or deprecated this session.) - -### Features Touched -- `` β€” -(Write "No changes" if no features were added or modified this session.) diff --git a/.opencode/skills/define-scope/feature.md.template b/.opencode/skills/define-scope/feature.md.template deleted file mode 100644 index 7a6b9e0..0000000 --- a/.opencode/skills/define-scope/feature.md.template +++ /dev/null @@ -1,29 +0,0 @@ -Feature: - - <2–4 sentence description of what this feature does and why it exists. - Written in plain language, always kept current by the PO.> - - Status: ELICITING | BASELINED (YYYY-MM-DD) - - Rules (Business): - - - - Constraints: - - - - Rule: - As a - I want - So that - - @id:a3f2b1c4 - Example: - Given - When - Then - - @deprecated @id:b5c6d7e8 - Example: - Given ... - When ... - Then ... diff --git a/.opencode/skills/define-scope/post-mortem.md.template b/.opencode/skills/define-scope/post-mortem.md.template deleted file mode 100644 index e455413..0000000 --- a/.opencode/skills/define-scope/post-mortem.md.template +++ /dev/null @@ -1,16 +0,0 @@ -# : - -## Failed At -Step 5 β€” stakeholder: "" - -## Root Cause - - -## Missed Gate - - -## Fix - - -## Restart Check - diff --git a/.opencode/skills/define-scope/scope-journal.md.template b/.opencode/skills/define-scope/scope-journal.md.template deleted file mode 100644 index 01f6058..0000000 --- a/.opencode/skills/define-scope/scope-journal.md.template +++ /dev/null @@ -1,36 +0,0 @@ -# Scope Journal: - -> Append-only record of all discovery session Q&A. -> Written by the product-owner. Read by the product-owner for resume checks. -> Never edit past entries β€” append new session blocks only. - ---- - -## YYYY-MM-DD β€” Session N -Status: IN-PROGRESS - -### General - -| ID | Question | Answer | -|----|----------|--------| -| Q1 | Who are the users? | ... | -| Q2 | What does the product do at a high level? | ... | -| Q3 | Why does it exist β€” what problem does it solve? | ... | -| Q4 | When and where is it used? | ... | -| Q5 | Success β€” what does "done" look like? | ... | -| Q6 | Failure β€” what must never happen? | ... | -| Q7 | Out-of-scope β€” what are we explicitly not building? | ... | - -### - -| ID | Question | Answer | -|----|----------|--------| -| Q8 | ... | ... | - -### Feature: - -| ID | Question | Answer | -|----|----------|--------| -| Q9 | ... | ... | - -Status: COMPLETE diff --git a/.opencode/skills/design-assets/SKILL.md b/.opencode/skills/design-assets/SKILL.md deleted file mode 100644 index 108c245..0000000 --- a/.opencode/skills/design-assets/SKILL.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: design-assets -description: Author and update SVG visual assets using project branding identity -version: "1.0" -author: designer -audience: designer -workflow: branding ---- - -# Design Assets - -Create or update `docs/assets/logo.svg` and `docs/assets/banner.svg` from the project's branding identity in `docs/branding.md`. - -## When to Use - -- Stakeholder requests a new or updated logo or banner -- `docs/assets/` is empty after project setup - -## Step-by-Step - -### 1. Read branding - -Read `docs/branding.md`. Extract: project name, tagline, primary/accent colors, tone of voice. If colors are absent, run `skill design-colors` first. - -### 2. Apply SVG structure rules - -All SVGs must follow these constraints (W3C SVG 2 spec β€” CSS properties take precedence over presentation attributes; inline ` - - - -``` - -### 6. Commit - -```bash -git add docs/assets/ -git commit -m "design(assets): " -``` - -## Checklist - -- [ ] `docs/branding.md` read before writing any SVG -- [ ] Colors sourced from `docs/branding.md`; `skill design-colors` run if absent -- [ ] `viewBox` set; no fixed `width`/`height` on `` root -- [ ] Colors use CSS custom properties with fallback hex -- [ ] System font stack; no external imports -- [ ] `role="img"` and `aria-label` present -- [ ] Banner readable at 400px wide; logo readable at 32px -- [ ] Files committed to `docs/assets/` diff --git a/.opencode/skills/design-colors/SKILL.md b/.opencode/skills/design-colors/SKILL.md deleted file mode 100644 index f7ca8df..0000000 --- a/.opencode/skills/design-colors/SKILL.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -name: design-colors -description: Select and validate a project color palette with WCAG contrast compliance -version: "1.0" -author: designer -audience: designer -workflow: branding ---- - -# Design Colors - -Select a color palette for the project and validate it for accessibility. Write the result to `docs/branding.md`. - -## When to Use - -- Stakeholder provides a theme word or asks for a color palette -- `docs/branding.md` exists but has no colors yet -- Stakeholder requests a color change - -## Step-by-Step - -### 1. Read branding context - -Read `docs/branding.md`. If colors are already set and the stakeholder has not asked to change them, stop β€” do not overwrite. - -### 2. Select primary hue - -Map the project theme or mission to a hue. Hue carries meaning before any other element is read (Itten 1961 β€” hue semantics precede form perception): - -| Theme / Mission | Hue | Semantic | -|---|---|---| -| Technology, trust, precision | Blue | Calm, reliable | -| Growth, nature, sustainability | Green | Life, progress | -| Creativity, energy, community | Orange | Warmth, action | -| Innovation, premium, research | Purple | Depth, curiosity | -| Urgency, passion, power | Red | Use sparingly | -| Clarity, neutrality | Grey | Professional, recedes | - -### 3. Build the palette - -Use a complementary scheme by default β€” a muted primary plus a pure complementary accent. This produces the most reliably professional result without requiring design expertise (Albers 1963 β€” simultaneous contrast; complementary pairs read as distinct without competing): - -- **Primary** β€” muted/deep tone of chosen hue (lower saturation, lower value). Used for surfaces, backgrounds, headers. -- **Accent** β€” complementary hue (180Β° on color wheel), pure/saturated. Used for links, highlights, diagram lines only. - -Example for green: Primary `#2D6A4F` (deep forest), Accent `#D4A017` (golden amber). - -Use analogous, split-complementary, or triadic only when the stakeholder explicitly requests it. - -### 4. Validate WCAG 2.1 AA - -Any color used as a text background must achieve β‰₯ 4.5:1 contrast with white `#FFFFFF` (WCAG 2.1 SC 1.4.3 β€” derived from ISO 9241-3 baseline Γ— 1.5 acuity loss factor): - -``` -sRGB β†’ linear: c ≀ 0.04045 ? c/12.92 : ((c+0.055)/1.055)^2.4 -L = 0.2126Β·R + 0.7152Β·G + 0.0722Β·B -Contrast = (L_lighter + 0.05) / (L_darker + 0.05) -``` - -If contrast < 4.5:1, darken the primary until compliant. Accent colors on non-text surfaces are exempt. - -### 5. Write to branding - -Update `docs/branding.md` under `## Visual`: - -```markdown -- **Primary color:** `#XXXXXX` β€” () -- **Accent color:** `#XXXXXX` β€” () - -> Colors meet WCAG 2.1 AA (X.X:1 contrast) when white text is placed on the primary. -``` - -## Checklist - -- [ ] Existing colors checked before proceeding -- [ ] Hue chosen from theme/mission semantics -- [ ] Primary is muted/deep (not pure saturated) -- [ ] Accent is complementary or stakeholder-specified -- [ ] White-on-primary contrast β‰₯ 4.5:1 calculated and reported -- [ ] `docs/branding.md` updated with hex codes, rationale, and contrast note diff --git a/.opencode/skills/flow/SKILL.md b/.opencode/skills/flow/SKILL.md deleted file mode 100644 index bdb7d76..0000000 --- a/.opencode/skills/flow/SKILL.md +++ /dev/null @@ -1,271 +0,0 @@ ---- -name: flow -version: "1.0" -description: Feature workflow protocol β€” read FLOW.md, auto-detect state, resume from checkpoint, update state -author: software-engineer -audience: all-agents -workflow: session-management ---- - -# Feature Workflow Protocol - -This skill defines the single-feature-at-a-time workflow state machine. Every feature flows through 5 steps. Only ONE feature is in progress at any time. The filesystem enforces this. - -## Prerequisites - -Before starting any flow, verify these exist. If any are missing, stop and alert the human. - -| Requirement | Verification Command | Missing Action | -|---|---|---| -| Agent: product-owner | `test -f .opencode/agents/product-owner.md` | Create agent file | -| Agent: system-architect | `test -f .opencode/agents/system-architect.md` | Create agent file | -| Agent: software-engineer | `test -f .opencode/agents/software-engineer.md` | Create agent file | -| Skill: run-session | `test -f .opencode/skills/run-session/SKILL.md` | Install skill | -| Skill: define-scope | `test -f .opencode/skills/define-scope/SKILL.md` | Install skill | -| Skill: architect | `test -f .opencode/skills/architect/SKILL.md` | Install skill | -| Skill: implement | `test -f .opencode/skills/implement/SKILL.md` | Install skill | -| Skill: verify | `test -f .opencode/skills/verify/SKILL.md` | Install skill | -| Skill: version-control | `test -f .opencode/skills/version-control/SKILL.md` | Install skill | -| Tool: uv | `command -v uv` | Install uv | -| Tool: git | `command -v git` | Install git | -| Directory: docs/features/ | `test -d docs/features/backlog` | Run setup-project | -| Directory: docs/adr/ | `test -d docs/adr` | Create directory | -| FLOW.md | `test -f FLOW.md` | Create from template | - -## State Machine - -States are checked IN ORDER. The first matching state is the current state. - -### Detection Rules - -1. **No file in `docs/features/in-progress/`** β†’ [IDLE] -2. **Feature in in-progress, no `Status: BASELINED`** β†’ [STEP-1-DISCOVERY] -3. **Feature has `Status: BASELINED`, no `Rule:` blocks** β†’ [STEP-1-STORIES] -4. **Feature has `Rule:` blocks, no `Example:` with @id** β†’ [STEP-1-CRITERIA] -5. **Feature has @id tags, no feat/ or fix/ branch exists** β†’ [STEP-2-READY] -6. **On feature branch, no test stubs in `tests/features//`** β†’ [STEP-2-ARCH] -7. **Test stubs exist, any have `@pytest.mark.skip`** β†’ [STEP-3-READY] -8. **Unskipped test exists that fails** β†’ [STEP-3-RED] -9. **All unskipped tests pass, skipped tests remain** β†’ [STEP-3-GREEN] -10. **All tests pass, no skipped tests** β†’ [STEP-4-READY] -11. **Manual state set by SA after Step 4 approval** β†’ [STEP-5-READY] -12. **On main branch, feature still in in-progress/** β†’ [STEP-5-MERGE] -13. **Post-mortem file exists for current feature** β†’ [POST-MORTEM] - -### State Details - -#### [IDLE] β†’ Waiting for feature selection -**Owner**: product-owner -**Detect**: No file in `docs/features/in-progress/` -**Action**: Select feature from backlog/ and move to in-progress/ -**Next**: [STEP-1-DISCOVERY] - -#### [STEP-1-DISCOVERY] β†’ Requirements discovery -**Owner**: product-owner -**Detect**: Feature in in-progress/, no `Status: BASELINED` in file -**Action**: Interview stakeholder, update scope_journal.md, discovery.md, glossary.md -**Success**: Feature baselined β†’ [STEP-1-STORIES] -**Failure**: More discovery needed β†’ Stay in [STEP-1-DISCOVERY] - -#### [STEP-1-STORIES] β†’ Write user stories -**Owner**: product-owner -**Detect**: Feature has `Status: BASELINED`, no `Rule:` blocks -**Action**: Write Rule: blocks with INVEST criteria -**Success**: Stories complete β†’ [STEP-1-CRITERIA] - -#### [STEP-1-CRITERIA] β†’ Write acceptance criteria -**Owner**: product-owner -**Detect**: Feature has `Rule:` blocks, no `Example:` blocks with @id -**Action**: Write Example: blocks with @id tags -**Success**: Criteria complete β†’ [STEP-2-READY] -**Commit**: `feat(criteria): write acceptance criteria for ` - -#### [STEP-2-READY] β†’ Ready for architecture -**Owner**: system-architect -**Detect**: Feature has @id tags, no feat/ branch exists -**Action**: Create branch feat/ from main -**Success**: Branch created β†’ [STEP-2-ARCH] - -#### [STEP-2-ARCH] β†’ Design architecture -**Owner**: system-architect -**Detect**: On feat/ branch, no test stubs in tests/features// -**Action**: Read feature, design stubs, write ADRs, update domain-model.md -**Success**: Run `uv run task test-fast` generates stubs β†’ [STEP-3-READY] -**Failure**: Spec unclear β†’ [STEP-1-DISCOVERY] (escalate to PO) -**Commit**: `feat(arch): design architecture` - -#### [STEP-3-READY] β†’ Ready for TDD -**Owner**: software-engineer -**Detect**: Test stubs exist, some have @pytest.mark.skip -**Action**: Pick first skipped @id, remove skip, write test -**Success**: Test written and fails β†’ [STEP-3-RED] - -#### [STEP-3-RED] β†’ Test failing -**Owner**: software-engineer -**Detect**: Unskipped test exists that fails -**Action**: Write minimal code to pass -**Success**: Test passes β†’ [STEP-3-GREEN] - -#### [STEP-3-GREEN] β†’ Test passing -**Owner**: software-engineer -**Detect**: All unskipped tests pass, more skipped tests remain -**Action**: Refactor if needed, then pick next @id -**Success**: More @ids β†’ [STEP-3-READY] -**Success**: All @ids done β†’ [STEP-4-READY] -**Commit**: After each @id or logical group - -#### [STEP-4-READY] β†’ Ready for verification -**Owner**: system-architect -**Detect**: All tests implemented (no @skip) and passing -**Action**: Run all quality checks, semantic review -**Success**: All checks pass β†’ [STEP-5-READY] -**Failure**: Issues found β†’ [STEP-3-READY] (document issues) - -#### [STEP-5-READY] β†’ Ready for acceptance -**Owner**: product-owner -**Detect**: Manual state (set after Step 4 approval) -**Action**: Demo and validate against criteria -**Success**: Feature accepted β†’ [STEP-5-MERGE] -**Failure**: Not accepted β†’ [POST-MORTEM] - -#### [STEP-5-MERGE] β†’ Merge to main -**Owner**: software-engineer -**Detect**: Feature accepted, still on feature branch -**Action**: Merge feat/ to main with --no-ff -**Success**: Merged β†’ [STEP-5-COMPLETE] - -#### [STEP-5-COMPLETE] β†’ Feature complete -**Owner**: product-owner -**Detect**: On main branch, feature still in in-progress/ -**Action**: Move feature from in-progress/ to completed/ -**Success**: Feature moved β†’ [IDLE] - -#### [POST-MORTEM] β†’ Failed feature analysis -**Owner**: product-owner -**Detect**: Post-mortem file exists for current feature -**Action**: Write post-mortem, create fix/ branch -**Success**: Post-mortem complete β†’ [STEP-2-ARCH] - -## Session Protocol - -### Session Start - -1. Read `FLOW.md` β€” find current feature, branch, status. -2. Run `detect-state` (see below) to verify the state is correct. -3. If the detected state differs from `FLOW.md` Status, update `FLOW.md` to match reality. -4. Check prerequisites table (above). If any are missing, stop and report. -5. If a feature is active, read the in-progress `.feature` file. -6. Run `git status` and `git branch --show-current` to understand workspace state. -7. Confirm scope: you are working on exactly one step of one feature. - -### Session End - -1. Update `FLOW.md`: - - Set Status to the detected state - - Update Session Log with what was done - - Update `Next:` line with one concrete action -2. Commit any uncommitted work (even WIP): - ```bash - git add -A - git commit -m "WIP(): " - ``` -3. If a step is fully complete, use the proper commit message instead of WIP. - -### Step Completion Protocol - -When a step completes within a session: - -1. Update `FLOW.md` to reflect the completed step before doing any other work. -2. Commit the `FLOW.md` update: - ```bash - git add FLOW.md - git commit -m "chore: complete step for " - ``` -3. Only then begin the next step (in a new session where possible). - -## Auto-Detection - -To detect the current state automatically, run these checks in order: - -```bash -# 1. Check for in-progress feature -ls docs/features/in-progress/*.feature 2>/dev/null | grep -v ".gitkeep" -# If empty β†’ [IDLE] - -# 2. Check feature baselined -grep -q "Status: BASELINED" docs/features/in-progress/*.feature -# If no match β†’ [STEP-1-DISCOVERY] - -# 3. Check for Rule blocks -grep -q "^Rule:" docs/features/in-progress/*.feature -# If no match β†’ [STEP-1-STORIES] - -# 4. Check for Example blocks with @id -grep -q "@id:" docs/features/in-progress/*.feature -# If no match β†’ [STEP-1-CRITERIA] - -# 5. Check for feature branch -git branch --show-current | grep -E "^feat/|^fix/" -# If no match β†’ [STEP-2-READY] - -# 6. Check for test stubs -ls tests/features/*/ 2>/dev/null | head -1 -# If empty β†’ [STEP-2-ARCH] - -# 7. Check for skipped tests -grep -r "@pytest.mark.skip" tests/features/*/ 2>/dev/null -# If found β†’ [STEP-3-READY] or [STEP-3-GREEN] -# If not found β†’ [STEP-4-READY] - -# 8. Check test failures -uv run task test-fast 2>&1 | grep -E "FAILED|ERROR" -# If found β†’ [STEP-3-RED] -# If not found and on main β†’ [STEP-5-MERGE] -``` - -## FLOW.md Format - -```markdown -# FLOW Protocol - -## Current Feature -**Feature**: | [NONE] -**Branch**: | [NONE] -**Status**: - -## Prerequisites -- [x] Agents: product-owner, system-architect, software-engineer -- [x] Skills: run-session, define-scope, architect, implement, verify, version-control -- [x] Tools: uv, git -- [x] Directories: docs/features/, docs/adr/ - -## Session Log - -**YYYY-MM-DD HH:MM** β€” β€” β€” - -## Next -Run @ β€” -``` - -## Rules - -1. Never skip reading `FLOW.md` at session start -2. Never end a session without updating `FLOW.md` -3. Never leave uncommitted changes β€” commit as WIP if needed -4. One step per session where possible; do not start Step N+1 in the same session as Step N -5. The "Next" line must be actionable enough that a fresh AI can execute it without asking questions -6. When a step completes, update `FLOW.md` and commit **before** any further work -7. The Session Log is append-only β€” never delete old entries -8. If `FLOW.md` is missing, create it from the template before doing any other work -9. If detected state differs from `FLOW.md` Status, trust the detected state and update `FLOW.md` - -## Output Style - -Use minimal output. Every message must contain only what the next agent or stakeholder needs to continue β€” findings, status, decisions, blockers, and the Next: line. - -- Use the fewest, least verbose tool calls necessary to achieve the step's goal -- Report results, not process -- No narration before or after tool calls -- No restating tool output in prose -- No summaries of what was just done -- Always close with Next: diff --git a/.opencode/skills/flow/flow.md.template b/.opencode/skills/flow/flow.md.template deleted file mode 100644 index 2f0731b..0000000 --- a/.opencode/skills/flow/flow.md.template +++ /dev/null @@ -1,20 +0,0 @@ -# FLOW Protocol - -This file tracks the current feature in progress. Only ONE feature flows through the system at a time. - -## Current Feature -**Feature**: [NONE] -**Branch**: [NONE] -**Status**: [IDLE] - -## Prerequisites -- [ ] Agents: product-owner, system-architect, software-engineer -- [ ] Skills: run-session, define-scope, architect, implement, verify, version-control -- [ ] Tools: uv, git -- [ ] Directories: docs/features/, docs/adr/ - -## Session Log - - -## Next -Run @product-owner β€” load skill select-feature and pick the next BASELINED feature from backlog. diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md deleted file mode 100644 index 62d701c..0000000 --- a/.opencode/skills/git-release/SKILL.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -name: git-release -description: Create releases with hybrid major.minor.calver versioning and optional custom release naming -version: "1.1" -author: stakeholder -audience: stakeholder -workflow: release-management ---- - -# Git Release - -Create a tagged GitHub release after the PO accepts the feature (Step 5). - -## Version Format - -`v{major}.{minor}.{YYYYMMDD}` - -- **Major**: breaking changes (API changes, removed features) -- **Minor**: new features; also incremented if two releases happen on the same day -- **Date**: today in YYYYMMDD format - -Examples: -``` -v1.2.20260302 β†’ v1.3.20260415 (new feature, new day) -v1.2.20260302 β†’ v2.0.20260415 (breaking change) -v1.2.20260415 β†’ v1.3.20260415 (same-day second release) -``` - -## Release Naming - -**Default**: no release name β€” the version tag alone is the release identifier. This is the industry-standard baseline (git tag, GitHub release title = version string). - -**Custom naming**: if `docs/branding.md` exists and `Release Naming > Convention` is set, apply it. The convention field specifies the pattern (e.g. `adjective-greek-figure`, `adjective-animal`, `codename`). - -Check previous names to avoid repetition: -```bash -gh release list --limit 20 -``` - -## Release Process - -**Guard**: `git branch --show-current` must output `main`. If not, stop β€” releases happen from `main` only. - -```bash -git checkout main -git fetch origin main -git merge --ff-only origin/main # fast-forward only; if this fails, main has diverged β€” resolve first -``` - - - -### 0. Read branding - -Read `docs/branding.md` if it exists: - -- If `Release Naming > Convention` is set: use that convention for the release name. Analyze commits and PRs to choose a name that reflects the release theme. -- If `Release Naming > Theme` is set: constrain the name to that thematic domain. -- If `Release Naming > Excluded words` is set: omit those words. -- If the file is absent or `Release Naming > Convention` is blank: skip naming β€” use version string only. - -### 1. Analyze changes since last release - -```bash -last_tag=$(git describe --tags --abbrev=0) -git log ${last_tag}..HEAD --oneline -gh pr list --state merged --limit 20 --json title,number,labels -``` - -### 2. Calculate new version - -```bash -current_date=$(date +%Y%m%d) -# Determine major.minor based on change type, then: -# new_version="v{major}.{minor}.${current_date}" -``` - -### 3. Update version in pyproject.toml and package __init__.py - -Both must match: -```bash -# Update pyproject.toml version field -# Update /__version__ to match -``` - -### 4. Update CHANGELOG.md - -Add at the top. If a release name was generated in Step 0, include it; otherwise omit it: -```markdown -## [v{version}] - {YYYY-MM-DD}[ - {Release Name}] - -### Added -- description (#PR-number) - -### Changed -- description (#PR-number) - -### Fixed -- description (#PR-number) -``` - -### 5. Update living docs - -Run the `update-docs` skill to reflect the newly accepted feature in C4 diagrams and the glossary. This step runs inline β€” do not commit separately. - -Load and execute the full `update-docs` skill now: -- Update `docs/context.md` (C4 Level 1) -- Update `docs/container.md` (C4 Level 2, if multi-container) -- Update `docs/glossary.md` (living glossary) - -The `update-docs` commit step is **skipped** here β€” all changed files are staged together with the version bump in step 6. - -### 6. Regenerate lockfile and commit version bump - -After updating `pyproject.toml`, regenerate the lockfile β€” CI runs `uv sync --locked` and will fail if it is stale: - -```bash -uv lock -git add pyproject.toml /__init__.py CHANGELOG.md uv.lock \ - docs/context.md docs/container.md docs/glossary.md -git commit -m "chore(release): bump version to v{version}[ - {Release Name}]" -# Include " - {Release Name}" only if a release name was generated in Step 0; omit otherwise. -``` - -### 7. Create GitHub release - -Assign the SHA first so it expands correctly inside the notes string: - -```bash -SHA=$(git rev-parse --short HEAD) -gh release create "v{version}" \ - --title "v{version}[ - {Release Name}]" \ - --notes "# v{version}[ - {Release Name}] - -> *\"{one-line tagline matching the release theme}\"* ← include only if a release name was generated - -## Changelog - -### Added -- feat: description (#PR) - -### Fixed -- fix: description (#PR) - -### Changed -- refactor/chore/docs: description (#PR) - -## Summary - -2-3 sentences describing what this release accomplishes[ and why the name fits β€” omit if no name]. - ---- -**SHA**: \`${SHA}\`" -# Replace [ - {Release Name}] with the actual name, or omit the bracketed portion entirely if Step 0 produced no name. -``` - -### 8. If a hotfix commit follows the release tag - -If CI fails after the release (e.g. a stale lockfile) and a hotfix commit is pushed, reassign the tag and GitHub release to that commit: - -```bash -# Delete the old tag locally and on remote -git tag -d "v{version}" -git push origin ":refs/tags/v{version}" - -# Recreate the tag on the hotfix commit -git tag "v{version}" {hotfix-sha} -git push origin "v{version}" - -# Update the GitHub release to point to the new tag -gh release edit "v{version}" --target {hotfix-sha} -``` - -The release notes and title do not need to change β€” only the target commit moves. - -## Quality Checklist - -- [ ] `task test` passes -- [ ] `task lint` passes -- [ ] `task static-check` passes -- [ ] `pyproject.toml` version updated -- [ ] `uv lock` run after version bump β€” lockfile must be up to date -- [ ] `/__version__` matches `pyproject.toml` version -- [ ] CHANGELOG.md updated -- [ ] `update-docs` skill run β€” C4 diagrams and glossary reflect the new feature -- [ ] Release name not used before -- [ ] Release notes follow the template format -- [ ] If a hotfix was pushed after the tag: tag reassigned to hotfix commit diff --git a/.opencode/skills/implement/SKILL.md b/.opencode/skills/implement/SKILL.md deleted file mode 100644 index 533370b..0000000 --- a/.opencode/skills/implement/SKILL.md +++ /dev/null @@ -1,304 +0,0 @@ ---- -name: implement -description: Step 3 β€” TDD Loop, one @id at a time -version: "5.0" -author: software-engineer -audience: software-engineer -workflow: feature-lifecycle ---- - -# Implement - -Step 3: RED β†’ GREEN β†’ REFACTOR, one @id at a time. The software-engineer owns this step entirely. - -## When to Use - -Load this skill when continuing Step 3 (TDD Loop) for an in-progress feature. Architecture stubs must already exist (created by the system-architect at Step 2). - -## Software-Engineer Quality Gate Priority Order - -During implementation, correctness priorities are (in order): - -1. **Design correctness** β€” YAGNI > KISS > DRY > SOLID > Object Calisthenics > appropriate design patterns > complex code > complicated code > failing code > no code -2. **One @id green** β€” the specific test under work passes, plus `test-fast` still passes -3. **Commit** β€” when a meaningful increment is green -4. **Quality tooling** β€” `lint`, `static-check`, full `test` with coverage run at end-of-feature handoff - -Design correctness is far more important than lint/pyright/coverage compliance. Never run lint (ruff check, ruff format), static-check (pyright), or coverage during the TDD loop β€” those are handoff-only checks. - ---- - -## Step 3 β€” TDD Loop - -### Prerequisites - -- [ ] Exactly one .feature `in_progress`. If not present, load `skill select-feature` -- [ ] On `feat/` or `fix/` branch (`git branch --show-current`). If on `main`, load `skill version-control` and create/switch to the branch first -- [ ] Architecture stubs present in `/` (committed by Step 2) -- [ ] Read `docs/system.md` β€” understand current system structure and constraints -- [ ] Read in-progress `.feature` file β€” understand acceptance criteria -- [ ] Test stub files exist in `tests/features//_test.py` β€” generated by pytest-beehave at Step 2 end; if missing, re-run `uv run task test-fast` and commit the generated files before entering RED - -### Build Test List - -1. List all `@id` tags from in-progress `.feature` file -2. Order: fewest dependencies first; most impactful within that set -3. Each `@id` = one TODO item, status: `pending` -4. Confirm each `@id` has a corresponding skipped stub in `tests/features//` β€” if any are missing, add them before proceeding - -### Outer Loop β€” One @id at a time - -**WIP limit**: exactly one `in_progress` at all times. - -For each pending `@id`: - -``` -INNER LOOP -β”œβ”€β”€ RED -β”‚ β”œβ”€β”€ Confirm stub for this @id exists in tests/features//_test.py with @pytest.mark.skip -β”‚ β”œβ”€β”€ Read existing stubs in `/` β€” base the test on the current data model and signatures -β”‚ β”œβ”€β”€ Write test body (Given/When/Then β†’ Arrange/Act/Assert); remove @pytest.mark.skip -β”‚ β”œβ”€β”€ Update stub signatures as needed β€” edit the `.py` file directly -β”‚ β”œβ”€β”€ uv run task test-fast -β”‚ └── EXIT: this @id FAILS -β”‚ (if it passes: test is wrong β€” fix it first) -β”‚ -β”œβ”€β”€ GREEN -β”‚ β”œβ”€β”€ Write minimum code β€” YAGNI + KISS only -β”‚ β”‚ (no DRY, SOLID, OC, Docstring, type hint here β€” those belong in REFACTOR) -β”‚ β”œβ”€β”€ uv run task test-fast -β”‚ └── EXIT: this @id passes AND all prior tests pass -β”‚ (fix implementation only; do not advance to next @id) -β”‚ -└── REFACTOR - β”œβ”€β”€ Load `skill refactor` β€” follow its Step-by-Step for this phase - β”œβ”€β”€ uv run task test-fast after each individual change - └── EXIT: test-fast passes; no smells remain - -Mark @id completed in FLOW.md Session Log -Commit when a meaningful increment is green -``` - -### Quality Gate (all @id green) - -```bash -uv run task lint -uv run task static-check -uv run task test-coverage # coverage must be 100% -timeout 10s uv run task run -``` - -If coverage < 100%: add test in `tests/unit/` for uncovered branch (do NOT add @id tests for coverage). - -All must pass before Self-Declaration. - -### Self-Declaration (once, after all quality gates pass) - - - -Communicate verbally to the system-architect. Answer honestly for each principle: - -As a software-engineer I declare that: -* 1. YAGNI: no code without a failing test β€” AGREE/DISAGREE | file:line -* 2. YAGNI: no speculative abstractions β€” AGREE/DISAGREE | file:line -* 3. KISS: simplest solution that passes β€” AGREE/DISAGREE | file:line -* 4. KISS: no premature optimization β€” AGREE/DISAGREE | file:line -* 5. DRY: no duplication β€” AGREE/DISAGREE | file:line -* 6. DRY: no redundant comments β€” AGREE/DISAGREE | file:line -* 7. SOLID-S: one reason to change per class β€” AGREE/DISAGREE | file:line -* 8. SOLID-O: open for extension, closed for modification β€” AGREE/DISAGREE | file:line -* 9. SOLID-L: subtypes substitutable β€” AGREE/DISAGREE | file:line -* 10. SOLID-I: no forced unused deps β€” AGREE/DISAGREE | file:line -* 11. SOLID-D: depend on abstractions, not concretions β€” AGREE/DISAGREE | file:line -* 12. OC-1: one level of indentation per method β€” AGREE/DISAGREE | deepest: file:line -* 13. OC-2: no else after return β€” AGREE/DISAGREE | file:line -* 14. OC-3: primitive types wrapped β€” AGREE/DISAGREE | file:line -* 15. OC-4: first-class collections β€” AGREE/DISAGREE | file:line -* 16. OC-5: one dot per line β€” AGREE/DISAGREE | file:line -* 17. OC-6: no abbreviations β€” AGREE/DISAGREE | file:line -* 18. OC-7: ≀20 lines per function, ≀50 per class β€” AGREE/DISAGREE | longest: file:line -* 19. OC-8: ≀2 instance variables per class (behavioural classes only; dataclasses, Pydantic models, value objects, and TypedDicts are exempt) β€” AGREE/DISAGREE | file:line -* 20. OC-9: no getters/setters β€” AGREE/DISAGREE | file:line -* 21. Patterns: no good reason remains to refactor using OOP or Design Patterns β€” AGREE/DISAGREE | file:line -* 22. Patterns: no creational smell β€” AGREE/DISAGREE | file:line -* 23. Patterns: no structural smell β€” AGREE/DISAGREE | file:line -* 24. Patterns: no behavioral smell β€” AGREE/DISAGREE | file:line -* 25. Semantic: tests operate at same abstraction as AC β€” AGREE/DISAGREE | file:line - -A `DISAGREE` answer is not automatic rejection β€” state the reason and fix before handing off. - -### Branch Hygiene (before handoff) - -Before signalling completion: -1. `git status` β€” working tree must be clean. Commit any remaining changes. -2. `git branch --show-current` β€” must be `feat/` or `fix/`, never `main`. -3. `git log main..HEAD --oneline` β€” must show 1+ commits. If empty, nothing was committed on this branch. -4. `git push origin $(git branch --show-current)` β€” all commits must be on origin. - -### Hand off to Step 4 (Verify) - -Signal completion to the system-architect. Provide: -- Feature file path -- Self-Declaration (communicated verbally, as above) -- Summary of what was implemented - ---- - -## Test Writing Conventions - -### Test File Layout - -``` -tests/features//_test.py -``` - -- `` = the `.feature` file stem with hyphens replaced by underscores, lowercase -- `` = the `Rule:` title slugified (lowercase, underscores) - -### Function Naming - -```python -def test__<@id>() -> None: -``` - -- `feature_slug` = the `.feature` file stem with spaces/hyphens replaced by underscores, lowercase -- `@id` = the `@id` from the `Example:` block - -### Docstring Format (mandatory) - -New tests start as skipped stubs. Remove `@pytest.mark.skip` when implementing in the RED phase. - -```python -@pytest.mark.skip(reason="not yet implemented") -def test__<@id>() -> None: - """ - <@id steps raw text including new lines> - """ -``` - -**Rules**: -- Docstring contains `Gherkin steps` as raw text on separate indented lines -- No extra metadata in docstring β€” traceability comes from function name `@id` suffix - -### Markers - -- `@pytest.mark.slow` β€” takes > 50ms (Hypothesis, DB, network, terminal I/O) -- `@pytest.mark.deprecated` β€” auto-skipped by pytest-beehave; used for superseded Examples - -```python -@pytest.mark.deprecated -def test_wall_bounce_a3f2b1c4() -> None: - ... - -@pytest.mark.slow -def test_checkout_flow_b2c3d4e5() -> None: - ... -``` - -### Hypothesis Tests - -When using `@given` in `tests/unit/`: - -```python -@pytest.mark.slow -@given(x=st.floats(min_value=-100, max_value=100, allow_nan=False)) -@example(x=0.0) -def test_wall_bounce_c4d5e6f7(x: float) -> None: - """ - Given: Any floating point input value - When: compute_distance is called - Then: The result is >= 0 - """ - assume(x != 0.0) - result = compute_distance(x) - assert result >= 0 -``` - -**Rules**: -- `@pytest.mark.slow` is mandatory on every `@given`-decorated test -- `@example(...)` is optional but encouraged -- Do not use Hypothesis for: I/O, side effects, network calls, database writes - -### Semantic Alignment Rule - -The test's Given/When/Then must operate at the **same abstraction level** as the AC's Steps. - -| AC says | Test must do | -|---|---| -| "When the user presses W" | Send `"W"` through the actual input mechanism | -| "When `update_player` receives 'W'" | Call `update_player("W")` directly | - -If testing through the real entry point is infeasible, escalate to PO to adjust the AC boundary. - -### Quality Rules - -- Write every test as if you cannot see the production code β€” test what a caller observes -- No `isinstance()`, `type()`, or internal attribute (`_x`) checks in assertions -- One assertion concept per test (multiple `assert` ok if they verify the same thing) -- No `pytest.mark.xfail` without written justification -- `pytest.mark.skip(reason="not yet implemented")` is only valid on stubs β€” remove it when implementing -- Test data embedded directly in the test, not loaded from external files - -### Test Tool Decision - -| Situation | Location | Tool | -|---|---|---| -| Deterministic scenario from a `.feature` `@id` | `tests/features/` | Plain pytest | -| Property holding across many input values | `tests/unit/` | Hypothesis `@given` | -| Specific behavior or single edge case | `tests/unit/` | Plain pytest | -| Stateful system with sequences of operations | `tests/unit/` | Hypothesis stateful testing | - ---- - -## Handling Spec Gaps - -If during implementation you discover a behavior not covered by existing acceptance criteria: -- **Do not extend criteria yourself** β€” escalate to PO -- Note the gap in FLOW.md under `## Next` -- The PO will decide whether to add a new Example to the `.feature` file - -Extra tests in `tests/unit/` are allowed freely (coverage, edge cases, etc.) β€” these do not need `@id` traceability. - ---- - -## Signature Design - - signatures are written during Step 2 (Architecture) by the system-architect and refined during Step 3 (RED) by the software-engineer. They live directly in the package `.py` files β€” never in the `.feature` file. - -Key rules: -- Bodies are always `...` in the architecture stub -- GREEN phase replaces `...` with the minimum implementation -- REFACTOR phase cleans up the result - -Use Python Protocols for external dependencies if they are identified in scope β€” never depend on a concrete class directly: - -```python -from typing import Protocol -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True) -class EmailAddress: - value: str - - def validate(self) -> None: ... - - -class UserRepository(Protocol): - def save(self, user: "User") -> None: ... - def find_by_email(self, email: EmailAddress) -> "User | None": ... -``` - ---- - -## Templates - -Templates for architecture files live in the `architect` skill's directory: - -- `domain-model.md.template` β€” `docs/domain-model.md` structure -- `system.md.template` β€” `docs/system.md` structure -- `adr.md.template` β€” individual ADR file structure - -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/implement -Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. -Note: file list is sampled. diff --git a/.opencode/skills/implement/adr.md.template b/.opencode/skills/implement/adr.md.template deleted file mode 100644 index 3670892..0000000 --- a/.opencode/skills/implement/adr.md.template +++ /dev/null @@ -1,23 +0,0 @@ -# ADR: - -> Architectural Decision Record -> Written by the software-engineer during Step 2 for non-obvious decisions with meaningful trade-offs. -> Routine YAGNI choices do not need a record. - -| Field | Value | -|-------|-------| -| **Date** | YYYY-MM-DD | -| **Feature** | | -| **Status** | Proposed | Accepted | Superseded | - -## Decision - - -## Reason - - -## Alternatives Considered - - -## Consequences - diff --git a/.opencode/skills/implement/domain-model.md.template b/.opencode/skills/implement/domain-model.md.template deleted file mode 100644 index 5df4e1a..0000000 --- a/.opencode/skills/implement/domain-model.md.template +++ /dev/null @@ -1,37 +0,0 @@ -# Domain Model: - -> Living reference of code-facing domain entities. -> Owned by the software-engineer. Created and updated at Step 2. -> The product-owner reads this file to check existing entities during discovery, but never writes to it. -> Append-only: add new entries at the bottom. Deprecate old entries by moving them to the Deprecated section. -> Never edit existing live entries β€” code depends on them. - ---- - -## Entities - -| Name | Type | Description | Bounded Context | First Appeared | -|------|------|-------------|-----------------|----------------| -| | Entity | | | | -| | Value Object | | | | -| | Aggregate | | | | - -## Verbs - -| Name | Actor | Object | Description | First Appeared | -|------|-------|--------|-------------|----------------| -| | | | | | - -## Relationships - -| Subject | Relation | Object | Cardinality | Notes | -|---------|----------|--------|-------------|-------| -| | | | <1:1 / 1:N / M:N> | | - ---- - -## Deprecated - -| Name | Type | Deprecated Date | Replaced By | Reason | -|------|------|-----------------|-------------|--------| -| | Entity | YYYY-MM-DD | | | diff --git a/.opencode/skills/implement/system.md.template b/.opencode/skills/implement/system.md.template deleted file mode 100644 index 05fb2b6..0000000 --- a/.opencode/skills/implement/system.md.template +++ /dev/null @@ -1,27 +0,0 @@ -# System Overview: - -> Current-state description of the production system. -> Rewritten by the software-engineer at Step 2 for each feature cycle. -> Reviewed by the product-owner at Step 5. -> Contains only completed features β€” nothing from backlog or in-progress. - -## Summary -<3–5 sentence description of what the system currently does, who uses it, and its primary boundaries.> - -## Actors -- `` β€” (from completed features) - -## Modules / Components -- `` β€” (from completed features and ADRs) - -## External Dependencies -- `` β€” (from ADRs) - -## Constraints -- - -## Relevant ADRs -- ADR-YYYY-MM-DD- β€” (only ADRs affecting current system state) - -## Completed Features -- `` β€” diff --git a/.opencode/skills/refactor/SKILL.md b/.opencode/skills/refactor/SKILL.md deleted file mode 100644 index 6d2d5c5..0000000 --- a/.opencode/skills/refactor/SKILL.md +++ /dev/null @@ -1,280 +0,0 @@ ---- -name: refactor -description: Safe refactoring protocol for TDD β€” green bar rule, two-hats discipline, preparatory refactoring, and smell catalogue -version: "2.0" -author: software-engineer -audience: software-engineer -workflow: feature-lifecycle ---- - -# Refactor - -Load this skill when entering the REFACTOR phase of a TDD cycle, or before starting RED on a new `@id` when preparatory refactoring is needed. - -Sources: Fowler *Refactoring* 2nd ed. (2018); Beck *Canon TDD* (2023); Beck *Tidy First?* (2023); Martin *SOLID* (2000); Bay *Object Calisthenics* (2005); Shvets *Refactoring.Guru* (2014–present). See `docs/research/oop-design.md` entries 33–36 and `docs/research/refactoring-empirical.md`. - ---- - -## The Definition - -A refactoring is a **behavior-preserving** transformation of internal structure. If the transformation changes observable behavior, it is not a refactoring β€” it is a feature change, and requires its own RED-GREEN-REFACTOR cycle. - ---- - -## The Green Bar Rule (absolute) - -**Refactoring is only permitted while all existing tests pass.** - -Every individual refactoring step must leave `test-fast` green. There are no exceptions. - ---- - -## The Two-Hats Rule - -Wear one hat at a time: - -| Hat | Activity | Allowed during this hat | -|---|---|---| -| **Feature hat** | RED β†’ GREEN | Write failing test, write minimum code to pass | -| **Refactoring hat** | REFACTOR | Restructure passing code; never add new behavior | - -**Never mix hats in the same step.** If you discover a refactoring is needed while making a test pass (GREEN), note it β€” finish GREEN first, then switch hats. - ---- - -## When to Use - -### 1. REFACTOR phase (opportunistic) - -After GREEN: `test-fast` passes for the current `@id`. Now restructure. - -### 2. Preparatory refactoring (before RED) - -When the current structure would make the next `@id` awkward to implement: -- Put on the **refactoring hat first** -- Refactor until the feature is easy to add -- Commit the preparatory refactoring separately (see Commit Discipline) -- Then put on the feature hat and run RED-GREEN-REFACTOR normally - -Beck: *"For each desired change, make the change easy (warning: this may be hard), then make the easy change."* - ---- - -## Step-by-Step - -### Step 1 β€” Identify the smell - -Run the smell checklist from your Self-Declaration or from the Architecture Smell Check. - -Smell categories from Shvets *Refactoring.Guru* (2014–present); each smell links to its Fowler catalogue entry. - -#### Bloaters β€” structures grown too large - -| Smell | Signal | Likely catalogue entry | -|---|---|---| -| Long Method | Method body needs a comment to understand any section | Extract Function, Decompose Conditional | -| Large Class | Class has too many responsibilities or instance variables | Extract Class, Extract Subclass | -| Primitive Obsession | Domain concept represented as a raw primitive | Replace Primitive with Object, Introduce Parameter Object | -| Long Parameter List | Function takes 3+ parameters, or parameter group repeats across signatures | Introduce Parameter Object, Replace Parameter with Query | -| Data Clumps | Same 2–3 data items always appear together across signatures or fields | Introduce Parameter Object, Extract Class | - -#### OO Abusers β€” misapplied OOP - -| Smell | Signal | Likely catalogue entry | -|---|---|---| -| Switch Statements | Repeated `if/elif` or match on a type flag across callers | Replace Conditional with Polymorphism, Strategy, State | -| Temporary Field | Instance variable set only in some code paths; `None` in others | Extract Class, Introduce Null Object | -| Refused Bequest | Subclass inherits methods/data it does not use or overrides to do nothing | Push Down Method/Field, Replace Inheritance with Delegation | -| Alternative Classes with Different Interfaces | Two classes do the same thing under different names/signatures | Rename Method, Extract Superclass, unify via Protocol | - -#### Change Preventers β€” changes ripple unexpectedly - -| Smell | Signal | Likely catalogue entry | -|---|---|---| -| Divergent Change | One class must change for multiple unrelated reasons | Extract Class (split by axis of change) | -| Shotgun Surgery | One concept change touches many classes | Move Function/Field, Inline Class, combine scattered behavior | -| Parallel Inheritance Hierarchies | Adding a subclass to one hierarchy forces a new subclass in another | Move Function/Field to flatten or unify hierarchies | - -#### Dispensables β€” dead weight - -| Smell | Signal | Likely catalogue entry | -|---|---|---| -| Comments | Comment explains *what* or *why* when the code could be self-explanatory | Extract Function, Rename Variable/Function | -| Duplicate Code | Same logic copied in 2+ places | Extract Function, Pull Up Method, Form Template Method | -| Lazy Class | Class does too little to justify its existence | Inline Class, Collapse Hierarchy | -| Data Class | Class holds only fields with getters/setters; no behavior | Move Function into class, Encapsulate Field | -| Dead Code | Unreachable code, unused variable, never-called function | Delete it | -| Speculative Generality | Abstractions added "for future use" with no current caller | Inline Class/Function, Remove unused parameters | - -#### Couplers β€” excessive inter-object dependency - -| Smell | Signal | Likely catalogue entry | -|---|---|---| -| Feature Envy | Method uses another class's data more than its own | Move Function, Extract Function | -| Inappropriate Intimacy | Class accesses another's private fields or implementation details | Move Function/Field, Extract Class, Replace Inheritance with Delegation | -| Message Chains | `a.b().c().d()` β€” navigating a chain of objects | Hide Delegate, Extract Function to encapsulate the chain | -| Middle Man | Class delegates most of its methods to another class | Inline Class, Remove Middle Man | -| Incomplete Library Class | External class lacks a needed method | Introduce Foreign Method, Introduce Extension Object | - -If pattern smell detected: load `skill apply-patterns` for pattern selection guidance. - -### Step 2 β€” Apply one catalogue entry at a time - -Apply a **single** catalogue entry, then run `test-fast` before moving to the next. - -Never batch multiple catalogue entries into one step β€” you lose the ability to pinpoint which step broke something. - -### Step 3 β€” Run after each step - -```bash -uv run task test-fast -``` - -All tests green β†’ proceed to next catalogue entry. -Any test red β†’ see "When a Refactoring Breaks a Test" below. - -### Step 4 β€” Commit when smell-free - -Once no smells remain and `test-fast` is green: - -```bash -uv run task test-fast # must pass -``` - -Commit (see Commit Discipline below). - ---- - -## Key Catalogue Entries - -### Extract Function -Pull a cohesive fragment into a named function. - -**Trigger**: a fragment needs a comment to explain what it does. -**Outcome**: the extracted function's name makes the comment unnecessary; the caller reads as a sequence of named steps. - -### Extract Class -Split a class that is doing two jobs. - -**Trigger**: a data cluster (2–3 fields that always travel together) with related behaviour that could be named independently. -**Outcome**: each class has one reason to change; the new class becomes a value object or a collaborator. - -### Introduce Parameter Object -Replace a recurring parameter group with a dedicated object. - -**Trigger**: the same 2+ parameters appear together across multiple function signatures. -**Outcome**: a named type captures the concept; callers are simplified; the object can later carry behaviour. - -### Replace Primitive with Object -Elevate a domain concept represented as a raw primitive to its own type. - -**Trigger**: a primitive has validation rules, formatting logic, or operations that are repeated at every call site. -**Outcome**: behaviour moves into the type; callers are protected from invalid states; the type can be named and tested independently. - -### Decompose Conditional / Guard Clauses -Flatten nested conditional logic to ≀2 levels. - -**Trigger**: OC-1 violation (nesting beyond one indent level per method), or multi-level nested `if` chains. -**Outcome**: each exit condition is expressed as an early return (guard clause); the happy path is at the left margin; no `else` after `return`. - ---- - -## When a Refactoring Breaks a Test - -A refactoring that breaks a test is **not a refactoring**. Stop. Diagnose: - -### Diagnosis flow - -``` -Test fails after a structural change - β”‚ - β–Ό -Is the test testing internal structure -(private methods, specific call chains, -concrete types) rather than observable behavior? - β”‚ - YES β”‚ NO - β”‚ └──→ The "refactoring" changed observable behavior. - β”‚ This is a FEATURE CHANGE. - β”‚ Revert the step. - β”‚ Put on the feature hat. - β”‚ Run RED-GREEN-REFACTOR for it explicitly. - β–Ό -Rewrite the test to use the public interface. -Re-apply the refactoring step. -Run test-fast β€” must be green. -``` - -**Never delete a failing test without diagnosing it first.** - ---- - -## Commit Discipline - -Refactoring commits are always **separate** from feature commits. - -| Commit type | Message format | When | -|---|---|---| -| Preparatory refactoring | `refactor(): ` | Before RED, to make the feature easier | -| REFACTOR phase | `refactor(): ` | After GREEN, cleaning up the green code | -| Feature addition | `feat(): ` | After GREEN (never mixed with refactor) | - -Never mix a structural cleanup with a behavior addition in one commit. This keeps history bisectable and CI green at every commit. - ---- - -## Self-Declaration Check (before exiting REFACTOR) - -Before marking the `@id` complete, verify all of the following. Each failed item is a smell β€” apply the catalogue entry, run `test-fast`, then re-check. - -### Green Bar -- [ ] `test-fast` passes -- [ ] No smell from the checklist in Step 1 remains - -### Object Calisthenics (Bay 2005) -| Rule | Constraint | Violation signal | -|---|---|---| -| OC-1 | One indent level per method | `for` inside `if` inside a method body | -| OC-2 | No `else` after `return` | `if cond: return x` then `else: return y` | -| OC-3 | Wrap primitives with domain meaning | `def process(user_id: int)` instead of `UserId` | -| OC-4 | Wrap collections with domain meaning | `list[Order]` passed around instead of `OrderCollection` | -| OC-5 | One dot per line | `obj.repo.find(id).name` | -| OC-6 | No abbreviations | `usr`, `mgr`, `cfg`, `val`, `tmp` | -| OC-7 | Classes ≀ 50 lines, methods ≀ 20 lines | Any method requiring scrolling | -| OC-8 | ≀ 2 instance variables per class *(behavioural classes only; dataclasses, Pydantic models, value objects, and TypedDicts are exempt)* | `__init__` with 3+ `self.x =` assignments in a behavioural class | -| OC-9 | No getters/setters | `def get_name(self)` / `def set_name(self, v)` | - -### SOLID (Martin 2000) -| Principle | Check | Violation signal | -|---|---|---| -| **S** β€” Single Responsibility | Does this class have exactly one reason to change? | Class handles data + formatting, or business logic + persistence | -| **O** β€” Open/Closed | Can new behavior be added without editing this class? | Adding a case requires editing an `if/elif` chain inside the class | -| **L** β€” Liskov Substitution | Do all subtypes honor the full contract of their base type? | Subclass raises on an inherited method, or narrows a precondition | -| **I** β€” Interface Segregation | Does every implementor use every method in the interface? | Implementors stub out methods they don't need | -| **D** β€” Dependency Inversion | Does domain code depend only on abstractions, not concrete I/O? | Domain class directly imports a database, file, or network class | - -### Law of Demeter / Tell, Don't Ask / CQS - -**Law of Demeter** β€” a method should only call methods on: `self`, its parameters, objects it creates, and its direct components. -- Violation signal: chaining through two or more intermediaries (`a.b().c()`). Ask `a` to do the thing instead of navigating through it. - -**Tell, Don't Ask** β€” tell objects what to do; don't query their state and decide externally. -- Violation signal: querying an object's status field, then setting it based on that query from outside the object. Move the decision into the object itself. - -**Command-Query Separation** β€” a method either changes state (command) or returns a value (query), never both. -- Apply to domain objects. Standard library collections are a known exception (e.g., pop-style methods). - -### Design Clarity Signals - -| Principle | Signal | -|---|---| -| Explicit over implicit | Dependencies stated at construction; no hidden side effects or magic initialization | -| Simple over complex | One function, one job; prefer a plain function over a class when no state is needed | -| Flat over nested | OC-1 β€” one indent level per method; early returns over deep nesting | -| Readability | OC-6 β€” no abbreviations; public items documented | -| Errors surface explicitly | Raise on invalid input; never silently swallow errors or return a default that hides failure | -| No ambiguous defaults | Invalid input raises; callers are never surprised by silent fallbacks | - -### Type and documentation hygiene -- [ ] Type annotations present on all public signatures -- [ ] Documentation present on all public classes and methods diff --git a/.opencode/skills/run-session/SKILL.md b/.opencode/skills/run-session/SKILL.md deleted file mode 100644 index 938f044..0000000 --- a/.opencode/skills/run-session/SKILL.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -name: run-session -version: "5.0" -description: Session start and end protocol β€” read FLOW.md, auto-detect state, resume from checkpoint, update and commit -author: software-engineer -audience: all-agents -workflow: session-management ---- - -# Session Workflow - -Every session starts by reading state. Every session ends by writing state. This makes any agent able to continue from where the last session stopped. - -The single source of state is `FLOW.md` in the project root. It tracks the current feature, branch, detected workflow state, and next action. - -## Read Policy - -Each agent reads only what is operationally necessary for their current step. Do not read files "for context" unless the step explicitly requires it. - -| Agent | Reads | -|---|---| -| PO (Step 1) | `FLOW.md`, `scope_journal.md` (resume check), `system.md`, `glossary.md`, `domain-model.md` (read-only, entity check), `docs/post-mortem/` (selective scan), in-progress `.feature` | -| SA (Step 2) | `FLOW.md`, `system.md`, `glossary.md`, in-progress `.feature`, targeted `.py` files | -| SE (Step 3) | `FLOW.md`, `system.md`, `glossary.md`, in-progress `.feature`, targeted `.py` files | -| SA (Step 4) | `FLOW.md`, `system.md`, `glossary.md`, `domain-model.md`, in-progress `.feature`, ADR files referenced in `system.md` | - -## Session Start - -1. **Read `FLOW.md`** β€” find current feature, current branch, detected status, and the "Next" line. - - If `FLOW.md` does not exist, create it from `.opencode/skills/flow/flow.md.template` - - If `FLOW.md` exists but is empty or malformed, recreate from template -2. **Run `detect-state`** β€” execute the auto-detection rules from `skill flow` to determine the actual workflow state from filesystem and git state. - - If detected state differs from `FLOW.md` Status, update `FLOW.md` to match reality -3. **Check prerequisites** β€” verify the Prerequisites table in `FLOW.md`. If any are unchecked, stop and report. -4. **If you are the PO** and Step 1 (SCOPE) is active: check `docs/scope_journal.md` for the most recent session block. - - If the most recent block has `Status: IN-PROGRESS` β†’ the previous session was interrupted. Resume it before starting a new session: finish updating `.feature` files and `docs/discovery.md`, then mark the block `Status: COMPLETE`. -5. If a feature is active at Step 2–5, read: - - `docs/features/in-progress/.feature` β€” feature file (Rules + Examples + @id) - - `docs/system.md` β€” current system overview and constraints -6. Run `git status` β€” understand what is committed vs. what is not -7. **If Step 2–5 is active**: run `git branch --show-current` and verify: - - **SA at Step 2 or Step 4**: must be on `feat/` or `fix/`. If on `main`, stop β€” load `skill version-control` and create the branch first. - - **SE at Step 3**: must be on `feat/` or `fix/`. If on `main`, stop β€” load `skill version-control` and create/switch to the branch first. -8. Confirm scope: you are working on exactly one step of one feature - -**If FLOW.md Status is [IDLE] or says "No feature in progress":** - -- **PO**: Load `skill select-feature` β€” it guides you through scoring and selecting the next BASELINED backlog feature. You must verify the feature has `Status: BASELINED` before moving it to `in-progress/`. Only you may move it. -- **Software-engineer or system-architect**: Update `FLOW.md` `Next:` line to `Run @product-owner β€” load skill select-feature and pick the next BASELINED feature from backlog.` Then **stop**. Never self-select a feature. Never create, edit, or move a `.feature` file. - -## Session End - -1. Update `FLOW.md`: - - Set Status to the detected state - - Append to Session Log with timestamp, agent, state, and action - - Update the "Next" line with one concrete action -2. Commit any uncommitted work (even WIP): - ```bash - git add -A - git commit -m "WIP(): " - ``` -3. If a step is fully complete, use the proper commit message instead of WIP. - -## Step Completion Protocol - -When a step completes within a session: - -1. Update `FLOW.md` to reflect the completed step before doing any other work. -2. Commit the `FLOW.md` update: - ```bash - git add FLOW.md - git commit -m "chore: complete step for " - ``` -3. Only then begin the next step (in a new session where possible β€” see Rule 4). - -## FLOW.md Format - -```markdown -# FLOW Protocol - -## Current Feature -**Feature**: | [NONE] -**Branch**: | [NONE] -**Status**: - -## Prerequisites -- [x] Agents: product-owner, system-architect, software-engineer -- [x] Skills: run-session, define-scope, architect, implement, verify, version-control -- [x] Tools: uv, git -- [x] Directories: docs/features/, docs/adr/ - -## Session Log -**YYYY-MM-DD HH:MM** β€” β€” β€” - -## Next -Run @ β€” -``` - -**"Next" line format**: Always prefix with `Run @` so the human knows exactly which agent to invoke. Agent names are defined in `AGENTS.md` β€” use the name exactly as listed there. Examples: -- `Run @software-engineer β€” implement @id:a1b2c3d4 (Step 3 RED)` -- `Run @system-architect β€” load skill architect and begin Step 2 (Architecture) for ` -- `Run @system-architect β€” verify feature at Step 4` -- `Run @product-owner β€” pick next BASELINED feature from backlog` -- `Run @product-owner β€” accept feature at Step 5` - -## Rules - -1. Never skip reading `FLOW.md` at session start -2. Never end a session without updating `FLOW.md` -3. Never leave uncommitted changes β€” commit as WIP if needed -4. One step per session where possible; do not start Step N+1 in the same session as Step N -5. The "Next" line must be actionable enough that a fresh AI can execute it without asking questions -6. When a step completes, update `FLOW.md` and commit **before** any further work -7. The Session Log is append-only β€” never delete old entries -8. If `FLOW.md` is missing, create it from `.opencode/skills/flow/flow.md.template` before doing any other work -9. If detected state differs from `FLOW.md` Status, trust the detected state and update `FLOW.md` -10. Output is minimal-signal: findings, status, decisions, blockers, Next: line only. Use the fewest, least verbose tool calls necessary. Report results, not process. No redundant prose. - -## Output Style - -Use minimal output. Every message must contain only what the next agent or stakeholder needs to continue β€” findings, status, decisions, blockers, and the Next: line. - -- Use the fewest, least verbose tool calls necessary to achieve the step's goal -- Report results, not process ("3 files changed" not "I ran git status and it showed...") -- No narration before or after tool calls -- No restating tool output in prose -- No summaries of what was just done -- Always close with Next: diff --git a/.opencode/skills/select-feature/SKILL.md b/.opencode/skills/select-feature/SKILL.md deleted file mode 100644 index f6583ff..0000000 --- a/.opencode/skills/select-feature/SKILL.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -name: select-feature -description: Score and select the next backlog feature by value, effort, and dependencies -version: "1.0" -author: product-owner -audience: product-owner -workflow: feature-lifecycle ---- - -# Feature Selection - -Select the next most valuable, unblocked feature from the backlog using a lightweight scoring model grounded in flow economics and dependency analysis. - -**Research basis**: Weighted Shortest Job First (WSJF) β€” Reinertsen *Principles of Product Development Flow* (2009); INVEST criteria β€” Wake (2003); Kano model β€” Kano (1984); Dependency analysis β€” PMBOK Critical Path Method. See `docs/research/requirements-elicitation.md`. - -**Core principle**: Cost of Delay Γ· Duration. Features with high user value and low implementation effort should start first. Features blocked by unfinished work should wait regardless of value. - -## When to Use - -Load this skill when `FLOW.md` Status is [IDLE] β€” before moving any feature to `in-progress/`. - -## Step-by-Step - -### 1. Verify WIP is Zero - -```bash -ls docs/features/in-progress/ -``` - -- 0 files β†’ proceed -- 1 file β†’ a feature is already in progress; do not start another; exit this skill -- >1 files β†’ WIP violation; stop and resolve before proceeding - -### 2. List BASELINED Candidates - -Read each `.feature` file in `docs/features/backlog/`. Check its discovery section for `Status: BASELINED`. - -- Non-BASELINED features are not eligible β€” they need Step 1 (scope) first -- If no BASELINED features exist: inform the stakeholder; run `@product-owner` with `skill define-scope` to baseline the most promising backlog item first - -**IMPORTANT** - -**NEVER move a feature to `in-progress/` unless its discovery section has `Status: BASELINED`. Only the PO may move `.feature` files β€” no other agent ever creates, edits, or moves them.** - -### 3. Score Each Candidate - -For each BASELINED feature, fill this table: - -| Feature | Value (1–5) | Effort (1–5) | Dependency (0/1) | WSJF | -|---|---|---|---|---| -| `` | | | | Value Γ· Effort | - -**Value (1–5)** β€” estimate user/business impact: -- 5: Must-have β€” core workflow blocked without it (Kano: basic need) -- 4: High β€” significantly improves the primary use case -- 3: Medium β€” useful but not blocking (Kano: performance) -- 2: Low β€” nice-to-have (Kano: delighter) -- 1: Minimal β€” cosmetic or out-of-scope edge case - -Use the number of `Must` Examples in the feature's `Rule:` blocks as a tiebreaker: more Musts β†’ higher value. - -**Effort (1–5)** β€” estimate implementation complexity: -- 1: Trivial β€” 1–2 `@id` Examples, no new domain concepts -- 2: Small β€” 3–5 `@id` Examples, one new domain entity -- 3: Medium β€” 6–8 `@id` Examples or cross-cutting concern -- 4: Large β€” >8 Examples or multiple interacting domain entities -- 5: Very large β€” spans multiple modules or has unknown complexity - -**Dependency (0/1)** β€” does this feature assume another backlog feature is already built? -- 0: Independent β€” no hard prerequisite -- 1: Blocked β€” requires another backlog feature to be completed first - -A Dependency=1 feature is **ineligible for selection** regardless of WSJF score. Apply WSJF only to Dependency=0 features. - -### 4. Select - -Pick the BASELINED, Dependency=0 feature with the highest WSJF score. - -Ties: prefer higher Value (user impact matters more than effort optimization). - -If all BASELINED features have Dependency=1: stop and resolve the blocking dependency first β€” select and complete the depended-upon feature. - -### 5. Move and Update FLOW.md - -```bash -mv docs/features/backlog/.feature docs/features/in-progress/.feature -``` - -Update `FLOW.md`: - -```markdown -# FLOW Protocol - -## Current Feature -**Feature**: -**Branch**: [NONE] -**Status**: [STEP-1-DISCOVERY] or [STEP-2-READY] β€” whichever is next - -## Prerequisites -- [x] Agents: product-owner, system-architect, software-engineer -- [x] Skills: run-session, define-scope, architect, implement, verify, version-control -- [x] Tools: uv, git -- [x] Directories: docs/features/, docs/adr/ - -## Session Log -**YYYY-MM-DD HH:MM** β€” product-owner β€” [IDLE] β†’ [] β€” selected from backlog - -## Next -Run @ β€” -``` - -- If the feature has no `Rule:` blocks yet β†’ Step 1 (SCOPE): `Run @product-owner β€” load skill define-scope and write stories` -- If the feature has `Rule:` blocks but no `@id` Examples β†’ Step 1 Stage 2 Step B (Criteria): `Run @product-owner β€” load skill define-scope and write acceptance criteria` -- If the feature has `@id` Examples β†’ Step 2 (ARCH): `Run @system-architect β€” load skill architect and write architecture stubs` - -### 6. Commit - -```bash -git add docs/features/in-progress/.feature FLOW.md -git commit -m "chore: select as next feature" -``` - -## Checklist - -- [ ] `in-progress/` confirmed empty before selection -- [ ] Only BASELINED features considered -- [ ] Dependency=1 features excluded from scoring -- [ ] WSJF scores filled for all candidates -- [ ] Selected feature has highest WSJF among Dependency=0 candidates -- [ ] Feature moved to `in-progress/` -- [ ] `FLOW.md` updated with correct Status and `Next` line -- [ ] Changes committed diff --git a/.opencode/skills/update-docs/SKILL.md b/.opencode/skills/update-docs/SKILL.md deleted file mode 100644 index 05b9af5..0000000 --- a/.opencode/skills/update-docs/SKILL.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -name: update-docs -description: Generate and update C4 architecture diagrams, living glossary, and system overview from existing project docs -version: "2.0" -author: product-owner -audience: product-owner -workflow: feature-lifecycle ---- - -# Living Docs - -This skill generates and updates two living documents after a feature is accepted (Step 5) or on stakeholder request: the **C4 architecture diagrams** and the **living glossary**. Both are derived from existing project documentation β€” no new decisions are made. - -The glossary is a secondary artifact derived from the code, the domain model, and domain-expert conversations. The canonical sources are the completed feature files, the discovery synthesis, and the architectural decisions. The glossary is a human-readable projection of those sources β€” not an independent authority. - -## When to Use - -- **As part of the release process (Step 5)** β€” the `git-release` skill calls this skill inline at step 5, before the version-bump commit. Do not commit separately; the release process stages all files together. -- **Stakeholder on demand** β€” when the stakeholder asks "what does the system look like?" or "what does term X mean in this context?". In this case, commit with the standalone message in Step 5 below. - -## Ownership Rules - -| Document | Created/Updated by | Inputs read | -|---|---|---| -| `docs/context.md` | `update-docs` skill (PO) | `docs/discovery.md`, `docs/features/completed/` | -| `docs/container.md` | `update-docs` skill (PO) | `docs/adr/ADR-*.md`, `docs/features/completed/` | -| `docs/glossary.md` | `update-docs` skill (PO) | `docs/domain-model.md`, `docs/glossary.md` (existing), `docs/adr/ADR-*.md`, `docs/features/completed/` | -| `docs/system.md` | SA (Step 2), PO reviews (Step 5) | `docs/discovery.md`, `docs/adr/ADR-*.md`, `docs/features/completed/` | -| `docs/discovery.md` | PO only (Step 1) | β€” | -| `docs/domain-model.md` | SA only (Step 2) | β€” | - -**Never edit `docs/adr/ADR-*.md`, `docs/discovery.md`, or `docs/domain-model.md` in this skill.** Those files are owned by their respective agents. This skill reads them; it never writes to them. - ---- - -## Step 1 β€” Read Phase (all before writing anything) - -Read in this order: - -1. `docs/discovery.md` β€” project scope, feature list per session -2. `docs/domain-model.md` β€” all entities, nouns, verbs, bounded contexts -3. `docs/features/completed/` β€” all completed `.feature` files (full text: Rules, Examples, Constraints) -4. `docs/adr/` β€” all architectural decision files (containers, modules, protocols, external deps) -5. `docs/context.md` and `docs/container.md` β€” existing C4 diagrams if they exist (update, do not replace from scratch) -6. `docs/glossary.md` β€” existing glossary if it exists (extend, never remove existing entries) -7. `docs/branding.md` β€” if present, read `Visual > Primary color` and `Accent color`. Apply to C4 Mermaid diagrams via `%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '', 'lineColor': ''}}}%%`. If absent or fields blank, use Mermaid defaults. - -Identify from the read phase: - -- **Actors** β€” named human roles from feature `As a ` clauses and discovery Scope section -- **External systems** β€” any system outside the package boundary named in features or architecture decisions -- **Containers** β€” deployable/runnable units identified in ADR files (Hexagonal adapters, CLIs, services) -- **Key domain terms** β€” all nouns and verbs from `docs/domain-model.md`, plus any terms defined in ADR decisions - ---- - -## Step 2 β€” Update C4 Context Diagram (Level 1) - -File: `docs/context.md` - -The Context diagram answers: **who uses the system and what external systems does it interact with?** - -Use Mermaid `C4Context` syntax. Use the template in `context.md.template` in this skill's directory. - -Rules: -- One `Person(...)` per distinct actor found in completed feature files -- One `System_Ext(...)` per external dependency identified in ADR files -- Relationships (`Rel`) use verb phrases from feature `When` clauses or architecture decision labels -- If no external systems are identified in ADRs, omit `System_Ext` entries -- If the file already exists: update only β€” add new actors/systems, update relationship labels. Never remove an existing entry unless the feature it came from has been explicitly superseded - ---- - -## Step 3 β€” Update C4 Container Diagram (Level 2) - -File: `docs/container.md` - -The Container diagram answers: **what are the major runnable/deployable units and how do they communicate?** - -Only generate this diagram if `docs/adr/` contains at least one decision identifying a distinct container boundary (e.g., a CLI entry point separate from a library, a web server, a background worker, an external service adapter). If the project is a single-container system, note this in the file and skip the diagram body. - -Use Mermaid `C4Container` syntax. Use the template in `container.md.template` in this skill's directory. - -Rules: -- Container names and responsibilities come directly from ADR decisions β€” do not invent them -- Technology labels come from `pyproject.toml` dependencies when identifiable (e.g., "Python / fire CLI", "Python / FastAPI") -- If the file already exists: update incrementally β€” do not regenerate from scratch - ---- - -## Step 4 β€” Update Living Glossary - -File: `docs/glossary.md` - -The glossary answers: **what does each domain term mean in this project's context?** - -Use the template in `glossary.md.template` in this skill's directory. - -### Rules - -- Extract all entities and verbs from `docs/domain-model.md` -- Extract all roles from `As a ` clauses in completed `.feature` files -- Extract all external system names from ADR decisions -- Extract any term defined or clarified in architectural decision `Reason:` fields -- **Do not remove existing glossary entries** β€” if a term's meaning has changed, add a `**Superseded by:**` line pointing to the new entry and write a new entry -- **Every term must have a traceable source** β€” completed feature files or ADR decisions. If a term appears in sources but is never defined, write `Definition: Term appears in [source] but has not been explicitly defined.` Do not invent a definition. -- Terms are sorted alphabetically within the file - -### Merge with existing glossary - -If `docs/glossary.md` already exists: -1. Read all existing entries -2. For each new term found in sources: check if it already exists in the glossary - - Exists, definition unchanged β†’ skip - - Exists, definition changed β†’ append `**Superseded by:** ` to old entry; write new entry - - Does not exist β†’ append new entry in alphabetical order - ---- - -## Step 5 β€” Commit - -**When called from the release process**: skip this step β€” the `git-release` skill stages and commits all files together. - -**When run standalone** (stakeholder on demand): commit after all diagrams and glossary are updated: - -``` -docs(update-docs): update C4 and glossary after -``` - -If triggered without a specific feature (general refresh): - -``` -docs(update-docs): refresh C4 diagrams and glossary -``` - ---- - -## Checklist - -- [ ] Read all source files before writing anything (including `docs/branding.md` if present) -- [ ] Context diagram reflects all actors from completed feature files -- [ ] Context diagram reflects all external systems from ADR files -- [ ] Container diagram present only if multi-container architecture confirmed in ADR files -- [ ] Glossary contains all entities and verbs from `docs/domain-model.md` -- [ ] No existing glossary entry removed -- [ ] Every new term has a traceable source in completed feature files or ADRs; no term is invented -- [ ] No edits made to ADR files, `docs/discovery.md`, or `docs/domain-model.md` -- [ ] If standalone: committed with `docs(update-docs): ...` message -- [ ] If called from release: files staged but not committed (release process commits) - ---- - -## Templates - -All templates for files written by this skill live in this skill's directory: - -- `context.md.template` β€” `docs/context.md` structure -- `container.md.template` β€” `docs/container.md` structure -- `glossary.md.template` β€” `docs/glossary.md` entry format - -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/update-docs -Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. -Note: file list is sampled. - - -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/container.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/context.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/glossary.md.template - diff --git a/.opencode/skills/update-docs/container.md.template b/.opencode/skills/update-docs/container.md.template deleted file mode 100644 index cea7bad..0000000 --- a/.opencode/skills/update-docs/container.md.template +++ /dev/null @@ -1,24 +0,0 @@ -# C4 β€” Container Diagram - -> Last updated: YYYY-MM-DD -> Source: docs/adr/ADR-*.md - -```mermaid -C4Container - title Container Diagram β€” - - Person(actor1, "", "") - - System_Boundary(sys, "") { - Container(container1, "", "", "") - Container(container2, "", "", "") - } - - System_Ext(ext1, "", "") - - Rel(actor1, container1, "") - Rel(container1, container2, "") - Rel(container1, ext1, "") -``` - -> Note: Only generate this diagram if `docs/adr/` contains at least one decision identifying a distinct container boundary. If the project is a single-container system, state that here and skip the diagram body. diff --git a/.opencode/skills/update-docs/context.md.template b/.opencode/skills/update-docs/context.md.template deleted file mode 100644 index bab9611..0000000 --- a/.opencode/skills/update-docs/context.md.template +++ /dev/null @@ -1,18 +0,0 @@ -# C4 β€” System Context - -> Last updated: YYYY-MM-DD -> Source: docs/discovery.md, docs/features/completed/ - -```mermaid -C4Context - title System Context β€” - - Person(actor1, "", "") - - System(system, "", "<3–5 word system description from discovery.md Scope>") - - System_Ext(ext1, "", "") - - Rel(actor1, system, "") - Rel(system, ext1, "") -``` diff --git a/.opencode/skills/update-docs/glossary.md.template b/.opencode/skills/update-docs/glossary.md.template deleted file mode 100644 index 32676e5..0000000 --- a/.opencode/skills/update-docs/glossary.md.template +++ /dev/null @@ -1,18 +0,0 @@ -# Glossary β€” - -> Living document. Updated after each completed feature by the `update-docs` skill. -> Source: docs/discovery.md, docs/features/completed/, docs/adr/ADR-*.md - ---- - -## - -**Type:** Noun | Verb | Domain Event | Concept | Role | External System - -**Definition:** - -**Bounded context:** - -**First appeared:** - ---- diff --git a/.opencode/skills/verify/SKILL.md b/.opencode/skills/verify/SKILL.md deleted file mode 100644 index 836791f..0000000 --- a/.opencode/skills/verify/SKILL.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -name: verify -description: Step 4 β€” run all verification commands, review code quality, and produce a written report -version: "6.0" -author: system-architect -audience: system-architect -workflow: feature-lifecycle ---- - -# Verify - -This skill guides the system-architect through Step 4: adversarial verification that the feature works correctly and respects the architecture designed in Step 2. The output is a written report with a clear APPROVED or REJECTED decision. - -**Your default hypothesis is that the code is broken despite passing automated checks. You designed the architecture; you know what should have been preserved. Your job is to find the failure mode. If you cannot find one after thorough investigation, APPROVE. If you find one, REJECTED.** - -**Every PASS/FAIL cell must have evidence.** Empty evidence = UNCHECKED = REJECTED. - -**You never move, create, or edit `.feature` files.** After producing an APPROVED report: update FLOW.md `Next:` to `Run @product-owner β€” accept feature at Step 5.` then stop. The PO accepts the feature and moves the file. - -The system-architect produces one written report (see template below) that includes: all gate results, the SE Self-Declaration Audit, the **Architect Review Stance Declaration**, and the final APPROVED/REJECTED verdict. Do not start until the software-engineer has committed all work and communicated the Self-Declaration verbally in the handoff message. - -## When to Use - -Load this skill when the software-engineer signals Step 3 complete and hands off for review (Step 4). Do not load it earlier. - -## Step-by-Step - -### 1. Read the Feature Docs - -Read `docs/features/in-progress/.feature`. Extract: -- All `@id` tags and their Example titles from `Rule:` blocks -- The interaction model (if the feature involves user interaction) -- The current-state overview in `docs/system.md` -- `docs/domain-model.md` β€” verify naming consistency of new classes/methods against existing entities -- `docs/glossary.md` β€” verify domain terms are used correctly -- The software-engineer's Self-Declaration (communicated verbally in the handoff message) - -Only read specific ADR files if `docs/system.md` references them as relevant to this feature. - -### 2. pyproject.toml Gate - -```bash -git diff main -- pyproject.toml -``` - -Any change β†’ REJECT immediately. The software-engineer must revert and get stakeholder approval. - -### 3. Branch Gate - -```bash -git branch --show-current -``` - -- Must output `feat/` or `fix/`. If `main` β†’ REJECT immediately β€” the SE is working on the wrong branch. - -```bash -git log main..HEAD --oneline -``` - -- Must show 1+ commits. If empty β†’ REJECT β€” nothing was committed on this branch. - -```bash -git merge-tree $(git merge-base HEAD main) HEAD main -``` - -- Empty output = clean merge possible. Non-empty output = conflicts exist β†’ REJECT β€” the SE must resolve conflicts on the feature branch before handoff. - -### 4. Check Commit History - -```bash -git log --oneline -20 -git status -``` - -Verify: -- Commits follow conventional commit format -- No "fix tests", "wip", "temp" commits -- No uncommitted changes: `git status` should be clean - -### 5. Production-Grade Gate - -Run before semantic review. If any row is FAIL, stop immediately with REJECTED. - -| Check | How to check | PASS | FAIL | Fix | -|---|---|---|---|---| -| App exits cleanly | `timeout 10s uv run task run` | Exit 0 or non-124 | Exit 124 (timeout/hang) | Fix the hang | -| Output changes when input changes | Run app, change an input or condition, observe output | Output changes accordingly | Output is static | Implement real logic | - -### 6. Self-Declaration Audit - -**Completeness check (hard gate β€” REJECT if failed)**: Count the numbered items in the SE's Self-Declaration. The template in `implement/SKILL.md` has exactly 25 items numbered 1–25. If the count is not 25, or any number in the sequence 1–25 is missing, REJECT immediately β€” do not proceed to item-level audit. - -Read the software-engineer's Self-Declaration from the handoff message. - -For every **AGREE** claim: -- Find the `file:line` β€” does it hold? - -For every **DISAGREE** claim: -- Read the justification carefully. -- If the constraint genuinely falls outside the SE's control (e.g. external library forces method chaining, dataclass/Pydantic/TypedDict exemption for ≀2 ivars): accept with a note in the report and suggest the closest compliant alternative if one exists. -- If the justification is weak, incomplete, or a best-practice alternative exists that the SE did not consider: REJECT with the specific alternative stated. -- If there is no justification: REJECT. - -Undeclared violations found during semantic review β†’ REJECT. - -### 7. Code Review - -Read the source files changed in this feature. **Do this before running lint/static-check/test** β€” if semantic review finds a design problem, commands will need to re-run after the fix anyway. - -**Stop on first failure category β€” do not accumulate issues.** - -#### 6a. Correctness β€” any FAIL β†’ REJECTED - -| Check | How to check | PASS | FAIL | Fix | -|---|---|---|---|---| -| No dead code | Read for unreachable statements, unused variables, impossible branches | None found | Any found | Remove or fix | -| No duplicate logic (DRY) | Search for repeated blocks doing the same thing | None found | Duplication found | Extract to shared function | -| No over-engineering (YAGNI) | Check for abstractions with no current use | None found | Unused abstraction | Remove unused code | - -#### 6b. Simplicity (KISS) β€” any FAIL β†’ REJECTED - -| Check | How to check | PASS | FAIL | Fix | -|---|---|---|---|---| -| Functions do one thing | Read each function; can you describe it without `and`? | Yes | No | Split into focused functions | -| Nesting ≀ 2 levels | Count indent levels in each function | ≀ 2 | > 2 | Extract inner block | -| Functions ≀ 20 lines | Count lines | ≀ 20 | > 20 | Extract helper | -| Classes ≀ 50 lines | Count lines | ≀ 50 | > 50 | Split class | - -#### 6c. Naming Consistency β€” any FAIL β†’ REJECTED - -| Check | How to check | PASS | FAIL | -|---|---|---|---| -| Classes match domain model | New class names appear in `docs/domain-model.md` or are justified | Yes | No | -| Methods match glossary | New method names use terms from `docs/glossary.md` | Yes | No | -| No invented synonyms | Same concept uses same name everywhere | Yes | No | - -If a new name is genuinely needed (not in domain model or glossary), the SE should have noted it in the handoff summary or in `docs/discovery.md`. If no justification exists, REJECT. - -#### 6d. SOLID β€” any FAIL β†’ REJECTED - -| Principle | Why it matters | What to check | How to check | -|---|---|---|---| -| SRP | Multiple change-reasons accumulate bugs | Each class/function has one reason to change | Count distinct concerns | -| OCP | Modifying existing code invalidates tests | New behavior via extension, not modification | Check if adding new case required editing existing class | -| LSP | Substitution failures cause silent errors | Subtypes behave identically to base | Check for narrowed contracts | -| ISP | Fat interfaces force unused methods | No Protocol forces stub implementations | Check for NotImplementedError | -| DIP | Concrete I/O makes unit testing impossible | High-level depends on abstractions | Check domain imports no I/O/DB | - -#### 6e. Object Calisthenics β€” any FAIL β†’ REJECTED - -Load `skill apply-patterns` and apply the full OC checklist (9 rules). Record a PASS/FAIL with `file:line` evidence for each rule. Rules 1 and 7 (nesting and entity size) share thresholds with 6b above. - -#### 6f. Design Patterns β€” any FAIL β†’ REJECTED - -| Code smell | Pattern missed | How to check | -|---|---|---| -| Multiple if/elif on type/state | State or Strategy | Search for `isinstance` chains | -| Complex `__init__` | Factory or Builder | Check line count and side effects | -| Callers know multiple components | Facade | Check caller coupling | -| External dep without Protocol | Repository/Adapter | Check dep injection | -| 0 domain classes, many functions | Missing domain model | Count classes vs functions | - -#### 6g. Tests β€” any FAIL β†’ REJECTED - -| Check | How to check | PASS | FAIL | -|---|---|---|---| -| Docstring format | Read each test docstring | Given/When/Then only | Extra metadata | -| Contract test | Would test survive internal rewrite? | Yes | No | -| No internal attribute access | Search for `_x` in assertions | None found | `_x`, `isinstance`, `type()` | -| Every `@id` has a mapped test | Match `@id` to test functions | All mapped | Missing test | -| No orphaned skipped stubs | Search for `@pytest.mark.skip` in `tests/features/` | None found | Any found β€” stub was written but never implemented | -| Function naming | Matches `test__<8char_hex>` | All match | Mismatch | -| Hypothesis tests have `@slow` | Read every `@given` for `@slow` marker | All present | Any missing | - -#### 6h. Code Quality β€” any FAIL β†’ REJECTED - -| Check | How to check | PASS | FAIL | -|---|---|---|---| -| No `noqa` comments | `grep -r "noqa" /` | None found | Any found | -| No `type: ignore` | `grep -r "type: ignore" /` | None found | Any found | -| Public functions have type hints | Read signatures | All annotated | Missing | -| Public functions have docstrings | Read source | Google-style | Missing | - -### 8. Run Verification Commands - -```bash -uv run task lint -uv run task static-check -uv run task test -``` - -Expected for each: exit 0, no errors. Record exact output on failure. - -If a command fails, stop and REJECT immediately. Do not run subsequent commands. - -### 9. Interactive Verification - -If the feature involves user interaction: run the app, provide real input, verify output changes. - -Record what input was given and what output was observed. - -### 10. Write the Report - -```markdown -## Step 4 Verification Report β€” - -### pyproject.toml Gate -| Check | Result | Notes | -|---|---|---| -| No changes from main | PASS / FAIL | | - -### Branch Gate -| Check | Result | Notes | -|---|---|---| -| On feat/ or fix/ | PASS / FAIL | | -| Commits ahead of main | PASS / FAIL | | -| No merge conflicts with main | PASS / FAIL | | - -### Production-Grade Gate -| Check | Result | Notes | -|---|---|---| -| App exits cleanly | PASS / FAIL / TIMEOUT | | -| Output driven by input | PASS / FAIL | | - -### Commands -| Command | Result | Notes | -|---------|--------|-------| -| uv run task lint | PASS / FAIL | | -| uv run task static-check | PASS / FAIL | | -| uv run task test | PASS / FAIL | | - -### Naming Consistency -| Check | Result | Notes | -|---|---|---| -| Classes match domain model | PASS / FAIL | | -| Methods match glossary | PASS / FAIL | | -| No invented synonyms | PASS / FAIL | | - -### Self-Declaration Audit -| # | Claim | SE Claims | Reviewer Verdict | Evidence | -|---|-------|-----------|------------------|----------| -| 1 | YAGNI: no code without a failing test | AGREE/DISAGREE | PASS/FAIL | | -| 2 | YAGNI: no speculative abstractions | AGREE/DISAGREE | PASS/FAIL | | -| 3 | KISS: simplest solution that passes | AGREE/DISAGREE | PASS/FAIL | | -| 4 | KISS: no premature optimization | AGREE/DISAGREE | PASS/FAIL | | -| 5 | DRY: no duplication | AGREE/DISAGREE | PASS/FAIL | | -| 6 | DRY: no redundant comments | AGREE/DISAGREE | PASS/FAIL | | -| 7 | SOLID-S: one reason to change per class | AGREE/DISAGREE | PASS/FAIL | | -| 8 | SOLID-O: open for extension, closed for modification | AGREE/DISAGREE | PASS/FAIL | | -| 9 | SOLID-L: subtypes substitutable | AGREE/DISAGREE | PASS/FAIL | | -| 10 | SOLID-I: no forced unused deps | AGREE/DISAGREE | PASS/FAIL | | -| 11 | SOLID-D: depend on abstractions, not concretions | AGREE/DISAGREE | PASS/FAIL | | -| 12 | OC-1: one level of indentation per method | AGREE/DISAGREE | PASS/FAIL | | -| 13 | OC-2: no else after return | AGREE/DISAGREE | PASS/FAIL | | -| 14 | OC-3: primitive types wrapped | AGREE/DISAGREE | PASS/FAIL | | -| 15 | OC-4: first-class collections | AGREE/DISAGREE | PASS/FAIL | | -| 16 | OC-5: one dot per line | AGREE/DISAGREE | PASS/FAIL | | -| 17 | OC-6: no abbreviations | AGREE/DISAGREE | PASS/FAIL | | -| 18 | OC-7: ≀20 lines per function, ≀50 per class | AGREE/DISAGREE | PASS/FAIL | | -| 19 | OC-8: ≀2 instance variables (behavioural classes only) | AGREE/DISAGREE | PASS/FAIL | | -| 20 | OC-9: no getters/setters | AGREE/DISAGREE | PASS/FAIL | | -| 21 | Patterns: no good reason remains to refactor using OOP or Design Patterns | AGREE/DISAGREE | PASS/FAIL | | -| 22 | Patterns: no creational smell | AGREE/DISAGREE | PASS/FAIL | | -| 23 | Patterns: no structural smell | AGREE/DISAGREE | PASS/FAIL | | -| 24 | Patterns: no behavioral smell | AGREE/DISAGREE | PASS/FAIL | | -| 25 | Semantic: tests operate at same abstraction as AC | AGREE/DISAGREE | PASS/FAIL | | - -### Architect Review Stance Declaration - -Write this block **before** the Decision. Every `DISAGREE` must include an inline explanation. A `DISAGREE` with no explanation auto-forces `REJECTED`. - -As a system-architect I declare: -* Adversarial: I actively tried to find a failure mode, not just confirm passing β€” AGREE/DISAGREE | note: -* Architecture preservation: I verified that stubs, Protocols, and ADR decisions from Step 2 were respected β€” AGREE/DISAGREE | violations: -* Manual trace: I traced at least one execution path manually beyond automated output β€” AGREE/DISAGREE | path: -* Boundary check: I checked the boundary conditions and edge cases of every Rule β€” AGREE/DISAGREE | gaps: -* Semantic read: I read each test against its AC and confirmed it tests the right observable behavior β€” AGREE/DISAGREE | mismatches: -* Independence: my verdict was not influenced by how much effort has already been spent β€” AGREE/DISAGREE - -### Decision -**APPROVED** β€” all gates passed, no undeclared violations -OR -**REJECTED** β€” fix the following: -1. `:` β€” - -### Next Steps -**If APPROVED**: Run `@product-owner` β€” accept the feature at Step 5. -**If REJECTED**: Run `@software-engineer` β€” apply the fixes listed above, re-run quality gate, update Self-Declaration, then signal Step 4 again. -``` diff --git a/.opencode/skills/version-control/SKILL.md b/.opencode/skills/version-control/SKILL.md deleted file mode 100644 index 7f67230..0000000 --- a/.opencode/skills/version-control/SKILL.md +++ /dev/null @@ -1,216 +0,0 @@ ---- -name: version-control -description: Git branching, merge safety, and commit hygiene for feature development -version: "1.0" -author: software-engineer -audience: software-engineer -workflow: git-management ---- - -# Version Control - -This skill governs all Git operations during feature development. The software-engineer owns branch creation, commit hygiene, merging to `main`, and post-mortem branch management. - -## Git Safety Protocol (read first β€” never violate) - -These rules are absolute. Violating them risks destroying shared history or losing work. - -- **No force push**: `git push --force` and `git push --force-with-lease` are forbidden. -- **No history rewrite on pushed branches**: After a branch has been pushed to `origin`, do not `git rebase -i`, `git commit --amend`, or `git reset --hard` on it. These commands rewrite history that others may have fetched. -- **Use `git revert` to undo**: If a commit on a pushed branch must be undone, create a new revert commit. This appends history safely. -- **No commits directly to `main`**: All feature work happens on branches. `main` receives code only via `--no-ff` merge from an approved feature branch. - ---- - -## Branch Lifecycle - -### Normal Feature Flow - -``` -main ──●────────────────────────────●─────► - \ / - \── feat/ ──●──●──●/ -``` - -1. **Create** from latest `main` -2. **Develop** all commits on the branch -3. **Merge** back to `main` with `--no-ff` after Step 5 acceptance - -### Post-Mortem Fix Flow - -``` -main ──●─────●───────────────────────●─────► - \ / / - \ / / - ● (start commit) / - \── fix/ ──●──●──●/ -``` - -1. **Find** the feature's original start commit -2. **Branch** `fix/` from that commit -3. **Commit post-mortem** as the first commit on the new branch -4. **Redo** Steps 2–5 on `fix/` -5. **Merge** back to `main` with `--no-ff` - ---- - -## 1. Create Feature Branch - -Run at the start of Step 2 (before the system-architect writes stubs). - -```bash -# Ensure you are on main and it is up to date -git branch --show-current # must output: main -git fetch origin main -git merge --ff-only origin/main # fast-forward only; if this fails, main has diverged β€” escalate - -# Create and switch to feature branch -git checkout -b feat/ - -# Push the branch to origin (establishes tracking) -git push -u origin feat/ -``` - -**Branch naming**: -- `feat/` β€” new feature -- `fix/` β€” post-mortem restart of a failed feature -- `docs/` β€” documentation-only changes -- `chore/` β€” tooling, deps, CI - -**If `main` has unmerged work**: The `git merge --ff-only` will fail. This means `main` is ahead of your local copy. Escalate to the PO or SA β€” do not resolve by merging or rebasing on your own. - ---- - -## 2. Commit Hygiene - -Every commit on a feature branch must follow conventional commits: - -``` -(): - -Types: feat, fix, test, refactor, chore, docs, perf, ci -``` - -**Forbidden commit messages** (reject immediately if you are tempted to use them): -- `wip`, `temp`, `fix tests`, `oops`, `try again`, `asdf` -- Any commit without a type prefix - -**Commit early, commit often**: A feature branch with 10 small, well-described commits is better than 1 giant commit. But do not commit broken code (tests must pass at each commit during Step 3). - ---- - -## 3. Branch Verification - -Run before every session start and before every handoff. - -```bash -# Verify you are on the correct branch -git branch --show-current # expect: feat/ or fix/ - -# Verify working tree is clean -git status # expect: "nothing to commit, working tree clean" - -# Verify branch is ahead of main (has commits) -git log main..HEAD --oneline # expect: 1+ commits listed -``` - -**If any check fails**: -- Wrong branch β†’ `git checkout feat/` (or create it if missing) -- Dirty working tree β†’ commit or stash before continuing -- No commits ahead of main β†’ you have not started work on this branch - ---- - -## 4. Merge Feature Branch to Main - -Run after PO acceptance (Step 5). This is the only way code enters `main`. - -```bash -# Ensure feature branch is clean and all commits are pushed -git status # must be clean -git push origin feat/ - -# Switch to main and update it -git checkout main -git fetch origin main -git merge --ff-only origin/main - -# Check for merge conflicts before the real merge -git merge-tree $(git merge-base HEAD feat/) HEAD feat/ -# If the output is non-empty, there are conflicts. Resolve them on the feature branch first. - -# Merge with --no-ff to preserve feature boundary -git merge --no-ff feat/ -m "feat(): merge to main" - -# Push main -git push origin main - -# Delete the feature branch (optional, but recommended) -git branch -d feat/ -git push origin --delete feat/ -``` - -**Why `--no-ff`**: Fast-forward merges erase the feature boundary from history. With `--no-ff`, the merge commit groups all feature commits together, making the feature revertible as a single unit. - ---- - -## 5. Post-Mortem Branch - -Run when a feature fails acceptance and the PO restarts it at Step 2. - -```bash -# Find the feature's original start commit -# The start commit is the commit where the feature branch was created from main. -# It is typically the first commit on the old feature branch. -git log --all --grep="feat()" --oneline -# Or, if the branch still exists: -git log --reverse main..feat/ --oneline # first line = start commit - -# Checkout the start commit and create fix branch -git checkout -b fix/ - -# Commit the post-mortem as the first commit on the new branch -git add docs/post-mortem/YYYY-MM-DD--.md -git commit -m "docs(post-mortem): root cause for " - -# Push the fix branch -git push -u origin fix/ -``` - -The system-architect then begins Step 2 on `fix/`, reading the post-mortem as input. All subsequent work (stubs, tests, implementation) happens on this branch. It merges to `main` with `--no-ff` after acceptance. - -**Old feature branch**: Keep it for reference until the fix branch is merged. Do not delete it prematurely β€” it contains the history the SA may need to consult. - ---- - -## 6. Conflict Detection - -Before merging a feature branch to `main`, check if `main` has diverged since the branch was created. - -```bash -# Check if main has new commits not in the feature branch -git log feat/..origin/main --oneline -# If output is non-empty, main has diverged. - -# Preview the merge without touching files -git merge-tree $(git merge-base main feat/) main feat/ -# Empty output = clean merge. Non-empty output = conflicts exist. -``` - -**If conflicts exist**: Resolve them on the feature branch before attempting merge to `main`. - -```bash -git checkout feat/ -git merge main # resolve conflicts, commit the merge -git push origin feat/ -``` - -Then retry the merge to `main`. - ---- - -## Reference - -- Pro Git, Scott Chacon & Ben Straub (free online: git-scm.com/book) -- Git Cheat Sheet (git-scm.com/cheatsheets) -- A successful Git branching model, Vincent Driessen (nvie.com/posts/a-successful-git-branching-model/) diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index ffb5e9d..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,301 +0,0 @@ -# Python Project Template - -A Python template to quickstart any project with a production-ready workflow, quality tooling, and AI-assisted development. - -## Workflow Overview - -Features flow through 5 steps with a WIP limit of 1 feature at a time. The filesystem enforces WIP: -- `docs/features/backlog/.feature` β€” features waiting to be worked on -- `docs/features/in-progress/.feature` β€” exactly one feature being built right now -- `docs/features/completed/.feature` β€” accepted and shipped features - -``` -STEP 1: SCOPE (product-owner) β†’ discovery + Gherkin stories + criteria -STEP 2: ARCH (system-architect) β†’ branch from main; read system.md + glossary.md + in-progress feature + targeted package files; write domain stubs; create/update domain-model.md; significant decisions as docs/adr/ADR-YYYY-MM-DD-.md; system.md rewritten -STEP 3: TDD LOOP (software-engineer) β†’ RED β†’ GREEN β†’ REFACTOR, one @id at a time -STEP 4: VERIFY (system-architect) β†’ run all commands, review code against architecture -STEP 5: ACCEPT (product-owner) β†’ demo, validate, SE merges branch to main with --no-ff, move .feature to completed/ (PO only) -``` - -### Branch Model - -All feature work happens on branches. `main` is the single source of truth and receives code only via `--no-ff` merge from an approved feature branch. - -**Normal flow**: -1. SE creates `feat/` from latest `main` at Step 2 start -2. All commits live on `feat/` through Steps 2–4 -3. After PO acceptance (Step 5), SE merges `feat/` to `main` with `--no-ff` -4. SE deletes the feature branch - -**Post-mortem flow** (failed feature restart): -1. Find the feature's original start commit -2. SE creates `fix/` from that commit -3. Post-mortem is committed as the first commit on `fix/` -4. Steps 2–5 rerun on `fix/`, then merge to `main` with `--no-ff` - -**Git Safety Protocol** (absolute β€” never violate): -- No force push (`git push --force` forbidden) -- No history rewrite on pushed branches (no `rebase -i`, `commit --amend`, `reset --hard` after push) -- Use `git revert` to undo changes on shared history -- No commits directly to `main` - -**Closed loop**: SA designs β†’ SE builds β†’ SA reviews. The same mind that designed the architecture verifies it. No context loss. - -**PO picks the next feature from backlog. No agent self-selects.** - -**Verification is adversarial.** The system-architect's job is to try to break the feature, not to confirm it works. The default hypothesis is "it might be broken despite green checks; prove otherwise." - -## Roles - -- **Product Owner (PO)** β€” AI agent. Interviews the stakeholder, writes discovery docs, Gherkin features, and acceptance criteria. Accepts or rejects deliveries. **Sole owner of all `.feature` file moves** (backlog β†’ in-progress before Step 2; in-progress β†’ completed after Step 5 acceptance). -- **Stakeholder** β€” Human. Answers PO's questions, provides domain knowledge, approves PO syntheses to confirm discovery is complete. -- **System Architect (SA)** β€” AI agent. Designs architecture, writes domain stubs, records decisions in ADRs, and verifies implementation respects those decisions. Owns `docs/domain-model.md`, `docs/system.md`, and `docs/adr/ADR-*.md`. Never edits or moves `.feature` files. Escalates spec gaps to PO. -- **Software Engineer (SE)** β€” AI agent. Implements everything: test bodies, production code, releases. Owns all `.py` files under the package. Never edits or moves `.feature` files. Escalates spec gaps to PO. If no `.feature` file is in `in-progress/`, stops and escalates to PO. - -## Feature File Chain of Responsibility - -`.feature` files are owned exclusively by the PO. **No other agent ever moves, creates, or edits them.** - -| Transition | Who | When | -|---|---|---| -| `backlog/` β†’ `in-progress/` | PO only | Before Step 2 begins; only if `Status: BASELINED` | -| `in-progress/` β†’ `completed/` | PO only | After Step 5 acceptance | - -**If an agent (SE or SA) finds no `.feature` in `in-progress/`**: update FLOW.md with the correct `Next:` escalation line and stop. Never self-select a backlog feature. - -## Agents - -- **product-owner** β€” defines scope (Stage 1 Discovery + Stage 2 Specification), picks features, accepts deliveries -- **system-architect** β€” architecture and domain design (Step 2), adversarial technical review (Step 4) -- **software-engineer** β€” TDD loop, implementation, tests, code, git, releases (Step 3 + release) -- **designer** β€” creates and updates visual assets (SVG banners, logos) and maintains `docs/branding.md` -- **setup-project** β€” one-time setup to initialize a new project from this template - -## Skills - -| Skill | Used By | Step | -|---|---|---| -| `run-session` | all agents | every session | -| `select-feature` | product-owner | between features (idle state) | -| `define-scope` | product-owner | 1 | -| `architect` | system-architect | 2 | -| `implement` | software-engineer | 3 | -| `apply-patterns` | system-architect, software-engineer | 2, 3 (on-demand, when GoF pattern needed) | -| `refactor` | software-engineer | 3 (REFACTOR phase + preparatory refactoring) | -| `verify` | system-architect | 4 | -| `check-quality` | software-engineer | pre-handoff (redirects to `verify`) | -| `version-control` | software-engineer | Step 2 (branch creation), Step 5 (merge to main), post-mortem branches | -| `create-pr` | system-architect | post-acceptance | -| `git-release` | stakeholder | post-acceptance | -| `update-docs` | product-owner | post-acceptance + on stakeholder demand | -| `design-colors` | designer | branding, color, WCAG compliance | -| `design-assets` | designer | SVG asset creation and updates | -| `flow` | all agents | every session β€” workflow state machine, auto-detection, prerequisites | -| `create-skill` | software-engineer | meta | -| `create-agent` | human-user | meta | - -**Branding**: Agents that generate docs, diagrams, release names, or visual assets read `docs/branding.md` if present. Absent or blank fields fall back to defaults (adjective-animal release names, Mermaid default colors, no wording constraints). `docs/branding.md` and `docs/assets/` are owned by the designer agent. - -**Session protocol**: Every agent loads `skill run-session` at session start. Load additional skills as needed for the current step. - -## Step 1 β€” SCOPE - -Step 1 has two stages: - -### Stage 1 β€” Discovery (PO + stakeholder, iterative) - -Discovery follows a block structure per session. See `skill define-scope` for the full protocol. - -**Block A β€” Session Start**: Resume check (if `IN-PROGRESS`), read `domain-model.md` (existing entities), declare scope. - -**Block B β€” General & Cross-cutting**: 5Ws, behavioral groups, bounded contexts. Active listening + reconciliation against `glossary.md` and `domain-model.md`. - -**Block C β€” Feature Discovery (per feature)**: Detailed questions, pre-mortem, create/update `.feature` files. - -**Block D β€” Session Close**: Append Q&A to `scope_journal.md`, update `glossary.md`, append synthesis to `discovery.md`, regression check on completed features, mark `COMPLETE`. - -**Key rules**: -- PO owns `scope_journal.md`, `discovery.md`, `glossary.md`, and `.feature` files -- PO reads `domain-model.md` but never writes to it β€” entity suggestions go in `discovery.md` for SA formalization at Step 2 -- Real-time split rule: >2 concerns or >8 candidate Examples β†’ split immediately -- Completed feature touched and changed β†’ move to `backlog/` - -**Baselining**: PO writes `Status: BASELINED (YYYY-MM-DD)` in the `.feature` file when the stakeholder approves that feature's discovery and the decomposition check passes. - -Commit per session: `feat(discovery): ` - -### Stage 2 β€” Specification (PO alone, per feature) - -Only runs on features with `Status: BASELINED`. No stakeholder involvement. If a gap requires stakeholder input, open a new Stage 1 session first. - -**Step A β€” Stories**: derive one `Rule:` block per user story from the baselined feature description. INVEST gate: all 6 letters must pass. -Commit: `feat(stories): write user stories for ` - -**Step B β€” Criteria**: PO writes `Example:` blocks with `@id` tags under each `Rule:`. Pre-mortem per Rule before writing any Examples. MoSCoW triage per Example. Examples are frozen after commit. -Commit: `feat(criteria): write acceptance criteria for ` - -**Criteria are frozen**: no `Example:` changes after commit. Adding a new Example with a new `@id` replaces old. - -### Bug Handling - -When a defect is reported: -1. **PO** adds a `@bug` Example to the relevant `Rule:` in the `.feature` file and moves (or keeps) the feature in `backlog/` for normal scheduling. -2. **SA** handles Step 2 (architecture) and **SE** handles Step 3 (TDD loop) when the feature is selected for development. The SE implements the specific `@bug`-tagged test in `tests/features//` and also writes a `@given` Hypothesis property test in `tests/unit/` covering the whole class of inputs. -3. Both tests are required. SE follows the normal TDD loop (Step 3). - -### Acceptance Failure & Restart - -If the stakeholder reports failure **after the PO has attempted Step 5 acceptance**: -1. **PO does not move the `.feature` file to `completed/`**. Ensure it remains in `in-progress/`. -2. **Team compiles a compact post-mortem** (`docs/post-mortem/YYYY-MM-DD--.md`, max 15 lines, process-level root cause). -3. **SE creates a fix branch** from the feature's original start commit: `git checkout -b fix/ `. The post-mortem is committed as the first commit on this branch. -4. **PO scans `docs/post-mortem/`** and selects relevant files by matching `` or ``. -5. **PO reads selected post-mortems**, then resets FLOW.md Status to [STEP-2-ARCH] with context. -6. **SA restarts Step 2** on `fix/`, reading relevant post-mortems as input. The same feature re-enters the ARCH step. -7. After acceptance, SE merges `fix/` to `main` with `--no-ff`. - -Post-mortems are append-only, never edited. If a failure mode recurs, write a new file referencing the old one. - -## Filesystem Structure - -``` -docs/ - scope_journal.md ← raw Q&A, PO appends after every session - discovery.md ← session synthesis changelog, PO appends after every session - domain-model.md ← living domain model, SA creates/updates at Step 2, PO reads only - adr/ ← one file per decision: ADR-YYYY-MM-DD-.md, SA creates at Step 2 - system.md ← current-state overview (completed features only), SA rewrites at Step 2, PO reviews at Step 5 - glossary.md ← living glossary, PO updates after each session - branding.md ← project identity, colors, release naming, wording (designer owns) - assets/ ← logo.svg, banner.svg, and other visual assets (designer owns) - context.md ← C4 Level 1 diagram, PO updates via update-docs skill - container.md ← C4 Level 2 diagram, PO updates via update-docs skill (if multi-container) - post-mortem/ ← compact post-mortems, PO-owned, append-only - features/ - backlog/.feature ← narrative + Rules + Examples - in-progress/.feature - completed/.feature - -tests/ - features// - _test.py ← one per Rule: block, software-engineer-written - unit/ - _test.py ← software-engineer-authored extras (no @id traceability) - -FLOW.md ← workflow state tracker (feature, branch, status, session log, next action) -``` - -Tests in `tests/unit/` are software-engineer-authored extras not covered by any `@id` criterion. Any test style is valid β€” plain `assert` or Hypothesis `@given`. Use Hypothesis when the test covers a **property** that holds across many inputs (mathematical invariants, parsing contracts, value object constraints). Use plain pytest for specific behaviors or single edge cases discovered during refactoring. - -- `@pytest.mark.slow` is mandatory on every `@given`-decorated test (Hypothesis is genuinely slow) -- `@example(...)` is optional but encouraged when using `@given` to document known corner cases -- No `@id` tags β€” tests with `@id` belong in `tests/features/`, written by software-engineer - -## Test File Layout - -``` -tests/features//_test.py -``` - -### Stub Format - -Stubs are auto-generated by pytest-beehave. The SA triggers generation at Step 2 end by running `uv run task test-fast`. pytest-beehave reads the in-progress `.feature` file and creates one skipped function per `@id`: - -```python -@pytest.mark.skip(reason="not yet implemented") -def test__<@id>() -> None: - """ - <@id steps raw text including new lines> - """ -``` - -### Markers -- `@pytest.mark.slow` β€” takes > 50ms; applied to Hypothesis tests and any test with I/O, network, or DB -- `@pytest.mark.deprecated` β€” auto-skipped by pytest-beehave; used for superseded Examples - -## Development Commands - -```bash -# Install dependencies -uv sync --all-extras - -# Run the application (for humans) -uv run task run - -# Run the application with timeout (for agents β€” prevents hanging) -timeout 10s uv run task run - -# Run tests (fast, no coverage) -uv run task test-fast - -# Run full test suite with coverage -uv run task test - -# Run tests with coverage report generation -uv run task test-build - -# Lint and format -uv run task lint - -# Type checking -uv run task static-check - -# Build documentation -uv run task doc-build -``` - -## Code Quality Standards - -- **Principles (in priority order)**: YAGNI > KISS > DRY > SOLID > Object Calisthenics > appropriate design patterns > complex code > complicate code > failing code > no code -- **Linting**: ruff format, ruff check, Google docstring convention, `noqa` forbidden -- **Type checking**: pyright, 0 errors required -- **Coverage**: 100% (measured against your actual package) -- **Function length**: ≀ 20 lines (code lines only, excluding docstrings) -- **Class length**: ≀ 50 lines (code lines only, excluding docstrings) -- **Max nesting**: 2 levels -- **Instance variables**: ≀ 2 per class *(exception: dataclasses, Pydantic models, value objects, and TypedDicts are exempt β€” they may carry as many fields as the domain requires)* -- **Semantic alignment**: tests must operate at the same abstraction level as the acceptance criteria they cover - -### Software-Engineer Quality Gate Priority Order - -During Step 3 (TDD Loop) and before handoff to Step 4: - -1. **Design correctness** β€” YAGNI > KISS > DRY > SOLID > Object Calisthenics > appropriate design patterns > complex code > complicated code > failing code > no code -2. **One test green** β€” the specific test under work passes, plus `test-fast` still passes -3. **Quality tooling** β€” `lint`, `static-check`, full `test` with coverage run at handoff to SA - -Design correctness is far more important than lint/pyright/coverage compliance. A well-designed codebase with minor lint issues is better than a lint-clean codebase with poor design. - -## Verification Philosophy - -- **Automated checks** (lint, typecheck, coverage) verify **syntax-level** correctness β€” the code is well-formed. -- **Human review** (semantic alignment, code review, manual testing) verifies **semantic-level** correctness β€” the code does what the user needs. -- Both are required. All-green automated checks are necessary but not sufficient for APPROVED. -- System-architect defaults to REJECTED unless correctness is proven. - -## Release Management - -Version format: `v{major}.{minor}.{YYYYMMDD}` - -- Minor bump for new features; major bump for breaking changes -- Same-day second release: increment minor, keep same date -- Release name: defined by `docs/branding.md > Release Naming > Convention`; absent or blank defaults to version string only (no name) - -**Releases happen from `main` only.** The SE ensures `main` is up to date with `origin/main` before creating a release. No releases from feature branches. - -The stakeholder initiates the release process. When the stakeholder requests a release, the system-architect or software-engineer loads `skill git-release` to execute it. - -## Session Management - -Every session: load `skill run-session`. Read `FLOW.md` first, update it at the end. - -`FLOW.md` is the workflow state tracker β€” it records the current feature, branch, detected state, and next action. It is append-only in the Session Log section. See `.opencode/skills/flow/SKILL.md` for the full state machine and auto-detection rules. - -## Setup - -To initialize a new project from this template: -```bash -@setup-project -``` - -The setup agent will ask for your project name, GitHub username, author info, and configure all template placeholders. diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e81b0bd..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,370 +0,0 @@ -# Changelog - -All notable changes to this template will be documented in this file. - -## [v6.4.20260420] - Minimal Prometheus - 2026-04-20 - -### Added -- **Branding system**: `docs/branding.md` β€” project identity, colour palette, release naming convention, and wording guidelines; agents read this file to personalise release names, C4 diagram colours, and docs without touching `.opencode/` (#89) -- **Designer agent** (`designer.md`): owns `docs/branding.md` and `docs/assets/`; uses `design-colors` and `design-assets` skills (#89) -- **`design-colors` skill**: step-by-step colour palette selection with WCAG 2.1 AA 4.5:1 contrast validation; Itten/Albers colour theory embedded inline (#89) -- **`design-assets` skill**: SVG banner and logo creation workflow; W3C SVG 2 spec and WCAG 1.1.1 `aria-label` requirements embedded (#89) -- **`setup-project` Step 6 Branding**: collects tagline, mission, vision, tone, theme, and colours; suggests WCAG-validated palettes when user provides a theme but no colours; writes `docs/branding.md` (#89) -- **Output Style + Rule #8** in `run-session` skill: minimalist output discipline β€” signal only, no tool narration, session ends with `Next:` line (#89) - -### Changed -- **Skill renames to verb-noun convention**: `session-workflow β†’ run-session`, `scope β†’ define-scope`, `implementation β†’ implement`, `feature-selection β†’ select-feature`, `living-docs β†’ update-docs`, `pr-management β†’ create-pr`, `design-patterns β†’ apply-patterns`, `code-quality β†’ check-quality` β€” all references updated across agents, skills, and `AGENTS.md` (#89) -- **`docs/images/` β†’ `docs/assets/`**: asset directory renamed; `README.md` path updated (#89) -- **`git-release` v1.1**: reads `docs/branding.md` for optional release naming and theme; release name omitted from commit/release title if convention is absent (#89) -- **`update-docs` skill**: reads `docs/branding.md` primary/accent colours to apply `%%{init:...}%%` theming to Mermaid C4 diagrams (#89) - -## [v6.2.20260419] - Autonomous Stenella - 2026-04-19 - -### Added -- **pytest-beehave integration**: `@id` tags now auto-assigned to untagged `Example:` blocks on every `pytest` run; test stubs auto-generated from `.feature` files at Step 2 end β€” no manual ID generation or stub writing required (#78) -- **Self-declaration defense in depth**: all 25 items numbered 1–25 in `implementation/SKILL.md`; `verify/SKILL.md` now hard-gates on completeness (count must equal 25, sequence must be gapless) before item audit begins (#78) - -### Changed -- **Naming convention**: `.feature` file paths now use `` (kebab); test directories use `` (underscore) β€” applied consistently across all skills, `AGENTS.md`, and docs (#78) -- **`conftest.py`**: removed manual `deprecated` marker skip hook β€” now owned entirely by pytest-beehave (#78) -- **`scope/SKILL.md`**: removed all manual `@id` generation instructions and `@id` uniqueness checklist items β€” assignment is automatic (#78) -- **`product-owner.md`**: removed `@id` from bug handling and gap-resolution table β€” PO writes `Example:` blocks only (#78) -- **README**: added "Why this template?" section; added `pytest-beehave` to tooling table; replaced static stub example with a two-part Gherkin-in β†’ stub-out illustration (#78) -- **`verify/SKILL.md` report table**: expanded Self-Declaration Audit from 21 collapsed rows to 25 numbered rows matching the implementation template exactly (#78) - -## [v6.1.20260419] - Contextual Ambystoma - 2026-04-19 (hotfix) - -### Added -- **living-docs skill**: new PO skill for generating C4 architecture diagrams (`docs/c4/context.md`, `docs/c4/container.md`) and maintaining the living glossary (`docs/glossary.md`) after each feature acceptance (Step 5) or on stakeholder demand -- **docs/c4/**: new directory for C4 Level 1 (Context) and Level 2 (Container) Mermaid diagrams; placeholder `.gitkeep` added -- **docs/glossary.md**: new living glossary file owned by `living-docs` skill (PO); terms sourced from completed feature files, `docs/discovery.md` Domain Model, and `docs/architecture.md` decisions -- **Scientific research β€” documentation.md**: new file with 4 entries (#59–62): Ko et al. 2007 (information needs), Winters et al. 2020 (docs-as-code), Procida 2021 (DiΓ‘taxis framework), Allspaw 2012 (blameless post-mortems) -- **Scientific research β€” domain-modeling.md**: 6 new DDD entries (#63–68): Evans DDD Reference CC-BY, Fowler UbiquitousLanguage bliki, Fowler BoundedContext bliki, Vernon IDDD, Verraes "UL is not a glossary", Evans Whirlpool process -- **Scientific research β€” architecture.md**: 4 new entries (#55–58): Nygard ADRs, Kruchten 4+1 View Model, Brown C4 Model, Parnas information hiding - -### Changed -- **discovery.md template**: `### Scope` section renamed to `### Context` β€” the section is a session-level general-context synthesis, not a complete project scope definition -- **scope/SKILL.md**: updated `### Scope` references to `### Context` in Step C instructions and template block -- **living-docs/SKILL.md**: glossary entry format updated β€” `**Context:**` renamed to `**Bounded context:**` (mandatory for multi-context projects); `Domain Event` added as a distinct Type value; secondary-artifact note added to preamble; source-traceability rule replaces "do not invent" rule; checklist updated accordingly -- **implementation/SKILL.md**: Step 2 Read Phase now includes `docs/glossary.md` as item 2 β€” SE reads existing domain terms before naming classes, methods, and modules to avoid inventing synonyms -- **create-skill/SKILL.md**: `living-docs` added to available skills table -- **AGENTS.md**: skills table updated with `living-docs`; filesystem structure section updated (`docs/c4/`, `docs/glossary.md` added; `docs/architecture/` subtree removed; TODO.md reference updated) - -### Removed -- **docs/architecture/**: folder deleted; the ADR log lives at `docs/architecture.md` (SE-owned); the old `adr-template.md` inside the folder was redundant -- **docs/workflow.md**: deleted; canonical workflow reference is `AGENTS.md` and the skills under `.opencode/skills/` -- **Dockerfile / docker-compose.yml**: removed as unused template artifacts - -## [v6.0.20260419] - Declarative Nautilus - 2026-04-19 - -### Added -- **PO Self-Declaration**: mandatory 11-claim checklist (INVEST I/V/S/T, observable Then, no impl details, entity coverage, distinct examples, unique IDs, pre-mortem, scope boundary) written into TODO.md at end of Stage 2 Step B before criteria commit; every DISAGREE is a hard blocker (#71) -- **Reviewer Stance Declaration**: 5-claim block (adversarial mindset, manual trace, boundary check, semantic read, independence) added to verify/SKILL.md report template before APPROVED/REJECTED verdict; DISAGREE allowed with explanation, unexplained DISAGREE = REJECTED (#71) -- **session-workflow**: Step 1 Stage 2 Criteria TODO format section with full Self-Declaration template and Rule 9 enforcing the declaration before criteria commit (#71) -- **Three append-only project docs**: `docs/discovery_journal.md` (raw Q&A), `docs/discovery.md` (synthesis changelog), `docs/architecture.md` (architectural decisions) replace the old flat `docs/features/discovery.md` (#70) - -### Changed -- **Discovery model** (breaking): Phase 1 / Phase 2 / Phase 3 / Phase 4 replaced by 2-stage model β€” Stage 1 Discovery (unified iterative sessions, PO + stakeholder) and Stage 2 Specification (PO alone, per BASELINED feature) (#70) -- **Feature file moves** (breaking): PO is now the sole owner of all `.feature` file moves (backlog β†’ in-progress and in-progress β†’ completed); SE and reviewer explicitly prohibited from moving files with clear escalation protocol (#70) -- **Session protocol**: discovery journal sessions use `Status: IN-PROGRESS` / `Status: COMPLETE` markers; real-time split rule (>2 concerns or >8 candidate Examples splits within the same session); journal writes only answered Q&A in groups (#70) -- **Bug handling**: explicit protocol β€” PO adds `@bug @id` Example, SE writes both the `@id` test in `tests/features/` and a `@given` Hypothesis property test in `tests/unit/`; both required (#70) -- **scope/SKILL.md**: full rewrite to 2-stage model with session start checklist, question order (general β†’ cross-cutting β†’ per-feature), after-questions steps, baselining section, and bug handling section (#70) -- **feature-selection/SKILL.md**: updated "Phase 4 (Criteria)" reference to "Stage 2 Step B (Criteria)" (#70) -- **All agent files and skills**: updated to reflect new document model, terminology, and chain of responsibility (#70, #71) - -## [v5.2.20260418] - Emergent Colugo - 2026-04-18 (hotfix) - -### Fixed -- **Role naming**: Replaced stale `developer` agent-role references with `software-engineer` in `implementation/SKILL.md`, `docs/scientific-research/ai-agents.md`, `docs/scientific-research/cognitive-science.md`, and `docs/features/completed/display-version.feature` -- **session-workflow**: Replaced hardcoded agent names in `## Next` line examples with `@` placeholders; added note pointing to `AGENTS.md` as source of truth; added missing Step 2 (Architecture) example - -## [v5.1.20260418] - Emergent Colugo - 2026-04-18 - -### Added -- **refactor skill**: Standalone skill with Fowler's full catalogue, green-bar rule, two-hats rule, SOLID/OC self-declaration table, and preparatory refactoring protocol β€” loaded on demand at REFACTOR phase -- **feature-selection skill**: WSJF-based backlog prioritisation (Reinertsen 2009) with Kano value scoring and dependency gate β€” PO loads this when `TODO.md` is idle -- **ADR template**: `docs/architecture/adr-template.md` for Step 2 architectural decisions -- **Logo and banner**: visual identity added to README (SVG assets in `docs/images/`) - -### Changed -- **Architecture stubs**: Step 2 now writes stubs directly into `/` instead of an Architecture section in the feature file; stubs have no docstrings (add after GREEN when lint enforces them); folder structure is suggested, not prescribed β€” `ports/` and `adapters/` only created when a concrete external dependency is confirmed -- **design-patterns skill**: Narrowed to pure GoF catalogue (23 patterns, smell-triggered before/after examples); SOLID, OC, LoD, CQS, Python Zen moved to refactor skill self-declaration checklist -- **session-workflow**: `Next` line in TODO.md now requires `Run @` prefix so the human always knows which agent to invoke; idle state loads `skill feature-selection` instead of a vague prompt -- **verify skill**: Added orphaned-stub check (skip-marked tests that were never implemented); report template now includes structured `Next Steps` block directing the human to the correct agent -- **Scientific research**: `docs/academic_research.md` split into 9 domain files under `docs/scientific-research/` (cognitive-science, testing, architecture, oop-design, refactoring-empirical, requirements-elicitation, domain-modeling, software-economics, ai-agents) - -### Fixed -- Stale `docs/architecture/STEP2-ARCH.md` reference removed from workflow diagram and skill -- Protocol smell-check gate now marked N/A when no external dependencies are identified in scope - -## [v5.0.20260418] - Structured Phascolarctos - 2026-04-18 - -### Added -- **design-patterns skill**: Full GoF pattern catalogue with smell-triggered patterns, SOLID, Object Calisthenics, Python Zen, Law of Demeter, CQS, Tell Don't Ask β€” loaded on demand at Steps 2-3 -- **create-agent skill**: Research-backed agent creation guide with OpenAI/Anthropic best practices, ownership boundaries, tool surface design, and escalation rules -- **software-engineer agent**: Dedicated agent file replacing `developer.md`; owns Steps 2-3 and release -- **3-session discovery structure**: Phase 1 and Phase 2 now each use a 3-session template with template gates (Β§1/Β§2/Β§3 must be confirmed before proceeding); active listening protocol (3 levels) codified in scope skill - -### Changed -- **5-step workflow** (breaking): Steps restructured β€” TDD loop merged into Step 3, Verify is Step 4, Accept is Step 5; all agents, skills, and docs updated to match -- **Behavior groups terminology**: "Cluster" renamed to "behavior group" throughout scope skill, AGENTS.md, workflow.md, and templates for clearer AI focus -- **Story candidates terminology**: Phase 3 now derives "story candidates" β†’ `Rule:` blocks, removing ambiguity from the cluster-to-story mapping -- **Test stub format** (breaking): Stubs now use `@pytest.mark.skip(reason="not yet implemented")` instead of `raise NotImplementedError`; skip marker is removed when implementing in RED phase -- **Dropped `@pytest.mark.unit` and `@pytest.mark.integration`**: Only `@pytest.mark.slow` and `@pytest.mark.deprecated` remain; folder structure (`tests/features/` vs `tests/unit/`) encodes test type -- **BASELINED gate enforced**: PO may not move a feature to `in-progress/` unless its discovery section has `Status: BASELINED`; enforced in product-owner.md and session-workflow -- **tdd skill removed**: Replaced by implementation skill with inline TDD guidance -- **gen_test_stubs.py removed**: Script deleted along with tdd skill - -### Fixed -- **pyproject.toml**: Removed broken `gen-tests` task; removed `raise NotImplementedError` from coverage exclusions; removed `unit`/`integration` marker definitions -- **Role naming**: `developer` β†’ `software-engineer` across all files -- **Step count**: All references to "6 steps" updated to "5 steps" - -## [v4.1.20260416] - Recursive Acinonyx - 2026-04-16 - -### Added -- **Single `.feature` file per feature**: Each feature is now one `.feature` file with `Rule:` blocks for user stories and `Example:` blocks for ACs β€” discovery content embedded in the feature description free text; replaces the folder-per-feature structure -- **Rule-scoped test files**: `gen_test_stubs.py` rewritten to parse `Rule:` blocks; each Rule maps to one test file (`_test.py`); function naming is now `test__()` -- **Hypothesis-only `tests/unit/`**: Every test in `tests/unit/` must use `@given`; `@pytest.mark.slow` is mandatory on all Hypothesis tests; plain `assert` tests without `@given` are forbidden -- **Mandatory `## Self-Declaration` in TODO.md**: Developer writes the 21-item checklist into a `## Self-Declaration (@id:)` block in `TODO.md` at `SELF-DECLARE` phase before requesting reviewer check (Rule 8 in session-workflow) - -### Changed -- **`gen_test_stubs.py`**: Scans `docs/features/{backlog,in-progress,completed}/*.feature` directly (not subfolders); generates one test file per `Rule:` block -- **`gen_todo.py`**: `find_in_progress_feature()` now finds `.feature` files directly in `in-progress/`; source path is `docs/features/in-progress/.feature` -- **`skills/tdd/SKILL.md`**: Test Tool Decision table updated to separate `tests/features/` (plain pytest, generated) from `tests/unit/` (Hypothesis only); `tests/unit/` rules section added -- **`skills/implementation/SKILL.md`**: Unit test rule tightened β€” `@given` required, `@pytest.mark.slow` mandatory, plain tests forbidden -- **`skills/verify/SKILL.md`**: Two new rows in section 4f: `@given` check and `@slow` check; two new rows in Standards Summary -- **`skills/scope/SKILL.md`**: All four phases rewritten for file-based workflow; `discovery-template.md` converted to `.feature` file template -- **`skills/session-workflow/SKILL.md`**: Step 4 TODO format updated with mandatory `## Self-Declaration` block template; Rule 8 added -- **Completed feature migrated**: `docs/features/completed/display-version/` (three files) merged into `docs/features/completed/display-version.feature` (single file with two `Rule:` blocks) - -### Fixed -- **OC-8 clarification**: The only valid fix for > 2 `self.x` is a new named class (Rule 3 or Rule 4); hardcoded constants, class-level variables, inlined literals, and parent-class moves are all invalid workarounds and remain FAIL - -## [v4.0.20260416] - Precise Tarsius - 2026-04-16 - -### Added -- **Per-test Design Self-Declaration**: After REFACTOR, developer fills a 20-item checklist (YAGNI β†’ KISS β†’ DRY β†’ SOLID-S/O/L/I/D β†’ OC rules 1–9) with `file:line` evidence before requesting reviewer check; reviewer independently audits claims using an 11-row comparison table (#58) -- **Package Verification step**: Mandatory before writing any code β€” read `pyproject.toml β†’ [tool.setuptools] packages`, confirm directory exists on disk; hard stop if missing (#58) -- **SELF-DECLARE phase**: New phase added to the Red-Green-Refactor cycle between REFACTOR and REVIEWER; Cycle State now `RED | GREEN | REFACTOR | SELF-DECLARE | REVIEWER(code-design) | COMMITTED` (#58) -- **template-config.yaml**: Declarative single source of truth for all setup-project substitutions β€” `defaults:` block with 6 parameters, `substitutions:` map with literal `old:` strings, `{variable}` `new:` patterns, and expected `count:` per file (#58) -- **Post-mortem docs**: Two ping-pong-cli post-mortems documenting the systemic failures that drove this release (#58) - -### Changed -- **verify/SKILL.md Scope Guard**: Reviewer receives completed Design Self-Declaration and independently verifies each claim; responds using structured 11-row comparison table (#58) -- **verify/SKILL.md section 4g**: New row β€” `Imports use correct package name` (check imports match `[tool.setuptools] packages`); existing rows made more precise with `pyproject.toml` references (#58) -- **reviewer.md per-test Step 4 section**: Rewritten to reference `skill implementation` verification table; clarifies no commands run during Step 4 reviews (#58) -- **reviewer.md Zero-Tolerance Rule 1**: Scoped to `(Step 5 only β€” per-test Step 4 checks are code-design only, no commands)` (#58) -- **setup-project.md**: Reads `template-config.yaml`; each apply step delegates to the config map rather than carrying implicit pattern knowledge (#58) -- **Template app simplified**: `app/version.py` deleted; `app/__main__.py` reduced from 41 to 23 lines (#58) - -### Fixed -- **gen_todo.py path**: `parents[5]` β†’ `parents[4]` β€” was resolving one directory above the project root (#58) -- **session-workflow Cycle State**: `SELF-DECLARE` phase added to documented phase list and Rule 6 (#58) -- **code-quality/SKILL.md**: Removed "has been absorbed" migration language (#58) -- **Dockerfile stale references**: `python_package_template.python_module_template` β†’ `app` in HEALTHCHECK and CMD (#58) -- **docker-compose.yml stale references**: `python_package_template` β†’ `app` in volume mounts and command (#58) - -### Breaking Changes -- `project_defaults.json` deleted β€” replaced by `template-config.yaml` (#58) -- `app/version.py` and `tests/version_test.py` deleted β€” template app simplified to minimal `__main__.py` + one Hypothesis unit test (#58) - -## [v3.2.20260415] - Vigilant Mantis - 2026-04-15 - -### Added -- **Adversarial verification mandate**: Reviewer's default hypothesis is now "the code is broken despite green checks" β€” job is to find the failure mode, not confirm it works (#54) -- **Production-grade gate**: New step 3 in verification β€” app must exit cleanly AND output must change when input changes; static output regardless of input = REJECTED (#54) -- **UUID Drift bash check**: One-liner detects duplicate UUIDs across test functions; any duplicate = REJECTED with fix instructions (#54) -- **docs/academic_research.md**: 15 cognitive and social science mechanisms with full citations grounding every workflow design decision (pre-mortem, implementation intentions, adversarial collaboration, elaborative encoding, and 11 more) (#54) -- **Design pattern decision table**: Added to `developer.md` and `implementation/SKILL.md`; any detected anti-pattern = REJECTED (#54) -- **Architecture contradiction check**: Developer must cross-check ADRs against ACs before writing production code (#54) -- **PO pre-mortem**: Added at scope step and acceptance step (#54) -- **Semantic alignment rule**: Tests must operate at same abstraction level as AC (#54) -- **Integration test requirement**: Multi-component features require at least one integration test through the public entry point (#54) -- **Verification Philosophy section**: Added to AGENTS.md β€” automated checks verify syntax-level correctness; human review verifies semantic-level correctness; both required (#54) - -### Changed -- **Verification order**: Code review before automated commands; run app first as production-grade gate (#54) -- **All review sections converted to tables**: Correctness, KISS, SOLID, ObjCal, Design Patterns, Tests, Versions/Build all have PASS/FAIL/Fix columns (#54) -- **UUID Uniqueness rule**: If only Given varies it is a property β€” use Hypothesis `@given` + `@example`, not multiple test functions; if When/Then differs use `extend-criteria` (#54) -- **Production-grade self-check in implementation**: Developer must verify output changes with input before handoff (#54) - -## [v3.1.20260414] - Tidal Capybara - 2026-04-14 - -### Added -- **extend-criteria skill**: New skill for any agent to add acceptance criteria discovered mid-flight or post-merge, with decision rule (gap within scope vs. new feature), per-role procedures, and commit protocol -- **Source: field on acceptance criteria**: Mandatory traceability field on every criterion (`stakeholder | po | developer | reviewer | bug`) β€” records who originated the requirement - -### Changed -- **Test function naming**: `test_` replaces `test__should_` -- **Test docstring first line**: UUID only (no trailing description) β€” `"""\n\nGiven: ...` -- **development commands**: All skill and agent files now use `uv run task` consistently (not bare `task`) -- **tests/ layout**: Documented as flat (no unit/ or integration/ subdirectories) -- **pytest.skip prohibition**: Aligned across files β€” allowed with written justification in the docstring -- **Marker decision table**: Moved to tdd/SKILL.md only (developer's decision, not PO's) -- **mv to in-progress**: Ownership reassigned to developer Step 2 (not PO scope step) -- **TODO.md status markers**: Added `[~]` (in progress) and `[-]` (cancelled) to documented legend -- **--doctest-modules**: Documented in implementation/SKILL.md (task test runs doctest modules) -- **verify/SKILL.md**: Report template uses flat `tests/:` path format -- **exit code wording**: `exit non-124` (was ambiguous `exit 0 or 124`) in developer.md -- **README.md**: `uv sync --all-extras` and `uv run task` commands throughout - -### Fixed -- Removed stale `docs/features/in-progress/auto-publish-docs.md` -- Split compound acceptance criterion (two outcomes in one Then) into two single-outcome criteria -- Added `@pytest.mark.slow` to Hypothesis tests in reference implementation -- Added `# Given / # When / # Then` body comments to all reference tests -- Removed duplicate assertion from `test_version_logs_correct_message` -- Moved `StringIO` import from test body to module-level imports - -## [v3.0.20260414] - Drifting Axolotl - 2026-04-14 - -### Breaking Changes -- **Workflow redesigned**: 8-phase/6-role system replaced with 6-step/3-role (Product Owner, Developer, Reviewer) -- **Roles removed**: architect, manager, repo-manager, requirements-gatherer, overseer agents deleted -- **Feature directories restructured**: `docs/features/{business,architecture}/` replaced with flat `docs/features/{backlog,in-progress,completed}/` - -### Added -- **product-owner agent**: Defines scope, acceptance criteria, picks features, accepts deliveries (Steps 1 + 6) -- **reviewer agent**: Read+bash only, runs all commands, produces APPROVED/REJECTED report (Step 5) -- **scope skill**: PO guide for writing user stories + UUID acceptance criteria -- **verify skill**: Reviewer guide for running commands and code review checklist -- **Unified docs site**: `docs/index.html` landing page linking to API Reference, Coverage, Test Results -- **ghp-import**: One-liner `task doc-publish` replaces complex inline Python - -### Changed -- **developer agent**: Owns all of Steps 2-4+6 including architecture, tests, code, and release -- **9 skills rewritten**: session-workflow, tdd, implementation, code-quality, pr-management, git-release, create-skill (lean, <150 lines each) -- **Test markers reduced**: from 11 (with duplicate) to 3: `unit`, `integration`, `slow` -- **doc-build**: Now generates all three outputs (pdoc API + pytest-cov HTML + pytest-html) -- **CI workflow**: Cleaned up to use `uv run task ` consistently -- **setup-project agent**: No longer uses setup_project.py; agent applies changes directly - -### Removed -- 11 skills deleted (architectural-analysis, delegation-coordination, epic-workflow, feature-definition, qa-enforcement, requirements-management, signature-design, workflow-coordination, prototype-script, create-agent, reference/) -- `setup_project.py` script and `.opencode/templates/` directory -- Wrong `dotenv` dependency (replaced nothing β€” was unused) -- `mutmut` dev dependency (YAGNI) - -## [v2.2.20260413] - Luminous Kestrel - 2026-04-13 - -### Added -- **Architecture-First Feature System** - New directory structure separating business and architecture features -- **Architectural Analysis Skill** - Systematic architecture documentation for each feature -- **8-Phase Development Cycle** - Expanded from 7-phase with dedicated Architecture Analysis phase - -### Changed -- **BDD β†’ Acceptance Criteria** - Renamed gherkin-validation to acceptance-criteria-validation for accurate terminology -- **Consistency Updates** - Fixed phase numbering, cross-references, and documentation across all agents and skills -- **Epic-Workflow Refactor** - Converted from epic-based to feature-selection with architecture-first priority -- **Manager Agent** - Enhanced with test signature creation capabilities - -### Migration Notes -- No breaking changes in this release -- Projects can continue using existing workflow - -## [v2.1.20260413] - Polished Gecko - 2026-04-13 - -### Added -- Docker simplification and cleanup -- V2 Development Workflow with CI/CD fixes -- Template refactoring for generic app package -- Enhanced QA enforcement skills - -### Changed -- Complexity fixes for CI compliance -- CodeQL config conflict resolved - -## [v2.0.20260411] - Armored Pangolin - 2026-04-11 - -### πŸš€ MAJOR RELEASE - V1 β†’ V2 Architecture Transition - -This represents a fundamental architectural shift from V1 (template validation workflows) to V2 (project development workflows). - -### Breaking Changes -- **Workflow Architecture**: Complete transition from template validation (V1) to project development (V2) -- **CI/CD Pipeline**: New comprehensive GitHub Actions workflow replacing template-specific workflows -- **Branch Structure**: V2/init becomes the new development foundation -- **Agent Configuration**: Updated agent roles and capabilities for project development - -### Security Improvements -- Enhanced GitHub Actions workflow security with proper permissions blocks -- Removed risky PIP_USER environment variable from CI/CD pipeline -- Added secure error handling to shell scripts with 'set -euo pipefail' -- Implemented job-level permissions for all CI workflow operations - -### Infrastructure & DevOps -- Modernized Docker setup with security-first containerization approach -- Comprehensive CI/CD pipeline with GitHub Actions integration -- Improved workflow security following GitHub Advanced Security recommendations -- Full project development workflow implementation - -### Development Experience -- Complete project-focused development environment -- Better error handling and security practices in automation -- Enhanced development workflow with secure defaults -- Improved CI/CD reliability and security posture - -### Migration Notes -- **BREAKING**: This is a major version requiring migration from V1 template workflows -- V1 template validation workflows are replaced by V2 project development workflows -- Projects using V1 should plan migration to V2 architecture -- All security improvements follow GitHub security best practices - -## [v1.7.20260410] - Vivid Cardinal - 2026-04-10 - -### Added -- **QA-gated Epic Workflow** - Complete epic-based development with mandatory quality checkpoints at each phase -- **Epic-workflow Skill** - Manages epic-based development with automatic feature progression -- **EPICS.md Template** - Epic tracking and management file for generated projects - -### Changed -- Updated all agent descriptions to use industry-standard roles (Development Lead, Software Architect, QA Specialist, Business Analyst, Release Engineer) -- Removed model specifications from all agents to make template model-agnostic -- Updated AGENTS.md to properly document all 5 generated project agents and all skills -- Updated README.md with new workflow and agent roles - -### Fixed -- Documentation now accurately reflects what exists in template - -## [v1.6.20260409] - Guardian Owl - 2026-04-09 - -### Added -- **Overseer Agent** - Quality assurance agent that reviews work after each test implementation and requests changes if needed -- **Requirements Gatherer Agent** - Agent that asks questions to understand project needs, updates documentation, creates detailed analysis for architect - -### Changed -- Updated developer workflow to include `@overseer` calls after Phase 3 (TDD tests) and Phase 7 (Quality Assurance) -- Updated AGENTS.md with new agents and updated workflow examples - -## [v1.0.0] - 2026-03-12 - -### Added -- **AI-Enhanced Development Workflow** - Complete OpenCode integration for AI-powered development -- **Developer Agent** - Main development agent with 8-phase TDD workflow -- **Architect Agent** - Design review agent for SOLID principles and object calisthenics compliance -- **Repository Manager Agent** - Git operations, PRs, and themed releases management -- **Development Skills** - feature-definition, prototype-script, tdd, signature-design, implementation, code-quality -- **Repository Skills** - git-release (hybrid calver versioning with themed releases), pr-management -- **Meta Skills** - create-skill, create-agent for extending OpenCode -- **Template Management** - template-manager agent, template-test, template-release skills -- **Comprehensive CI Workflow** - Template validation, generated project tests, Docker builds -- **Validation Scripts** - cookiecutter.json, pyproject.toml, YAML frontmatter validation - -### Changed -- Updated README.md with modern AI-focused branding -- Updated generated project README template with AI development workflow - -### Features -- **7-Phase Development Cycle**: Feature Definition β†’ Prototype β†’ TDD β†’ Signature Design β†’ Architecture Review β†’ Implementation β†’ Quality Assurance -- **SOLID Principles Enforcement** - Single responsibility, dependency inversion, interface segregation -- **Object Calisthenics** - No primitives, small classes, behavior-rich objects -- **Hybrid Calver Versioning**: v1.2.20260302 format with themed releases -- **Themed Release Names**: "Swift Cheetah", "Vigilant Owl", "Creative Fox" based on PR sentiment -- **Property-Based Testing**: Hypothesis integration for robust test coverage - -### Migration Notes -- This is the first semantic version release -- No breaking changes to cookiecutter.json structure -- Generated projects now include OpenCode agents and skills -- Existing projects can regenerate to get new features diff --git a/FLOW.md b/FLOW.md deleted file mode 100644 index f3e883d..0000000 --- a/FLOW.md +++ /dev/null @@ -1,20 +0,0 @@ -# FLOW Protocol - -This file tracks the current feature in progress. Only ONE feature flows through the system at a time. - -## Current Feature -**Feature**: [NONE] -**Branch**: [NONE] -**Status**: [IDLE] - -## Prerequisites -- [x] Agents: product-owner, system-architect, software-engineer -- [x] Skills: run-session, define-scope, architect, implement, verify, version-control -- [x] Tools: uv, git -- [x] Directories: docs/features/, docs/adr/ - -## Session Log - - -## Next -Run @product-owner β€” load skill select-feature and pick the next BASELINED feature from backlog. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 9ae9e81..0000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2026, eol - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/README.md b/README.md deleted file mode 100644 index 5d4b6ec..0000000 --- a/README.md +++ /dev/null @@ -1,182 +0,0 @@ -
- -Python Project Template - -

- -[![Contributors][contributors-shield]][contributors-url] -[![Forks][forks-shield]][forks-url] -[![Stargazers][stars-shield]][stars-url] -[![Issues][issues-shield]][issues-url] -[![MIT License][license-shield]][license-url] -[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=for-the-badge)](https://nullhack.github.io/python-project-template/coverage/) -[![CI](https://img.shields.io/github/actions/workflow/status/nullhack/python-project-template/ci.yml?style=for-the-badge&label=CI)](https://github.com/nullhack/python-project-template/actions/workflows/ci.yml) -[![Python](https://img.shields.io/badge/python-3.13-blue?style=for-the-badge)](https://www.python.org/downloads/) - -**From zero to hero β€” production-ready Python, without the ceremony.** - -
- ---- - -## Quick Start - -```bash -git clone https://github.com/nullhack/python-project-template -cd python-project-template -curl -LsSf https://astral.sh/uv/install.sh | sh # skip if uv installed -uv sync --all-extras -opencode && @setup-project # personalise for your project -uv run task test && uv run task lint && uv run task static-check -``` - ---- - -## Why this template? - -Most Python templates give you a folder structure and a `Makefile`. This one gives you a **complete delivery system**: five AI agents, a structured five-step workflow, and quality gates that cannot be silenced by convention. - -The goal is to give every project β€” from its first commit β€” the same rigour that mature teams take years to establish. - -- **No feature starts without written acceptance criteria** β€” Gherkin `Example:` blocks traced to tests -- **No feature ships without adversarial review** β€” the system-architect's default hypothesis is "broken" -- **No guesswork on test stubs** β€” generated automatically from `.feature` files -- **No manual `@id` tags** β€” assigned automatically when you run tests -- **No ambiguity on workflow state** β€” `FLOW.md` auto-detects current step from filesystem and git state -- **AI agents for every role** β€” each agent has scoped instructions and cannot exceed its authority - ---- - -## How it works - -### The delivery cycle - -``` -SCOPE β†’ ARCH β†’ TDD LOOP β†’ VERIFY β†’ ACCEPT -``` - -| Step | Role | Output | -|------|------|--------| -| **1 Β· SCOPE** | Product Owner | Discovery interviews + Gherkin stories + acceptance criteria | -| **2 Β· ARCH** | System Architect | Module stubs, ADRs, auto-generated test stubs | -| **3 Β· TDD LOOP** | Software Engineer | RED β†’ GREEN β†’ REFACTOR, one criterion at a time | -| **4 Β· VERIFY** | System Architect | Adversarial check β€” lint, types, coverage, semantic review | -| **5 Β· ACCEPT** | Product Owner | Demo, validate, ship | - -**WIP limit: 1 feature at a time.** Features are `.feature` files that move through folders: - -``` -docs/features/backlog/ ← waiting -docs/features/in-progress/ ← building (max 1) -docs/features/completed/ ← shipped -``` - -### AI agents included - -| Agent | Responsibility | -|-------|---------------| -| `@product-owner` | Scope, stories, acceptance criteria, delivery acceptance | -| `@software-engineer` | TDD loop, implementation, git, releases | -| `@system-architect` | Adversarial verification β€” default position: broken | -| `@designer` | Visual identity, colour palette, SVG assets | -| `@setup-project` | One-time project initialisation | - -### Quality tooling, pre-configured - -| Tool | Role | -|------|------| -| `uv` | Package & environment management | -| `ruff` | Lint + format (Google docstrings) | -| `pyright` | Static type checking β€” 0 errors | -| `pytest` + `hypothesis` | Tests + property-based testing | -| `pytest-beehave` | Auto-generates test stubs from `.feature` files | -| `pytest-cov` | Coverage β€” 100% required | -| `pdoc` | API docs β†’ GitHub Pages | -| `taskipy` | Task runner | - ---- - -## Commands - -```bash -uv run task test # Full suite + coverage -uv run task test-fast # Fast, no coverage (use during TDD loop) -uv run task lint # ruff check + format -uv run task static-check # pyright -uv run task run # Run the app -``` - ---- - -## Code standards - -| | | -|---|---| -| Coverage | 100% | -| Type errors | 0 | -| Function length | ≀ 20 lines | -| Class length | ≀ 50 lines | -| Max nesting | 2 levels | -| Principles | YAGNI β€Ί KISS β€Ί DRY β€Ί SOLID β€Ί Object Calisthenics | - ---- - -## Test convention - -Write acceptance criteria in Gherkin: - -```gherkin -@id:a3f2b1c4 -Example: User sees version on startup - Given the application starts - When no arguments are passed - Then the version string is printed to stdout -``` - -Run tests once β€” a traced, skipped stub appears automatically: - -```python -@pytest.mark.skip(reason="not yet implemented") -def test_display_version_a3f2b1c4() -> None: - """ - Given the application starts - When no arguments are passed - Then the version string is printed to stdout - """ -``` - -Each test traces to exactly one acceptance criterion. No orphan tests. No untested criteria. - ---- - -## Branding - -When you run `@setup-project`, the agent collects your project's identity β€” name, tagline, mission, colour palette, and release naming convention β€” and writes `docs/branding.md`. All agents read this file. Release names, C4 diagram colours, and generated copy all reflect your project's identity without you touching `.opencode/`. - -Absent or blank fields fall back to defaults: adjective-animal release names, Mermaid default colours, no wording constraints. - ---- - -## Versioning - -`v{major}.{minor}.{YYYYMMDD}` β€” each release gets a unique name derived from your branding convention. By default: an adjective paired with an animal (scientific name). Configure your own theme in `docs/branding.md`. - ---- - -## License - -MIT β€” see [LICENSE](LICENSE). - -**Author:** [@nullhack](https://github.com/nullhack) Β· [Documentation](https://nullhack.github.io/python-project-template) - - -[contributors-shield]: https://img.shields.io/github/contributors/nullhack/python-project-template.svg?style=for-the-badge -[contributors-url]: https://github.com/nullhack/python-project-template/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/nullhack/python-project-template.svg?style=for-the-badge -[forks-url]: https://github.com/nullhack/python-project-template/network/members -[stars-shield]: https://img.shields.io/github/stars/nullhack/python-project-template.svg?style=for-the-badge -[stars-url]: https://github.com/nullhack/python-project-template/stargazers -[issues-shield]: https://img.shields.io/github/issues/nullhack/python-project-template.svg?style=for-the-badge -[issues-url]: https://github.com/nullhack/python-project-template/issues -[license-shield]: https://img.shields.io/badge/license-MIT-green?style=for-the-badge -[license-url]: https://github.com/nullhack/python-project-template/blob/main/LICENSE diff --git a/api/app.html b/api/app.html new file mode 100644 index 0000000..91d9f58 --- /dev/null +++ b/api/app.html @@ -0,0 +1,52 @@ + + + + + + + app API documentation + + + + + + + + + +
+
+

+app

+ +

Application package.

+
+ + + + + +
1"""Application package."""
+
+ + +
+
+ + \ No newline at end of file diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..e7ed126 --- /dev/null +++ b/api/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/api/search.js b/api/search.js new file mode 100644 index 0000000..7b58c46 --- /dev/null +++ b/api/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oApplication package.

\n"}}, "docInfo": {"app": {"qualname": 0, "fullname": 1, "annotation": 0, "default_value": 0, "signature": 0, "bases": 0, "doc": 5}}, "length": 1, "save": true}, "index": {"qualname": {"root": {"docs": {}, "df": 0}}, "fullname": {"root": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {"app": {"tf": 1}}, "df": 1}}}}}, "annotation": {"root": {"docs": {}, "df": 0}}, "default_value": {"root": {"docs": {}, "df": 0}}, "signature": {"root": {"docs": {}, "df": 0}}, "bases": {"root": {"docs": {}, "df": 0}}, "doc": {"root": {"docs": {"app": {"tf": 1.7320508075688772}}, "df": 1, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"app": {"tf": 1}}, "df": 1}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {"app": {"tf": 1}}, "df": 1}}}}}}}}}}, "pipeline": ["trimmer"], "_isPrebuiltIndex": true}; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 18b665e..0000000 --- a/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Application package.""" diff --git a/app/__main__.py b/app/__main__.py deleted file mode 100644 index a200610..0000000 --- a/app/__main__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Entry point for running the application as a module.""" - -import logging - -import fire - -logger = logging.getLogger(__name__) - - -def main(verbosity: str = "INFO") -> None: - """Run the application. - - Args: - verbosity: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL). - """ - logging.basicConfig( - level=getattr(logging, verbosity.upper(), logging.INFO), - format="%(levelname)s - %(name)s: %(message)s", - ) - logger.info("Ready.") - - -if __name__ == "__main__": - fire.Fire(main) diff --git a/docs/assets/banner.svg b/assets/banner.svg similarity index 100% rename from docs/assets/banner.svg rename to assets/banner.svg diff --git a/docs/assets/logo.svg b/assets/logo.svg similarity index 100% rename from docs/assets/logo.svg rename to assets/logo.svg diff --git a/docs/branding.md b/branding.md similarity index 100% rename from docs/branding.md rename to branding.md diff --git a/docs/container.md b/container.md similarity index 100% rename from docs/container.md rename to container.md diff --git a/docs/context.md b/context.md similarity index 100% rename from docs/context.md rename to context.md diff --git a/coverage/.gitignore b/coverage/.gitignore new file mode 100644 index 0000000..ccccf14 --- /dev/null +++ b/coverage/.gitignore @@ -0,0 +1,2 @@ +# Created by coverage.py +* diff --git a/coverage/class_index.html b/coverage/class_index.html new file mode 100644 index 0000000..32d2fe3 --- /dev/null +++ b/coverage/class_index.html @@ -0,0 +1,131 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.5, + created at 2026-04-21 17:43 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Fileclass statementsmissingexcluded coverage
app / __init__.py(no class) 000 100%
app / __main__.py(no class) 602 100%
Total  602 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/coverage/coverage_html_cb_dd2e7eb5.js b/coverage/coverage_html_cb_dd2e7eb5.js new file mode 100644 index 0000000..6f87174 --- /dev/null +++ b/coverage/coverage_html_cb_dd2e7eb5.js @@ -0,0 +1,735 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// General helpers +function debounce(callback, wait) { + let timeoutId = null; + return function(...args) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + callback.apply(this, args); + }, wait); + }; +}; + +function checkVisible(element) { + const rect = element.getBoundingClientRect(); + const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight); + const viewTop = 30; + return !(rect.bottom < viewTop || rect.top >= viewBottom); +} + +function on_click(sel, fn) { + const elt = document.querySelector(sel); + if (elt) { + elt.addEventListener("click", fn); + } +} + +// Helpers for table sorting +function getCellValue(row, column = 0) { + const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.childElementCount == 1) { + var child = cell.firstElementChild; + if (child.tagName === "A") { + child = child.firstElementChild; + } + if (child instanceof HTMLDataElement && child.value) { + return child.value; + } + } + return cell.innerText || cell.textContent; +} + +function rowComparator(rowA, rowB, column = 0) { + let valueA = getCellValue(rowA, column); + let valueB = getCellValue(rowB, column); + if (!isNaN(valueA) && !isNaN(valueB)) { + return valueA - valueB; + } + return valueA.localeCompare(valueB, undefined, {numeric: true}); +} + +function sortColumn(th) { + // Get the current sorting direction of the selected header, + // clear state on other headers and then set the new sorting direction. + const currentSortOrder = th.getAttribute("aria-sort"); + [...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none")); + var direction; + if (currentSortOrder === "none") { + direction = th.dataset.defaultSortOrder || "ascending"; + } + else if (currentSortOrder === "ascending") { + direction = "descending"; + } + else { + direction = "ascending"; + } + th.setAttribute("aria-sort", direction); + + const column = [...th.parentElement.cells].indexOf(th) + + // Sort all rows and afterwards append them in order to move them in the DOM. + Array.from(th.closest("table").querySelectorAll("tbody tr")) + .sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1)) + .forEach(tr => tr.parentElement.appendChild(tr)); + + // Save the sort order for next time. + if (th.id !== "region") { + let th_id = "file"; // Sort by file if we don't have a column id + let current_direction = direction; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)) + } + localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({ + "th_id": th.id, + "direction": current_direction + })); + if (th.id !== th_id || document.getElementById("region")) { + // Sort column has changed, unset sorting by function or class. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": false, + "region_direction": current_direction + })); + } + } + else { + // Sort column has changed to by function or class, remember that. + localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({ + "by_region": true, + "region_direction": direction + })); + } +} + +// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + document.querySelectorAll("[data-shortcut]").forEach(element => { + document.addEventListener("keypress", event => { + if (event.target.tagName.toLowerCase() === "input") { + return; // ignore keypress from search filter + } + if (event.key === element.dataset.shortcut) { + element.click(); + } + }); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Populate the filter and hide100 inputs if there are saved values for them. + const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE); + if (saved_filter_value) { + document.getElementById("filter").value = saved_filter_value; + } + const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE); + if (saved_hide100_value) { + document.getElementById("hide100").checked = JSON.parse(saved_hide100_value); + } + + // Cache elements. + const table = document.querySelector("table.index"); + const table_body_rows = table.querySelectorAll("tbody tr"); + const no_rows = document.getElementById("no_rows"); + + const footer = table.tFoot.rows[0]; + const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio)); + + // Observe filter keyevents. + const filter_handler = (event => { + // Keep running total of each metric, first index contains number of shown rows + const totals = ratio_columns.map( + is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0 + ); + + var text = document.getElementById("filter").value; + // Store filter value + localStorage.setItem(coverage.FILTER_STORAGE, text); + const casefold = (text === text.toLowerCase()); + const hide100 = document.getElementById("hide100").checked; + // Store hide value. + localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100)); + + // Hide / show elements. + table_body_rows.forEach(row => { + var show = false; + // Check the text filter. + for (let column = 0; column < totals.length; column++) { + cell = row.cells[column]; + if (cell.classList.contains("name")) { + var celltext = cell.textContent; + if (casefold) { + celltext = celltext.toLowerCase(); + } + if (celltext.includes(text)) { + show = true; + } + } + } + + // Check the "hide covered" filter. + if (show && hide100) { + const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" "); + show = (numer !== denom); + } + + if (!show) { + // hide + row.classList.add("hidden"); + return; + } + + // show + row.classList.remove("hidden"); + totals[0]++; + + for (let column = 0; column < totals.length; column++) { + // Accumulate dynamic totals + cell = row.cells[column] // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + if (ratio_columns[column] && cell.dataset.ratio) { + // Column stores a ratio + const [numer, denom] = cell.dataset.ratio.split(" "); + totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection + totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection + } + else { + totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection + } + } + }); + + // Show placeholder if no rows will be displayed. + if (!totals[0]) { + // Show placeholder, hide table. + no_rows.style.display = "block"; + table.style.display = "none"; + return; + } + + // Hide placeholder, show table. + no_rows.style.display = null; + table.style.display = null; + + // Calculate new dynamic sum values based on visible rows. + for (let column = 0; column < totals.length; column++) { + // Get footer cell element. + const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection + if (cell.matches(".name, .spacer")) { + continue; + } + + // Set value into dynamic footer cell element. + if (ratio_columns[column]) { + // Percentage column uses the numerator and denominator, + // and adapts to the number of decimal places. + const match = /\.([0-9]+)/.exec(cell.textContent); + const places = match ? match[1].length : 0; + const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection + cell.dataset.ratio = `${numer} ${denom}`; + // Check denom to prevent NaN if filtered files contain no statements + cell.textContent = denom + ? `${(numer * 100 / denom).toFixed(places)}%` + : `${(100).toFixed(places)}%`; + } + else { + cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection + } + } + }); + + document.getElementById("filter").addEventListener("input", debounce(filter_handler)); + document.getElementById("hide100").addEventListener("input", debounce(filter_handler)); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + document.getElementById("filter").dispatchEvent(new Event("input")); + document.getElementById("hide100").dispatchEvent(new Event("input")); +}; +coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE"; +coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE"; + +// Set up the click-to-sort columns. +coverage.wire_up_sorting = function () { + document.querySelectorAll("[data-sortable] th[aria-sort]").forEach( + th => th.addEventListener("click", e => sortColumn(e.target)) + ); + + // Look for a localStorage item containing previous sort settings: + let th_id = "file", direction = "ascending"; + const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE); + if (stored_list) { + ({th_id, direction} = JSON.parse(stored_list)); + } + let by_region = false, region_direction = "ascending"; + const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION); + if (sorted_by_region) { + ({ + by_region, + region_direction + } = JSON.parse(sorted_by_region)); + } + + const region_id = "region"; + if (by_region && document.getElementById(region_id)) { + direction = region_direction; + } + // If we are in a page that has a column with id of "region", sort on + // it if the last sort was by function or class. + let th; + if (document.getElementById(region_id)) { + th = document.getElementById(by_region ? region_id : th_id); + } + else { + th = document.getElementById(th_id); + } + th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending"); + th.click() +}; + +coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2"; +coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION"; + +// Loaded on index.html +coverage.index_ready = function () { + coverage.assign_shortkeys(); + coverage.wire_up_filter(); + coverage.wire_up_sorting(); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + + on_click(".button_show_hide_help", coverage.show_hide_help); +}; + +// -- pyfile stuff -- + +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + +coverage.pyfile_ready = function () { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === "t") { + document.querySelector(frag).closest(".n").classList.add("highlight"); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + on_click(".button_toggle_run", coverage.toggle_lines); + on_click(".button_toggle_mis", coverage.toggle_lines); + on_click(".button_toggle_exc", coverage.toggle_lines); + on_click(".button_toggle_par", coverage.toggle_lines); + + on_click(".button_next_chunk", coverage.to_next_chunk_nicely); + on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely); + on_click(".button_top_of_page", coverage.to_top); + on_click(".button_first_chunk", coverage.to_first_chunk); + + on_click(".button_prev_file", coverage.to_prev_file); + on_click(".button_next_file", coverage.to_next_file); + on_click(".button_to_index", coverage.to_index); + + on_click(".button_show_hide_help", coverage.show_hide_help); + + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection + } + + coverage.assign_shortkeys(); + coverage.init_scroll_markers(); + coverage.wire_up_sticky_header(); + + document.querySelectorAll("[id^=ctxs]").forEach( + cbox => cbox.addEventListener("click", coverage.expand_contexts) + ); + + // Rebuild scroll markers when the window height changes. + window.addEventListener("resize", coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (event) { + const btn = event.target.closest("button"); + const category = btn.value + const show = !btn.classList.contains("show_" + category); + coverage.set_line_visibilty(category, show); + coverage.build_scroll_markers(); + coverage.filters[category] = show; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (category, should_show) { + const cls = "show_" + category; + const btn = document.querySelector(".button_toggle_" + category); + if (btn) { + if (should_show) { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls)); + btn.classList.add(cls); + } + else { + document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls)); + btn.classList.remove(cls); + } + } +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return document.getElementById("t" + n)?.closest("p"); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +coverage.to_prev_file = function () { + window.location = document.getElementById("prevFileLink").href; +} + +coverage.to_next_file = function () { + window.location = document.getElementById("nextFileLink").href; +} + +coverage.to_index = function () { + location.href = document.getElementById("indexLink").href; +} + +coverage.show_hide_help = function () { + const helpCheck = document.getElementById("help_panel_state") + helpCheck.checked = !helpCheck.checked; +} + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + const classes = line_elt?.className; + if (!classes) { + return null; + } + const match = classes.match(/\bshow_\w+\b/); + if (!match) { + return null; + } + return match[0]; +}; + +coverage.to_next_chunk = function () { + const c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + const c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 1 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + if (probe <= 0) { + return; + } + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + const begin = coverage.line_elt(coverage.sel_begin); + const end = coverage.line_elt(coverage.sel_end-1); + + return ( + (checkVisible(begin) ? 1 : 0) + + (checkVisible(end) ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the top line on the screen as selection. + + // This will select the top-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(0, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(1); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: + // Set the lowest line on the screen as selection. + + // This will select the bottom-left of the viewport + // As this is most likely the span with the line number we take the parent + const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement; + if (line.parentElement !== document.getElementById("source")) { + // The element is not a source line but the header or similar + coverage.select_line_or_chunk(coverage.lines_len); + } + else { + // We extract the line number from the id + coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10)); + } + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (!probe_line) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (!probe_line) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + // Highlight the lines in the chunk + document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight")); + for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) { + coverage.line_elt(probe).querySelector(".n").classList.add("highlight"); + } + + coverage.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + const element = coverage.line_elt(coverage.sel_begin); + coverage.scroll_window(element.offsetTop - 60); + } +}; + +coverage.scroll_window = function (to_pos) { + window.scroll({top: to_pos, behavior: "smooth"}); +}; + +coverage.init_scroll_markers = function () { + // Init some variables + coverage.lines_len = document.querySelectorAll("#source > p").length; + + // Build html + coverage.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + const temp_scroll_marker = document.getElementById("scroll_marker") + if (temp_scroll_marker) temp_scroll_marker.remove(); + // Don't build markers if the window has no scroll bar. + if (document.body.scrollHeight <= window.innerHeight) { + return; + } + + const marker_scale = window.innerHeight / document.body.scrollHeight; + const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10); + + let previous_line = -99, last_mark, last_top; + + const scroll_marker = document.createElement("div"); + scroll_marker.id = "scroll_marker"; + document.getElementById("source").querySelectorAll( + "p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par" + ).forEach(element => { + const line_top = Math.floor(element.offsetTop * marker_scale); + const line_number = parseInt(element.querySelector(".n a").id.substr(1)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.style.height = `${line_top + line_height - last_top}px`; + } + else { + // Add colored line in scroll_marker block. + last_mark = document.createElement("div"); + last_mark.id = `m${line_number}`; + last_mark.classList.add("marker"); + last_mark.style.height = `${line_height}px`; + last_mark.style.top = `${line_top}px`; + scroll_marker.append(last_mark); + last_top = line_top; + } + + previous_line = line_number; + }); + + // Append last to prevent layout calculation + document.body.append(scroll_marker); +}; + +coverage.wire_up_sticky_header = function () { + const header = document.querySelector("header"); + const header_bottom = ( + header.querySelector(".content h2").getBoundingClientRect().top - + header.getBoundingClientRect().top + ); + + function updateHeader() { + if (window.scrollY > header_bottom) { + header.classList.add("sticky"); + } + else { + header.classList.remove("sticky"); + } + } + + window.addEventListener("scroll", updateHeader); + updateHeader(); +}; + +coverage.expand_contexts = function (e) { + var ctxs = e.target.parentNode.querySelector(".ctxs"); + + if (!ctxs.classList.contains("expanded")) { + var ctxs_text = ctxs.textContent; + var width = Number(ctxs_text[0]); + ctxs.textContent = ""; + for (var i = 1; i < ctxs_text.length; i += width) { + key = ctxs_text.substring(i, i + width).trim(); + ctxs.appendChild(document.createTextNode(contexts[key])); + ctxs.appendChild(document.createElement("br")); + } + ctxs.classList.add("expanded"); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + if (document.body.classList.contains("indexfile")) { + coverage.index_ready(); + } + else { + coverage.pyfile_ready(); + } +}); diff --git a/coverage/favicon_32_cb_c827f16f.png b/coverage/favicon_32_cb_c827f16f.png new file mode 100644 index 0000000..8649f04 Binary files /dev/null and b/coverage/favicon_32_cb_c827f16f.png differ diff --git a/coverage/function_index.html b/coverage/function_index.html new file mode 100644 index 0000000..b0ffe21 --- /dev/null +++ b/coverage/function_index.html @@ -0,0 +1,141 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.5, + created at 2026-04-21 17:43 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Filefunction statementsmissingexcluded coverage
app / __init__.py(no function) 000 100%
app / __main__.pymain 200 100%
app / __main__.py(no function) 402 100%
Total  602 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/coverage/index.html b/coverage/index.html new file mode 100644 index 0000000..b631776 --- /dev/null +++ b/coverage/index.html @@ -0,0 +1,126 @@ + + + + + Coverage report + + + + + +
+
+

Coverage report: + 100% +

+ +
+ +
+ + +
+
+

+ Files + Functions + Classes +

+

+ coverage.py v7.13.5, + created at 2026-04-21 17:43 +0000 +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File statementsmissingexcluded coverage
app / __init__.py 000 100%
app / __main__.py 602 100%
Total 602 100%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/coverage/keybd_closed_cb_900cfef5.png b/coverage/keybd_closed_cb_900cfef5.png new file mode 100644 index 0000000..ba119c4 Binary files /dev/null and b/coverage/keybd_closed_cb_900cfef5.png differ diff --git a/coverage/status.json b/coverage/status.json new file mode 100644 index 0000000..56bff63 --- /dev/null +++ b/coverage/status.json @@ -0,0 +1 @@ +{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.13.5","globals":"c60d5a9173fc3fe84ddd86701d19ab9c","files":{"z_5f5a17c013354698___init___py":{"hash":"ebec8d5020931e512a945fa346dc7ffc","index":{"url":"z_5f5a17c013354698___init___py.html","file":"app/__init__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":0,"n_excluded":0,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_5f5a17c013354698___main___py":{"hash":"c2993f0990b7f8d443cc10002679c060","index":{"url":"z_5f5a17c013354698___main___py.html","file":"app/__main__.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":6,"n_excluded":2,"n_missing":0,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}} \ No newline at end of file diff --git a/coverage/style_cb_9ff733b0.css b/coverage/style_cb_9ff733b0.css new file mode 100644 index 0000000..5e304ce --- /dev/null +++ b/coverage/style_cb_9ff733b0.css @@ -0,0 +1,389 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +.hidden { display: none; } + +header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; } + +@media (prefers-color-scheme: dark) { header { background: black; } } + +@media (prefers-color-scheme: dark) { header { border-color: #333; } } + +header .content { padding: 1rem 3.5rem; } + +header h2 { margin-top: .5em; font-size: 1em; } + +header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } } + +@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } } + +header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } } + +header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } } + +header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; } + +header.sticky .text { display: none; } + +header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; } + +header.sticky .content { padding: 0.5rem 3.5rem; } + +header.sticky .content p { font-size: 1em; } + +header.sticky ~ #source { padding-top: 6.5em; } + +main { position: relative; z-index: 1; } + +footer { margin: 1rem 3.5rem; } + +footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3.5rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; } + +#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } } + +#filter_container #filter:focus { border-color: #007acc; } + +#filter_container :disabled ~ label { color: #ccc; } + +@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } } + +#filter_container label { font-size: .875em; color: #666; } + +@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } } + +header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { header button { background: #333; } } + +@media (prefers-color-scheme: dark) { header button { border-color: #444; } } + +header button:active, header button:focus { outline: 2px dashed #007acc; } + +header button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } } + +header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } } + +header button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } } + +header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } } + +header button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { header button.exc { background: #333; } } + +header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } } + +header button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { header button.par { background: #650; } } + +header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } } + +#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#help_panel_wrapper { float: right; position: relative; } + +#keyboard_icon { margin: 5px; } + +#help_panel_state { display: none; } + +#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; } + +#help_panel .keyhelp p { margin-top: .75em; } + +#help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile #help_panel { width: 25em; } + +.pyfile #help_panel { width: 18em; } + +#help_panel_state:checked ~ #help_panel { display: block; } + +kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; } + +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n.highlight { background: #ffdd00; } + +#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str, #source p .t .fst { color: #0451a5; } + +@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; } + +#source p.mis.mis2.show_mis .t { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } } + +#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; } + +#source p.run.run2.show_run .t { background: #eeffee; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } } + +#source p.run.run2.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; } + +#source p.exc.exc2.show_exc .t { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } } + +#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #bbbb00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; } + +#source p.par.par2.show_par .t { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } } + +#source p.par.par2.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "β–Ά "; } + +#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "β–Ό "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; } + +#index td.left, #index th.left { text-align: left; } + +#index td.spacer, #index th.spacer { border: none; padding: 0; } + +#index td.spacer:hover, #index th.spacer:hover { background: inherit; } + +#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +@media (prefers-color-scheme: dark) { #index th { border-color: #444; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; } + +#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; } + +@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } } + +#index th[aria-sort="ascending"] .arrows::after { content: " β–²"; } + +#index th[aria-sort="descending"] .arrows::after { content: " β–Ό"; } + +#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; } + +@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } } + +#index td.name { font-size: 1.15em; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index td.name .no-noun { font-style: italic; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; } + +#index tr.region:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } } + +#index tr.region:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/coverage/z_5f5a17c013354698___init___py.html b/coverage/z_5f5a17c013354698___init___py.html new file mode 100644 index 0000000..b8f153b --- /dev/null +++ b/coverage/z_5f5a17c013354698___init___py.html @@ -0,0 +1,98 @@ + + + + + Coverage for app/__init__.py: 100% + + + + + +
+
+

+ Coverage for app / __init__.py: + 100% +

+ +

+ 0 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.5, + created at 2026-04-21 17:43 +0000 +

+ +
+
+
+

1"""Application package.""" 

+
+ + + diff --git a/coverage/z_5f5a17c013354698___main___py.html b/coverage/z_5f5a17c013354698___main___py.html new file mode 100644 index 0000000..77319cf --- /dev/null +++ b/coverage/z_5f5a17c013354698___main___py.html @@ -0,0 +1,121 @@ + + + + + Coverage for app/__main__.py: 100% + + + + + +
+
+

+ Coverage for app / __main__.py: + 100% +

+ +

+ 6 statements   + + + +

+

+ « prev     + ^ index     + » next +       + coverage.py v7.13.5, + created at 2026-04-21 17:43 +0000 +

+ +
+
+
+

1"""Entry point for running the application as a module.""" 

+

2 

+

3import logging 

+

4 

+

5import fire 

+

6 

+

7logger = logging.getLogger(__name__) 

+

8 

+

9 

+

10def main(verbosity: str = "INFO") -> None: 

+

11 """Run the application. 

+

12 

+

13 Args: 

+

14 verbosity: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL). 

+

15 """ 

+

16 logging.basicConfig( 

+

17 level=getattr(logging, verbosity.upper(), logging.INFO), 

+

18 format="%(levelname)s - %(name)s: %(message)s", 

+

19 ) 

+

20 logger.info("Ready.") 

+

21 

+

22 

+

23if __name__ == "__main__": 

+

24 fire.Fire(main) 

+
+ + + diff --git a/docs/discovery.md b/discovery.md similarity index 100% rename from docs/discovery.md rename to discovery.md diff --git a/docs/features/completed/.gitkeep b/features/backlog/.gitkeep similarity index 100% rename from docs/features/completed/.gitkeep rename to features/backlog/.gitkeep diff --git a/docs/features/in-progress/.gitkeep b/features/completed/.gitkeep similarity index 100% rename from docs/features/in-progress/.gitkeep rename to features/completed/.gitkeep diff --git a/docs/features/completed/display-version.feature b/features/completed/display-version.feature similarity index 100% rename from docs/features/completed/display-version.feature rename to features/completed/display-version.feature diff --git a/docs/post-mortem/.gitkeep b/features/in-progress/.gitkeep similarity index 100% rename from docs/post-mortem/.gitkeep rename to features/in-progress/.gitkeep diff --git a/docs/index.html b/index.html similarity index 100% rename from docs/index.html rename to index.html diff --git a/post-mortem/.gitkeep b/post-mortem/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index a338217..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,158 +0,0 @@ -[project] -name = "python-project-template" -version = "6.4.20260420" -description = "Python template with some awesome tools to quickstart any Python project" -readme = "README.md" -requires-python = ">=3.13" -license = { file = "LICENSE" } -authors = [ - { name = "eol", email = "nullhack@users.noreply.github.com" } -] -maintainers = [ - { name = "eol", email = "nullhack@users.noreply.github.com" } -] -dependencies = [ - "fire>=0.7.1", -] - -[project.urls] -Repository = "https://github.com/nullhack/python-project-template" -Documentation = "https://github.com/nullhack/python-project-template/tree/main/docs/api/" - -[project.optional-dependencies] -dev = [ - "pdoc>=14.0", - "pytest>=9.0.3", - "pytest-beehave[html]>=3.3,<4", - "pytest-cov>=6.1.1", - "pytest-mock>=3.14.0", - "ruff>=0.11.5", - "taskipy>=1.14.1", - "hypothesis>=6.148.4", - "pyright>=1.1.407", - "ghp-import>=2.1.0", -] - -[tool.setuptools] -packages = ["app"] - -[tool.ruff.lint] -ignore = [] -select = [ - "A", - "ANN", - "ASYNC", - "B", - "C4", - "C9", - "D", - "DTZ", - "E", - "ERA", - "F", - "FURB", - "G", - "I", - "ICN", - "LOG", - "N", - "NPY", - "PD", - "PT", - "PTH", - "R", - "RUF", - "S", - "SIM", - "T20", - "TD", - "W", -] -preview = true -mccabe.max-complexity = 10 -pydocstyle.convention = "google" - -[tool.ruff.lint.per-file-ignores] -"tests/**" = ["S101", "ANN", "D205", "D212", "D415", "D100", "D103"] -".opencode/skills/**/scripts/*.py" = ["T20"] - -[tool.pytest.ini_options] -minversion = "6.0" -markers = [ - "slow: marks tests as slow (deselect with '-m \"not slow\"')", - "deprecated: marks tests for deprecated AC; automatically skipped (deselect with '-m \"not deprecated\"')", - "bug: marks tests that reproduce a reported defect (deselect with '-m \"not bug\"')", -] -addopts = """ ---maxfail=10 \ ---color=yes \ ---tb=short \ --q \ -""" -testpaths = ["tests"] -python_files = ["*_test.py"] -python_functions = ["test_*"] -render_collapsed = "all" - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "if 0:", - "if __name__ == .__main__.:", -] - -[tool.taskipy.tasks] -run = "python -m app" -test-coverage = """\ -pytest \ - --cov-config=pyproject.toml \ - --cov=app \ - --cov-fail-under=100 \ - --tb=no -""" -test-build = """\ -pytest \ - -p no:beehave \ - --doctest-modules \ - --cov-config=pyproject.toml \ - --cov-report html:docs/coverage \ - --cov-report term:skip-covered \ - --cov=app \ - --cov-fail-under=100 \ - --hypothesis-show-statistics \ - --html=docs/tests/report.html \ - --self-contained-html \ -""" -test = "pytest --tb=short" -test-fast = "pytest -m \"not slow\" -q --no-header --tb=no" -test-slow = "pytest -m slow" -ruff-check = "ruff check . --fix" -ruff-format = "ruff format ." -ruff-format-check = "ruff format . --check" -lint = "task ruff-check && task ruff-format" -doc-serve = "pdoc ./app --host localhost --port 8080" -doc-build = """\ -pdoc ./app -o docs/api --search && \ -pytest \ - --cov-config=pyproject.toml \ - --cov-report html:docs/coverage \ - --cov=app \ - --html=docs/tests/report.html \ - --self-contained-html \ - -q \ -""" -doc-publish = "task doc-build && ghp-import -n -p -f docs" -static-check = "pyright" - -[dependency-groups] -dev = [ - "gherkin-official>=39.0.0", - "safety>=3.7.0", -] - -[tool.beehave] -features_path = "docs/features" diff --git a/docs/research/README.md b/research/README.md similarity index 100% rename from docs/research/README.md rename to research/README.md diff --git a/docs/research/ai-agents.md b/research/ai-agents.md similarity index 100% rename from docs/research/ai-agents.md rename to research/ai-agents.md diff --git a/docs/research/architecture.md b/research/architecture.md similarity index 100% rename from docs/research/architecture.md rename to research/architecture.md diff --git a/docs/research/cognitive-science.md b/research/cognitive-science.md similarity index 100% rename from docs/research/cognitive-science.md rename to research/cognitive-science.md diff --git a/docs/research/documentation.md b/research/documentation.md similarity index 100% rename from docs/research/documentation.md rename to research/documentation.md diff --git a/docs/research/domain-modeling.md b/research/domain-modeling.md similarity index 100% rename from docs/research/domain-modeling.md rename to research/domain-modeling.md diff --git a/docs/research/oop-design.md b/research/oop-design.md similarity index 100% rename from docs/research/oop-design.md rename to research/oop-design.md diff --git a/docs/research/refactoring-empirical.md b/research/refactoring-empirical.md similarity index 100% rename from docs/research/refactoring-empirical.md rename to research/refactoring-empirical.md diff --git a/docs/research/requirements-elicitation.md b/research/requirements-elicitation.md similarity index 100% rename from docs/research/requirements-elicitation.md rename to research/requirements-elicitation.md diff --git a/docs/research/software-economics.md b/research/software-economics.md similarity index 100% rename from docs/research/software-economics.md rename to research/software-economics.md diff --git a/docs/research/testing.md b/research/testing.md similarity index 100% rename from docs/research/testing.md rename to research/testing.md diff --git a/docs/research/version-control.md b/research/version-control.md similarity index 100% rename from docs/research/version-control.md rename to research/version-control.md diff --git a/docs/scope_journal.md b/scope_journal.md similarity index 100% rename from docs/scope_journal.md rename to scope_journal.md diff --git a/docs/system.md b/system.md similarity index 100% rename from docs/system.md rename to system.md diff --git a/template-config.yaml b/template-config.yaml deleted file mode 100644 index c3d5160..0000000 --- a/template-config.yaml +++ /dev/null @@ -1,148 +0,0 @@ -# Template configuration β€” single source of truth for project setup. -# Read by the setup-project agent. The "defaults" section reflects the current -# template values. The "substitutions" section maps every literal string in every -# template file to its replacement pattern, using {variable} tokens. -# -# After running @setup-project the defaults section is updated with the values -# the user provided. -# -# Parameter descriptions: -# github_username β€” GitHub handle used in URLs and git remote -# project_name β€” kebab-case repository name (e.g. my-awesome-project) -# package_name β€” snake_case Python package directory (e.g. my_awesome_project) -# project_description β€” one sentence describing what the project does -# author_name β€” author's full name -# author_email β€” author's email address - -defaults: - github_username: nullhack - project_name: python-project-template - package_name: app - project_description: "Python template with some awesome tools to quickstart any Python project" - author_name: eol - author_email: nullhack@users.noreply.github.com - -# Substitution map β€” every file the setup agent must edit. -# Each entry has: -# old: literal string currently in the file -# new: replacement string with {variable} tokens -# count: expected number of replacements (for agent verification) - -substitutions: - pyproject.toml: - - old: 'name = "python-project-template"' - new: 'name = "{project_name}"' - count: 1 - - old: '"Python template with some awesome tools to quickstart any Python project"' - new: '"{project_description}"' - count: 1 - - old: '{ name = "eol", email = "nullhack@users.noreply.github.com" }' - new: '{{ name = "{author_name}", email = "{author_email}" }}' - count: 2 - - old: "https://github.com/nullhack/python-project-template" - new: "https://github.com/{github_username}/{project_name}" - count: 2 - - old: 'packages = ["app"]' - new: 'packages = ["{package_name}"]' - count: 1 - - old: "python -m app" - new: "python -m {package_name}" - count: 1 - - old: "--cov=app" - new: "--cov={package_name}" - count: 2 - - old: "pdoc ./app" - new: "pdoc ./{package_name}" - count: 2 - # Version: set to 0.1.YYYYMMDD using today's date (agent action, not text substitution) - - README.md: - - old: "nullhack" - new: "{github_username}" - count: many - - old: "python-project-template" - new: "{project_name}" - count: many - - old: "eol" - new: "{author_name}" - count: 1 - note: "only the author credit line β€” do not replace occurrences in other contexts" - - .github/workflows/ci.yml: - - old: "import app" - new: "import {package_name}" - count: 2 - - Dockerfile: - - old: "# Simplified Dockerfile for python-project-template" - new: "# Simplified Dockerfile for {project_name}" - count: 1 - - old: "CMD python -m app || exit 1" - new: "CMD python -m {package_name} || exit 1" - count: 1 - - old: 'CMD ["python", "-m", "app"]' - new: 'CMD ["python", "-m", "{package_name}"]' - count: 1 - - old: 'LABEL maintainer="eol"' - new: 'LABEL maintainer="{author_name}"' - count: 1 - - old: '"Python template with some awesome tools to quickstart any Python project"' - new: '"{project_description}"' - count: 1 - - old: "https://github.com/nullhack/python-project-template" - new: "https://github.com/{github_username}/{project_name}" - count: 1 - - docker-compose.yml: - - old: "# Docker Compose for python-project-template" - new: "# Docker Compose for {project_name}" - count: 1 - - old: "./app:/app/app" - new: "./{package_name}:/app/{package_name}" - count: 1 - - old: "python -m app" - new: "python -m {package_name}" - count: 1 - - old: "./app:/app/app:ro" - new: "./{package_name}:/app/{package_name}:ro" - count: 1 - - .dockerignore: - - old: "# Docker ignore file for python-project-template" - new: "# Docker ignore file for {project_name}" - count: 1 - - docs/index.html: - - old: 'href="api/app.html"' - new: 'href="api/{package_name}.html"' - count: 1 - - LICENSE: - - old: "Copyright (c) 2026, eol" - new: "Copyright (c) 2026, {author_name}" - count: 1 - - tests/unit/app_test.py: - - old: "from app.__main__ import" - new: "from {package_name}.__main__ import" - count: 1 - - template-config.yaml: - - old: "github_username: nullhack" - new: "github_username: {github_username}" - count: 1 - - old: "project_name: python-project-template" - new: "project_name: {project_name}" - count: 1 - - old: "package_name: app" - new: "package_name: {package_name}" - count: 1 - - old: '"Python template with some awesome tools to quickstart any Python project"' - new: '"{project_description}"' - count: 1 - - old: "author_name: eol" - new: "author_name: {author_name}" - count: 1 - - old: "author_email: nullhack@users.noreply.github.com" - new: "author_email: {author_email}" - count: 1 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index f1b390f..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests module.""" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index a5c8f50..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,23 +0,0 @@ -import pytest - - -def pytest_html_report_title(report): - report.title = "Test Report" - - -@pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item, call): - outcome = yield - report = outcome.get_result() - - docstring = item.obj.__doc__ or "" - report.docstrings = docstring - - -def pytest_html_results_table_header(cells): - cells.insert(2, "Documentation") - - -def pytest_html_results_table_row(report, cells): - docstring = getattr(report, "docstrings", "") or "" - cells.insert(2, f"{docstring}") diff --git a/tests/report.html b/tests/report.html new file mode 100644 index 0000000..8b55787 --- /dev/null +++ b/tests/report.html @@ -0,0 +1,1096 @@ + + + + + Test Report + + + + +

Test Report

+

Report generated on 21-Apr-2026 at 17:43:00 by pytest-html + v4.2.0

+
+

Environment

+
+
+ + + + + +
+
+

Summary

+
+
+

1 test took 00:00:01.

+

(Un)check the boxes to filter the results.

+
+ +
+
+
+
+ + 0 Failed, + + 1 Passed, + + 0 Skipped, + + 0 Expected failures, + + 0 Unexpected passes, + + 0 Errors, + + 0 Reruns + + 0 Retried, +
+
+  /  +
+
+
+
+
+
+
+
+ + + + + + + + + + + +
ResultTestDocumentationAcceptance CriteriaDurationLinks
+ + + \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e0310a0..0000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit tests.""" diff --git a/tests/unit/app_test.py b/tests/unit/app_test.py deleted file mode 100644 index 206c51f..0000000 --- a/tests/unit/app_test.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Unit tests for the application entry point.""" - -import pytest -from hypothesis import example, given -from hypothesis import strategies as st - -from app.__main__ import main - - -@given(verbosity=st.sampled_from(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])) -@example(verbosity="INFO") -def test_app_main_runs_with_valid_verbosity(verbosity: str) -> None: - """ - Given: A valid verbosity level string - When: main() is called with that verbosity - Then: It completes without raising an exception - """ - main(verbosity) diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 5aa2efe..0000000 --- a/uv.lock +++ /dev/null @@ -1,1219 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.13" - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "authlib" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "joserfc" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" }, -] - -[[package]] -name = "certifi" -version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "click" -version = "8.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, -] - -[[package]] -name = "dparse" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" }, -] - -[[package]] -name = "filelock" -version = "3.29.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, -] - -[[package]] -name = "fire" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "termcolor" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" }, -] - -[[package]] -name = "gherkin-official" -version = "39.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/cf/8c0f7ec0e041c12ab59fae0c01b95ac69113a2fecb45618780525f8ca5ee/gherkin_official-39.0.0.tar.gz", hash = "sha256:675b9c6c0c342b0ec44bddf927de923adbd79879277816ce96bf248533677060", size = 33683, upload-time = "2026-03-01T16:46:42.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/b3/743f97b16ef781283cde3c7b06a95b309a75ae2f4003a6611d35abc3c613/gherkin_official-39.0.0-py3-none-any.whl", hash = "sha256:1fd9b8709c00d946c0fd617a9834d4cb2af026213a2e8e7822fe24dd5064fe22", size = 38471, upload-time = "2026-03-01T16:46:43.308Z" }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "hypothesis" -version = "6.152.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sortedcontainers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/b1/c32bcddb9aab9e3abc700f1f56faf14e7655c64a16ca47701a57362276ea/hypothesis-6.152.1.tar.gz", hash = "sha256:4f4ed934eee295dd84ee97592477d23e8dc03e9f12ae0ee30a4e7c9ef3fca3b0", size = 465029, upload-time = "2026-04-14T22:29:24.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/83/860fb3075e00b0fc19a22a2301bc3c96f00437558c3911bdd0a3573a4a53/hypothesis-6.152.1-py3-none-any.whl", hash = "sha256:40a3619d9e0cb97b018857c7986f75cf5de2e5ec0fa8a0b172d00747758f749e", size = 530752, upload-time = "2026-04-14T22:29:20.893Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, -] - -[[package]] -name = "joserfc" -version = "1.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "markdown2" -version = "2.5.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/ae/07d4a5fcaa5509221287d289323d75ac8eda5a5a4ac9de2accf7bbcc2b88/markdown2-2.5.5.tar.gz", hash = "sha256:001547e68f6e7fcf0f1cb83f7e82f48aa7d48b2c6a321f0cd20a853a8a2d1664", size = 157249, upload-time = "2026-03-02T20:46:53.411Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/af/4b3891eb0a49d6cfd5cbf3e9bf514c943afc2b0f13e2c57cc57cd88ecc21/markdown2-2.5.5-py3-none-any.whl", hash = "sha256:be798587e09d1f52d2e4d96a649c4b82a778c75f9929aad52a2c95747fa26941", size = 56250, upload-time = "2026-03-02T20:46:52.032Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "marshmallow" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "mslex" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" }, -] - -[[package]] -name = "nltk" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[package]] -name = "packaging" -version = "26.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, -] - -[[package]] -name = "pdoc" -version = "16.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown2" }, - { name = "markupsafe" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "psutil" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, - { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, - { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, - { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, - { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, - { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836, upload-time = "2026-04-17T09:31:59.636Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/ca/b45c378e6e8d0b90577288b533e04e95b7afd61bb1d51b6c263176435489/pydantic-2.13.2-py3-none-any.whl", hash = "sha256:a525087f4c03d7e7456a3de89b64cd693d2229933bb1068b9af6befd5563694e", size = 471947, upload-time = "2026-04-17T09:31:57.541Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269, upload-time = "2026-04-17T09:10:07.017Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/2b/662e48254479a2d3450ba24b1e25061108b64339794232f503990c519144/pydantic_core-2.46.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d26e9eea3715008a09a74585fe9becd0c67fbb145dc4df9756d597d7230a652c", size = 2101762, upload-time = "2026-04-17T09:10:13.87Z" }, - { url = "https://files.pythonhosted.org/packages/73/ab/bafd7c7503757ccc8ec4d1911e106fe474c629443648c51a88f08b0fe91a/pydantic_core-2.46.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48b36e3235140510dc7861f0cd58b714b1cdd3d48f75e10ce52e69866b746f10", size = 1951814, upload-time = "2026-04-17T09:12:25.934Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/7549c2d57ba2e9a42caa5861a2d398dbe31c02c6aca783253ace59ce84f8/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b1f99dc451f1a3981f236151465bcf995bbe712d0727c9f7b236fe228a8133", size = 1977329, upload-time = "2026-04-17T09:13:37.605Z" }, - { url = "https://files.pythonhosted.org/packages/18/50/7ed4a8a0d478a4dca8f0134a5efa7193f03cc8520dd4c9509339fb2e5002/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8641c8d535c2d95b45c2e19b646ecd23ebba35d461e0ae48a3498277006250ab", size = 2051832, upload-time = "2026-04-17T09:12:49.771Z" }, - { url = "https://files.pythonhosted.org/packages/dc/16/bb35b193741c0298ddc5f5e4234269efdc0c65e2bcd198aa0de9b68845e4/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20fb194788a0a50993e87013e693494ba183a2af5b44e99cf060bbae10912b11", size = 2233127, upload-time = "2026-04-17T09:11:04.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/a5/98f4b637149185addea19e1785ea20c373cca31b202f589111d8209d9873/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9262d11d0cd11ee3303a95156939402bed6cedfe5ed0e331b95a283a4da6eb8b", size = 2297418, upload-time = "2026-04-17T09:11:25.929Z" }, - { url = "https://files.pythonhosted.org/packages/36/90/93a5d21990b152da7b7507b7fddb0b935f6a0984d57ac3ec45a6e17777a2/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac204542736aa295fa25f713b7fad6fc50b46ab7764d16087575c85f085174f3", size = 2093735, upload-time = "2026-04-17T09:12:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/14/22/b8b1ffdddf08b4e84380bcb67f41dbbf4c171377c1d36fc6290794bb2094/pydantic_core-2.46.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9a7c43a0584742dface3ca0daf6f719d46c1ac2f87cf080050f9ae052c75e1b2", size = 2127570, upload-time = "2026-04-17T09:11:53.906Z" }, - { url = "https://files.pythonhosted.org/packages/c6/26/e60d72b4e2d0ce1fa811044a974412ac1c567fe067d97b3e6b290530786e/pydantic_core-2.46.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd05e1edb6a90ad446fa268ab09e59202766b837597b714b2492db11ee87fab9", size = 2183524, upload-time = "2026-04-17T09:11:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/35/32/36bec7584a1eefb17dec4dfa1c946d3fe4440f466c5705b8adfda69c9a9f/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:91155b110788b5501abc7ea954f1d08606219e4e28e3c73a94124307c06efb80", size = 2185408, upload-time = "2026-04-17T09:10:57.228Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d6/1a5689d873620efd67d6b163db0c444c056adb0849b5bc33e2b9f09665a6/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e4e2c72a529fa03ff228be1d2b76944013f428220b764e03cc50ada67e17a42c", size = 2335171, upload-time = "2026-04-17T09:11:43.369Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/675104802abe8ef502b072050ee5f2e915251aa1a3af87e1015ce31ec42d/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:56291ec1a11c3499890c99a8fd9053b47e60fe837a77ec72c0671b1b8b3dce24", size = 2362743, upload-time = "2026-04-17T09:10:18.333Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bc/86c5dde4fa6e24467680eef5047da3c1a19be0a527d0d8e14aa76b39307c/pydantic_core-2.46.2-cp313-cp313-win32.whl", hash = "sha256:b50f9c5f826ddca1246f055148df939f5f3f2d0d96db73de28e2233f22210d4c", size = 1958074, upload-time = "2026-04-17T09:12:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/2a/97/2537e8c1282b2c4eb062580c0d7a4339e10b072b803d1ee0b7f1f0a5c22c/pydantic_core-2.46.2-cp313-cp313-win_amd64.whl", hash = "sha256:251a57788823230ca8cbc99e6245d1a2ed6e180ec4864f251c94182c580c7f2e", size = 2071741, upload-time = "2026-04-17T09:13:32.405Z" }, - { url = "https://files.pythonhosted.org/packages/da/aa/2ee75798706f9dbc4e76dbe59e41a396c5c311e3d6223b9cf6a5fa7780be/pydantic_core-2.46.2-cp313-cp313-win_arm64.whl", hash = "sha256:315d32d1a71494d6b4e1e14a9fa7a4329597b4c4340088ad7e1a9dafbeed92a9", size = 2025955, upload-time = "2026-04-17T09:10:15.567Z" }, - { url = "https://files.pythonhosted.org/packages/d0/96/a50ccb6b539ae780f73cea74905468777680e30c6c3bdf714b9d4c116ea0/pydantic_core-2.46.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4f59b45f3ef8650c0c736a57f59031d47ed9df4c0a64e83796849d7d14863a2d", size = 2097111, upload-time = "2026-04-17T09:10:49.617Z" }, - { url = "https://files.pythonhosted.org/packages/34/5f/fdead7b3afa822ab6e5a18ee0ecffd54937de1877c01ed13a342e0fb3f07/pydantic_core-2.46.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a075a29ebef752784a91532a1a85be6b234ccffec0a9d7978a92696387c3da6", size = 1951904, upload-time = "2026-04-17T09:12:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/95/e0/1c5d547e550cdab1bec737492aa08865337af6fe7fc9b96f7f45f17d9519/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d12d786e30c04a9d307c5d7080bf720d9bac7f1668191d8e37633a9562749e2", size = 1978667, upload-time = "2026-04-17T09:11:35.589Z" }, - { url = "https://files.pythonhosted.org/packages/0e/cb/665ce629e218c8228302cb94beff4f6531082a2c87d3ecc3d5e63a26f392/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d5e6d6343b0b5dcacb3503b5de90022968da8ed0ab9ab39d3eda71c20cbf84e", size = 2046721, upload-time = "2026-04-17T09:11:47.725Z" }, - { url = "https://files.pythonhosted.org/packages/77/e9/6cb2cf60f54c1472bbdfce19d957553b43dbba79d1d7b2930a195c594785/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:233eebac0999b6b9ba76eb56f3ec8fce13164aa16b6d2225a36a79e0f95b5973", size = 2228483, upload-time = "2026-04-17T09:12:08.837Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/93e018dd5571f781ebaeda8c0cf65398489d5bee9b1f484df0b6149b43b9/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cc0eee720dd2f14f3b7c349469402b99ad81a174ab49d3533974529e9d93992", size = 2294663, upload-time = "2026-04-17T09:12:52.053Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/49e57ca55c770c93d9bb046666a54949b42e3c9099a0c5fe94557873fe30/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83ee76bf2c9910513dbc19e7d82367131fa7508dedd6186a462393071cc11059", size = 2098742, upload-time = "2026-04-17T09:13:45.472Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b0/6e46b5cd3332af665f794b8cdeea206618a8630bd9e7bcc36864518fce81/pydantic_core-2.46.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:d61db38eb4ee5192f0c261b7f2d38e420b554df8912245e3546aee5c45e2fd78", size = 2125922, upload-time = "2026-04-17T09:12:54.304Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/40850c81585be443a2abfdf7f795f8fae831baf8e2f9b2133c8246ac671c/pydantic_core-2.46.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f09a713d17bcd55da8ab02ebd9110c5246a49c44182af213b5212800af8bc83", size = 2183000, upload-time = "2026-04-17T09:10:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/04/af/8493d7dfa03ebb7866909e577c6aa65ea0de7377b86023cc51d0c8e11db3/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:30cacc5fb696e64b8ef6fd31d9549d394dd7d52760db072eecb98e37e3af1677", size = 2180335, upload-time = "2026-04-17T09:12:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/72/5b/1f6a344c4ffdf284da41c6067b82d5ebcbd11ce1b515ae4b662d4adb6f61/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:7ccfb105fcfe91a22bbb5563ad3dc124bc1aa75bfd2e53a780ab05f78cdf6108", size = 2330002, upload-time = "2026-04-17T09:12:02.958Z" }, - { url = "https://files.pythonhosted.org/packages/25/ff/9a694126c12d6d2f48a0cafa6f8eef88ef0d8825600e18d03ff2e896c3b2/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:13ffef637dc8370c249e5b26bd18e9a80a4fca3d809618c44e18ec834a7ca7a8", size = 2359920, upload-time = "2026-04-17T09:10:27.764Z" }, - { url = "https://files.pythonhosted.org/packages/51/c8/3a35c763d68a9cb2675eb10ef242cf66c5d4701b28ae12e688d67d2c180e/pydantic_core-2.46.2-cp314-cp314-win32.whl", hash = "sha256:1b0ab6d756ca2704a938e6c31b53f290c2f9c10d3914235410302a149de1a83e", size = 1953701, upload-time = "2026-04-17T09:13:30.021Z" }, - { url = "https://files.pythonhosted.org/packages/1a/6a/f2726a780365f7dfd89d62036f984f7acb99978c60c5e1fa7c0cb898ed11/pydantic_core-2.46.2-cp314-cp314-win_amd64.whl", hash = "sha256:99ebade8c9ada4df975372d8dd25883daa0e379a05f1cd0c99aa0c04368d01a6", size = 2071867, upload-time = "2026-04-17T09:10:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/e1/79/76baacb9feba3d7c399b245ca1a29c74ea0db04ea693811374827eec2290/pydantic_core-2.46.2-cp314-cp314-win_arm64.whl", hash = "sha256:de87422197cf7f83db91d89c86a21660d749b3cd76cd8a45d115b8e675670f02", size = 2017252, upload-time = "2026-04-17T09:10:26.175Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3b/77c26938f817668d9ad9bab1a905cb23f11d9a3d4bf724d429b3e55a8eaf/pydantic_core-2.46.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:236f22b4a206b5b61db955396b7cf9e2e1ff77f372efe9570128ccfcd6a525eb", size = 2094545, upload-time = "2026-04-17T09:12:19.339Z" }, - { url = "https://files.pythonhosted.org/packages/fe/de/42c13f590e3c260966aa49bcdb1674774f975467c49abd51191e502bea28/pydantic_core-2.46.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c2012f64d2cd7cca50f49f22445aa5a88691ac2b4498ee0a9a977f8ca4f7289f", size = 1933953, upload-time = "2026-04-17T09:09:55.889Z" }, - { url = "https://files.pythonhosted.org/packages/4e/84/ebe3ebb3e2d8db656937cfa6f97f544cb7132f2307a4a7dfdcd0ea102a12/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d07d6c63106d3a9c9a333e2636f9c82c703b1a9e3b079299e58747964e4fdb72", size = 1974435, upload-time = "2026-04-17T09:10:12.371Z" }, - { url = "https://files.pythonhosted.org/packages/b9/15/0bf51ca6709477cd4ef86148b6d7844f3308f029eac361dd0383f1e17b1a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c326a2b4b85e959d9a1fc3a11f32f84611b6ec07c053e1828a860edf8d068208", size = 2031113, upload-time = "2026-04-17T09:10:00.752Z" }, - { url = "https://files.pythonhosted.org/packages/02/ae/b7b5af9b79db036d9e61a44c481c17a213dc8fc4b8b71fe6875a72fc778b/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac8a65e798f2462552c00d2e013d532c94d646729dda98458beaf51f9ec7b120", size = 2236325, upload-time = "2026-04-17T09:10:33.227Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ae/ecef7477b5a03d4a499708f7e75d2836452ebb70b776c2d64612b334f57a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a3c2bc1cc8164bedbc160b7bb1e8cc1e8b9c27f69ae4f9ae2b976cdae02b2dd", size = 2278135, upload-time = "2026-04-17T09:10:23.287Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/2f9d82faa47af6c39fc3f120145fd915971e1e0cb6b55b494fad9fdf8275/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69aa5e10b7e8b1bb4a6888650fd12fcbf11d396ca11d4a44de1450875702830", size = 2109071, upload-time = "2026-04-17T09:11:06.149Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9c/677cf10873fbd0b116575ab7b97c90482b21564f8a8040beb18edef7a577/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4e6df5c3301e65fb42bc5338bf9a1027a02b0a31dc7f54c33775229af474daf0", size = 2106028, upload-time = "2026-04-17T09:10:51.525Z" }, - { url = "https://files.pythonhosted.org/packages/d6/53/6a06183544daba51c059123a2064a99039df25f115a06bdb26f2ea177038/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c2f6e32548ac8d559b47944effcf8ae4d81c161f6b6c885edc53bc08b8f192d", size = 2164816, upload-time = "2026-04-17T09:11:56.187Z" }, - { url = "https://files.pythonhosted.org/packages/57/6f/10fcdd9e3eca66fc828eef0f6f5850f2dd3bca2c59e6e041fb8bc3da39be/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:b089a81c58e6ea0485562bbbbbca4f65c0549521606d5ef27fba217aac9b665a", size = 2166130, upload-time = "2026-04-17T09:10:03.804Z" }, - { url = "https://files.pythonhosted.org/packages/29/83/92d3fd0e0156cad2e3cb5c26de73794af78ac9fa0c22ab666e566dd67061/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:7f700a6d6f64112ae9193709b84303bbab84424ad4b47d0253301aabce9dfc70", size = 2316605, upload-time = "2026-04-17T09:12:45.249Z" }, - { url = "https://files.pythonhosted.org/packages/97/f1/facffdb970981068219582e499b8d0871ed163ffcc6b347de5c412669e4c/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:67db6814beaa5fefe91101ec7eb9efda613795767be96f7cf58b1ca8c9ca9972", size = 2358385, upload-time = "2026-04-17T09:09:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a1/b8160b2f22b2199467bc68581a4ed380643c16b348a27d6165c6c242d694/pydantic_core-2.46.2-cp314-cp314t-win32.whl", hash = "sha256:32fbc7447be8e3be99bf7869f7066308f16be55b61f9882c2cefc7931f5c7664", size = 1942373, upload-time = "2026-04-17T09:12:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/0d/90/db89acabe5b150e11d1b59fe3d947dda2ef6abbfef5c82f056ff63802f5d/pydantic_core-2.46.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b317a2b97019c0b95ce99f4f901ae383f40132da6706cdf1731066a73394c25c", size = 2052078, upload-time = "2026-04-17T09:10:19.96Z" }, - { url = "https://files.pythonhosted.org/packages/97/32/e19b83ceb07a3f1bb21798407790bbc9a31740158fd132b94139cb84e16c/pydantic_core-2.46.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7dcb9d40930dfad7ab6b20bcc6ca9d2b030b0f347a0cd9909b54bd53ead521b1", size = 2016941, upload-time = "2026-04-17T09:12:34.447Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pyright" -version = "1.1.408" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "pytest-beehave" -version = "3.3.20260419" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fire" }, - { name = "gherkin-official" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/55/5cac9181f3f30a37e700e002a3d6bc51f6d9b62f3ab13a142f56bec6039f/pytest_beehave-3.3.20260419.tar.gz", hash = "sha256:86f199b213b77cec7082e0d7f96b11ba61b92fc3dcecc2f2753a07f407ad1cbe", size = 35117, upload-time = "2026-04-20T01:01:02.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/76/2766d4dc64137ad05128728f84bd7f2c8ef7a433d30cada289dc6c1bcd0b/pytest_beehave-3.3.20260419-py3-none-any.whl", hash = "sha256:e0048a15af8ccd9e50c0841f43ac1e90bc9d2f5f59b22048d4b79e01016c5f0a", size = 35535, upload-time = "2026-04-20T01:00:59.897Z" }, -] - -[package.optional-dependencies] -html = [ - { name = "pytest-html" }, -] - -[[package]] -name = "pytest-cov" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, -] - -[[package]] -name = "pytest-html" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "pytest" }, - { name = "pytest-metadata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" }, -] - -[[package]] -name = "pytest-metadata" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-project-template" -version = "6.4.20260420" -source = { virtual = "." } -dependencies = [ - { name = "fire" }, -] - -[package.optional-dependencies] -dev = [ - { name = "ghp-import" }, - { name = "hypothesis" }, - { name = "pdoc" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-beehave", extra = ["html"] }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "ruff" }, - { name = "taskipy" }, -] - -[package.dev-dependencies] -dev = [ - { name = "gherkin-official" }, - { name = "safety" }, -] - -[package.metadata] -requires-dist = [ - { name = "fire", specifier = ">=0.7.1" }, - { name = "ghp-import", marker = "extra == 'dev'", specifier = ">=2.1.0" }, - { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.148.4" }, - { name = "pdoc", marker = "extra == 'dev'", specifier = ">=14.0" }, - { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.407" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" }, - { name = "pytest-beehave", extras = ["html"], marker = "extra == 'dev'", specifier = ">=3.3,<4" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, - { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.5" }, - { name = "taskipy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, -] -provides-extras = ["dev"] - -[package.metadata.requires-dev] -dev = [ - { name = "gherkin-official", specifier = ">=39.0.0" }, - { name = "safety", specifier = ">=3.7.0" }, -] - -[[package]] -name = "regex" -version = "2026.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, - { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, - { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, - { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, - { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, - { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, - { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, - { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, - { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, - { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, - { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, - { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, - { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, - { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, - { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, - { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, - { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, - { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, - { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, - { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, - { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, - { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, - { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, -] - -[[package]] -name = "requests" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, - { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, - { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, - { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, - { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, -] - -[[package]] -name = "safety" -version = "3.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "authlib" }, - { name = "click" }, - { name = "dparse" }, - { name = "filelock" }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "marshmallow" }, - { name = "nltk" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "ruamel-yaml" }, - { name = "safety-schemas" }, - { name = "tenacity" }, - { name = "tomlkit" }, - { name = "typer" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" }, -] - -[[package]] -name = "safety-schemas" -version = "0.0.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dparse" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "ruamel-yaml" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - -[[package]] -name = "taskipy" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "mslex", marker = "sys_platform == 'win32'" }, - { name = "psutil" }, - { name = "tomli", marker = "python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475, upload-time = "2024-11-26T16:37:46.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052, upload-time = "2024-11-26T16:37:44.546Z" }, -] - -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - -[[package]] -name = "termcolor" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, -] - -[[package]] -name = "tomlkit" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "typer" -version = "0.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -]