diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..ad523114 --- /dev/null +++ b/.envrc @@ -0,0 +1,18 @@ +# Automatically activate Python venv and sync dependencies on cd +# Requires: direnv (https://direnv.net/) +# Install: sudo apt install direnv && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc + +# Create venv if it doesn't exist +if [[ ! -d .venv ]]; then + echo "Creating .venv with uv..." + uv venv .venv +fi + +# Activate the venv +source .venv/bin/activate + +# Ensure dependencies are synced +uv sync --quiet + +# Set PYO3 to use this venv's Python +export PYO3_PYTHON="${PWD}/.venv/bin/python" diff --git a/.github/workflows/ci-minimal.yml b/.github/workflows/ci-minimal.yml deleted file mode 100644 index 3e6ac109..00000000 --- a/.github/workflows/ci-minimal.yml +++ /dev/null @@ -1,191 +0,0 @@ -name: CI (Minimal) - -on: - push: - branches: [main, "release/*", "feature/*", "feat/*", "fix/*"] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -env: - RUSTFLAGS: "-D warnings" - CARGO_TERM_COLOR: always - -jobs: - # Fast compilation check on MSRV and stable - check: - name: Check (${{ matrix.rust }}) - runs-on: [self-hosted, Linux, X64, fedora, nobara] - strategy: - matrix: - rust: [stable] # Use stable only - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@v1 - with: - toolchain: ${{ matrix.rust }} - - - name: Get Rust version - id: rust-version - run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ steps.rust-version.outputs.version }} - - - name: Type check - run: cargo check --workspace --all-targets --locked - - - name: Build - run: cargo build --workspace --locked - - # Linting and formatting (stable only) - lint: - name: Lint & Format - runs-on: [self-hosted, Linux, X64, fedora, nobara] - continue-on-error: true - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Get Rust version - id: rust-version - run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ steps.rust-version.outputs.version }} - - - name: Check formatting - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --all-targets --locked -- -D warnings - - - name: Build docs - run: cargo doc --workspace --no-deps --locked - - # Unit and integration tests (not E2E) - test: - name: Test - runs-on: [self-hosted, Linux, X64, fedora, nobara] - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - - name: Get Rust version - id: rust-version - run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ steps.rust-version.outputs.version }} - - # Core build dependencies only (no X11/text injection stuff) - - name: Verify core deps - run: | - set -euo pipefail - for cmd in gcc g++ make pkg-config; do - command -v $cmd || { echo "Missing: $cmd"; exit 1; } - done - pkg-config --exists alsa || { echo "Missing: libalsa-dev"; exit 1; } - - - name: Run tests - run: | - # Use nextest if available, otherwise fallback - if command -v cargo-nextest &>/dev/null; then - cargo nextest run --workspace --locked - else - cargo test --workspace --locked - fi - - # Optional: Text injection tests (only if tools available) - text-injection: - name: Text Injection (Optional) - runs-on: [self-hosted, Linux, X64, fedora, nobara] - continue-on-error: true # Don't fail CI if this fails - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - - name: Get Rust version - id: rust-version - run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ steps.rust-version.outputs.version }} - - - name: Check if tools available - id: check_tools - run: | - has_tools=true - for tool in xdotool Xvfb openbox; do - if ! command -v $tool &>/dev/null; then - echo "Missing: $tool" - has_tools=false - fi - done - echo "available=$has_tools" >> $GITHUB_OUTPUT - - - name: Run text injection tests - if: steps.check_tools.outputs.available == 'true' - env: - DISPLAY: :99 - run: | - # Start headless X server - Xvfb :99 -screen 0 1024x768x24 & - sleep 2 - openbox & - sleep 1 - - cargo test -p coldvox-text-injection --locked - - - name: Skip text injection tests - if: steps.check_tools.outputs.available != 'true' - run: echo "⚠️ Skipping - X11 tools not available" - - # Optional: Whisper E2E tests (only if model available) - whisper-e2e: - name: Whisper E2E (Optional) - runs-on: [self-hosted, Linux, X64, fedora, nobara] - continue-on-error: true # Don't fail CI if this fails - env: - WHISPER_MODEL_SIZE: tiny # Use tiny model for minimal CI - steps: - - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable - - - name: Get Rust version - id: rust-version - run: echo "version=$(rustc --version | cut -d' ' -f2)" >> $GITHUB_OUTPUT - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ steps.rust-version.outputs.version }} - - - name: Setup Whisper model - id: setup - run: bash scripts/ci/setup-whisper-cache.sh - - - name: Run Whisper Golden Master test - env: - WHISPER_MODEL_PATH: ${{ steps.setup.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ steps.setup.outputs.model_size }} - run: | - pip install faster-whisper - export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])") - cargo test -p coldvox-app --test golden_master -- --nocapture - - - name: Skip Whisper E2E - if: steps.setup.outputs.model_path == '' - run: echo "⚠️ Skipping - Whisper model not available" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d227f9b..79d73fdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,6 @@ defaults: shell: bash env: - RUSTFLAGS: "-D warnings" CARGO_TERM_COLOR: always WHISPER_MODEL_SIZE: tiny MIN_FREE_DISK_GB: 10 @@ -68,10 +67,17 @@ jobs: for wf in "${files[@]}" do echo "-- $wf" + # gh workflow view requires the workflow to exist on default branch first. + # For new workflows in PRs, this will fail - skip them gracefully. if ! gh workflow view "$wf" --ref "$GITHUB_SHA" --yaml >/dev/null 2>&1 then - echo "ERROR: Failed to render $wf via gh" >&2 - failed=1 + # Check if workflow exists on default branch + if gh workflow view "$wf" --yaml >/dev/null 2>&1; then + echo "ERROR: Failed to render $wf via gh (exists on default branch but fails to render)" >&2 + failed=1 + else + echo "SKIP: $wf is new (not yet on default branch), will validate after merge" + fi fi done @@ -80,7 +86,7 @@ jobs: echo "One or more workflows failed server-side validation via gh." >&2 exit 1 fi - echo "All workflows render via gh." + echo "All workflows validated (new workflows skipped)." setup-whisper-dependencies: name: Setup Whisper Dependencies @@ -97,7 +103,7 @@ jobs: # Security scanning for vulnerabilities and license compliance security_audit: name: Security Audit - runs-on: [self-hosted, Linux, X64, fedora, nobara] + runs-on: ubuntu-latest steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.0 @@ -118,9 +124,9 @@ jobs: run: cargo deny check # Build, check, and test with multiple Rust versions - build_and_check: - name: Build & Test - runs-on: [self-hosted, Linux, X64, fedora, nobara] + unit_tests_hosted: + name: Unit Tests & Golden Master (Hosted) + runs-on: ubuntu-latest needs: [setup-whisper-dependencies] strategy: matrix: @@ -128,6 +134,11 @@ jobs: steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.0 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y xdotool wget unzip gcc g++ make xvfb openbox dbus-x11 wl-clipboard xclip ydotool x11-utils wmctrl pkg-config pulseaudio libasound2-dev libgtk-3-dev libatspi2.0-dev libxtst-dev python3-pip python3-venv + - name: Set up Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: @@ -135,11 +146,6 @@ jobs: components: rustfmt, clippy override: true - - name: Setup ColdVox - uses: ./.github/actions/setup-coldvox - with: - skip-toolchain: "true" - # Only run formatting and linting on stable - name: Check formatting (advisory) if: matrix.rust-version == 'stable' @@ -181,17 +187,24 @@ jobs: echo "=== Running Tests ===" cargo test --workspace --locked -- + - name: Run Golden Master pipeline test + if: matrix.rust-version == 'stable' + env: + WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} + WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} + run: | + echo "=== Running Golden Master Test ===" + # Install Python dependencies for Golden Master + pip install faster-whisper + export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])") + cargo test -p coldvox-app --test golden_master -- --nocapture + # GUI groundwork check integrated here - name: Detect and test Qt 6 GUI if: matrix.rust-version == 'stable' run: | - if bash scripts/ci/detect-qt6.sh - then - echo "✅ Qt 6 detected - building GUI" - cargo check -p coldvox-gui --features qt-ui --locked - else - echo "⚠️ Qt 6 not detected - skipping GUI build" - fi + # Qt6 might not be easily available on ubuntu-latest without extra actions, skipping for now or adding if needed + echo "Skipping Qt6 check on hosted runner" - name: Upload test artifacts on failure if: failure() @@ -204,7 +217,7 @@ jobs: retention-days: 7 text_injection_tests: - name: Text Injection Tests + name: Hardware Integration Tests (Self-Hosted) runs-on: [self-hosted, Linux, X64, fedora, nobara] needs: [setup-whisper-dependencies] timeout-minutes: 30 @@ -213,11 +226,38 @@ jobs: RUST_LOG: debug RUST_TEST_TIME_UNIT: 10000 RUST_TEST_TIME_INTEGRATION: 30000 + SCCACHE_DIR: $HOME/.cache/sccache steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.0 - uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 with: toolchain: stable + + # Setup sccache via justfile (installs if missing, enables RUSTC_WRAPPER) + - name: Setup sccache + run: | + # Install just if not available + if ! command -v just >/dev/null 2>&1; then + cargo install just --locked + fi + + # Run setup-sccache recipe (idempotent - installs if missing) + just setup-sccache + + # Enable sccache wrapper after installation + SCCACHE_BIN="" + if command -v sccache >/dev/null 2>&1; then + SCCACHE_BIN="$(command -v sccache)" + elif [[ -x "$HOME/.cargo/bin/sccache" ]]; then + SCCACHE_BIN="$HOME/.cargo/bin/sccache" + fi + + if [[ -n "$SCCACHE_BIN" ]]; then + "$SCCACHE_BIN" --start-server || true + echo "RUSTC_WRAPPER=$SCCACHE_BIN" >> "$GITHUB_ENV" + echo "sccache enabled: $SCCACHE_BIN" + fi + - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: Setup ColdVox @@ -262,9 +302,12 @@ jobs: # Set per-test timeout to prevent hanging export RUST_TEST_TIME_UNIT="10000" # 10 second timeout per test export RUST_TEST_TIME_INTEGRATION="30000" # 30 second for integration tests + # Note: atspi feature not enabled - AT-SPI Collection.GetMatches requires + # a full desktop session, not available in headless Xvfb environment. + # AT-SPI tests will skip gracefully. cargo test -p coldvox-text-injection \ --features real-injection-tests \ - -- --nocapture --test-threads=1 --timeout 600 + -- --nocapture --test-threads=1 ' - name: Build pipeline (default features) @@ -285,18 +328,14 @@ jobs: - name: Build main application run: cargo build --locked -p coldvox-app - - name: Run Golden Master pipeline test + - name: Run Hardware Capability Checks env: - WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} + COLDVOX_E2E_REAL_INJECTION: "1" + COLDVOX_E2E_REAL_AUDIO: "1" run: | - echo "=== Golden Master Environment Validation ===" - echo "WHISPER_MODEL_PATH: $WHISPER_MODEL_PATH" - echo "WHISPER_MODEL_SIZE: $WHISPER_MODEL_SIZE" - echo "=== Running Golden Master Test ===" - pip install faster-whisper - export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])") - cargo test -p coldvox-app --test golden_master -- --nocapture + echo "=== Running Hardware Capability Checks ===" + # We run the new hardware_check test file, enabling the ignored tests + cargo test -p coldvox-app --test hardware_check -- --nocapture --include-ignored - name: Upload test artifacts on failure if: failure() @@ -361,12 +400,12 @@ jobs: ci_success: name: CI Success Summary - runs-on: [self-hosted, Linux, X64, fedora, nobara] + runs-on: ubuntu-latest needs: - validate-workflows - setup-whisper-dependencies - security_audit - - build_and_check + - unit_tests_hosted - text_injection_tests - moonshine_check if: always() @@ -378,12 +417,12 @@ jobs: echo "- validate-workflows: ${{ needs.validate-workflows.result }}" >> report.md echo "- setup-whisper-dependencies: ${{ needs.setup-whisper-dependencies.result }}" >> report.md echo "- security_audit: ${{ needs.security_audit.result }}" >> report.md - echo "- build_and_check: ${{ needs.build_and_check.result }}" >> report.md + echo "- unit_tests_hosted: ${{ needs.unit_tests_hosted.result }}" >> report.md echo "- text_injection_tests: ${{ needs.text_injection_tests.result }}" >> report.md echo "- moonshine_check: ${{ needs.moonshine_check.result }} (optional)" >> report.md if [[ "${{ needs.setup-whisper-dependencies.result }}" != "success" ]]; then echo "::error::Setup Whisper dependencies failed."; exit 1; fi if [[ "${{ needs.security_audit.result }}" != "success" ]]; then echo "::warning::Security audit failed - check for vulnerabilities."; fi - if [[ "${{ needs.build_and_check.result }}" != "success" ]]; then echo "::error::Build and check failed."; exit 1; fi + if [[ "${{ needs.unit_tests_hosted.result }}" != "success" ]]; then echo "::error::Build and check failed."; exit 1; fi if [[ "${{ needs.text_injection_tests.result }}" != "success" ]]; then echo "::error::Text injection tests failed."; exit 1; fi if [[ "${{ needs.moonshine_check.result }}" != "success" ]]; then echo "::warning::Moonshine check failed (optional)."; fi echo "All critical stages passed successfully." diff --git a/.github/workflows/vosk-integration.yml b/.github/workflows/vosk-integration.yml deleted file mode 100644 index 6bd43e2e..00000000 --- a/.github/workflows/vosk-integration.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: (disabled) Whisper Integration Tests - -on: - pull_request: - paths: - - "crates/coldvox-stt/**" - - "crates/app/**" - - "examples/whisper_*.rs" - - ".github/workflows/vosk-integration.yml" - schedule: - - cron: "0 0 * * 0" - workflow_dispatch: - -permissions: - contents: read - pull-requests: read - -concurrency: - group: whisper-${{ github.ref }} - cancel-in-progress: false - -jobs: - _disabled_notice: - runs-on: ubuntu-latest - steps: - - run: echo "Whisper integration workflow disabled due to backend pivot; see PR for details." - # The following jobs are intentionally disabled pending the new pure-Rust backend - # to avoid Python/venv and model-cache dependencies in CI. - # Original content retained below for history; guarded by always-false condition. - setup-whisper-dependencies: - if: false - name: Setup Whisper Dependencies - runs-on: [self-hosted, Linux, X64, fedora, nobara] - env: - WHISPER_MODEL_SIZE: base # Use base model for integration tests - outputs: - model_path: ${{ steps.setup.outputs.model_path }} - model_size: ${{ steps.setup.outputs.model_size }} - steps: - - uses: actions/checkout@v6 - - name: Setup Whisper Model - id: setup - run: bash scripts/ci/setup-whisper-cache.sh - - whisper-tests: - if: false - name: Whisper STT Integration - needs: [setup-whisper-dependencies] - runs-on: [self-hosted, Linux, X64, fedora, nobara] - timeout-minutes: 30 - env: - RUST_TEST_TIME_UNIT: 10000 - RUST_TEST_TIME_INTEGRATION: 30000 - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Setup ColdVox - uses: ./.github/actions/setup-coldvox - - name: Install cargo-nextest - run: cargo install cargo-nextest --locked - - name: Build with Whisper - env: - WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} - run: | - cargo build --locked -p coldvox-stt --features whisper - cargo build --locked -p coldvox-app --features whisper - - name: Run Whisper tests - env: - WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} - RUST_TEST_THREADS: 1 - run: | - cargo nextest run --locked -p coldvox-stt --features whisper - - name: Run Golden Master pipeline test - env: - WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} - run: | - pip install faster-whisper - export PYTHONPATH=$(python3 -c "import site; print(site.getsitepackages()[0])") - cargo test -p coldvox-app --test golden_master -- --nocapture - - name: Test Whisper examples - env: - WHISPER_MODEL_PATH: ${{ needs.setup-whisper-dependencies.outputs.model_path }} - WHISPER_MODEL_SIZE: ${{ needs.setup-whisper-dependencies.outputs.model_size }} - run: | - if ls examples/whisper_*.rs 1> /dev/null 2>&1; then - for example in examples/whisper_*.rs; do - name=$(basename $example .rs) - cargo run --locked --example $name --features whisper,examples || true - done - fi - - name: Upload artifacts on failure - if: failure() - uses: actions/upload-artifact@v5 - with: - name: whisper-test-artifacts - path: | - target/debug/**/*.log - logs/ - transcripts/ - retention-days: 7 diff --git a/.gitignore b/.gitignore index cb6870b4..bc4fbdab 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .idea/ *.swp *.swo +*.kate-swp *~ .DS_Store @@ -24,6 +25,10 @@ flamegraph.svg .env.local .venv/ +# AI Agent Frameworks (BMAD, Claude, etc) +.bmad/ +.claude/ + # Logs logs/ *.log @@ -77,4 +82,3 @@ docs/config.md docs/install.md docs/reference.md docs/usage.md -.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ef04628e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + + - repo: https://github.com/rhysd/actionlint + rev: v1.6.26 + hooks: + - id: actionlint + + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --all -- --check + language: system + types: [rust] + pass_filenames: false + + - id: cargo-clippy + name: cargo clippy + entry: cargo clippy --all-targets --locked + language: system + types: [rust] + pass_filenames: false + + - id: uv-lock-check + name: uv lock check + entry: uv lock --check + language: system + files: ^pyproject\.toml$|^uv\.lock$ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index 98b83ffc..9c518e27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,8 @@ > Canonical AI agent instructions for ColdVox. All tools (Claude Code, Copilot, Cursor, Kilo Code, etc.) should read this file. +> **⚠️ CRITICAL**: Some documented features are broken or removed. See [`criticalActionPlan.md`](criticalActionPlan.md) before following STT instructions. + ## Project Overview ColdVox is a Rust-based voice AI pipeline: audio capture → VAD → STT → text injection. Multi-crate Cargo workspace under `crates/`. @@ -37,8 +39,7 @@ Always prefer file/crate-scoped commands over full workspace commands for faster cargo check -p coldvox-stt # Clippy single crate -cargo clippy -p coldvox-audio -- -D warnings - +cargo clippy -p coldvox-audio # Test single crate cargo test -p coldvox-text-injection diff --git a/CLAUDE.md b/CLAUDE.md index 03e37a82..0ffc5dcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with this repository. +> **⚠️ CRITICAL**: STT documentation below is outdated. Whisper is removed, Parakeet broken. See [`criticalActionPlan.md`](criticalActionPlan.md). **Only Moonshine works.** + **@import AGENTS.md** - Read `AGENTS.md` for canonical project instructions (structure, commands, do/don't rules, worktrees). ## Claude-Specific Guidelines diff --git a/Cargo.lock b/Cargo.lock index 9ed83b10..c03e9356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -86,22 +86,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -365,9 +365,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bit-set" @@ -467,9 +467,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cassowary" @@ -488,9 +488,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.47" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -600,9 +600,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7a06c0b31fff5ff2e1e7d37dbf940864e2a974b336e1a2938d10af6e8fb283" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", @@ -753,6 +753,7 @@ dependencies = [ "tokio-test", "toml", "tracing", + "tracing-appender", "tracing-subscriber", "unicode-segmentation", "wl-clipboard-rs", @@ -879,9 +880,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] @@ -1079,9 +1080,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1121,9 +1122,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.187" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8465678d499296e2cbf9d3acf14307458fd69b471a31b65b3c519efe8b5e187" +checksum = "a7620f6cfc4dcca21f2b085b7a890e16c60fd66f560cd69ee60594908dc72ab1" dependencies = [ "cc", "cxx-build", @@ -1136,12 +1137,12 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.187" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e" +checksum = "7a9bc1a22964ff6a355fbec24cf68266a0ed28f8b84c0864c386474ea3d0e479" dependencies = [ "cc", - "codespan-reporting 0.13.0", + "codespan-reporting 0.13.1", "indexmap", "proc-macro2", "quote", @@ -1151,11 +1152,11 @@ dependencies = [ [[package]] name = "cxx-gen" -version = "0.7.187" +version = "0.7.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba09d9f00a6fbb25cab5522687f5d659561a770301cd90aeb3e0425a4192dcb6" +checksum = "0e660ae76bd5fcfe7baa8510b93bdb684be66a5f84b72fd91eb7f90ad24a7c3b" dependencies = [ - "codespan-reporting 0.13.0", + "codespan-reporting 0.13.1", "indexmap", "proc-macro2", "quote", @@ -1233,12 +1234,12 @@ dependencies = [ [[package]] name = "cxxbridge-cmd" -version = "1.0.187" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a" +checksum = "b1f29a879d35f7906e3c9b77d7a1005a6a0787d330c09dfe4ffb5f617728cb44" dependencies = [ "clap", - "codespan-reporting 0.13.0", + "codespan-reporting 0.13.1", "indexmap", "proc-macro2", "quote", @@ -1247,15 +1248,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.187" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29b52102aa395386d77d322b3a0522f2035e716171c2c60aa87cc5e9466e523" +checksum = "d67109015f93f683e364085aa6489a5b2118b4a40058482101d699936a7836d6" [[package]] name = "cxxbridge-macro" -version = "1.0.187" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd" +checksum = "d187e019e7b05a1f3e69a8396b70800ee867aa9fc2ab972761173ccee03742df" dependencies = [ "indexmap", "proc-macro2", @@ -1429,9 +1430,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1469,22 +1470,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ - "convert_case 0.7.1", + "convert_case 0.10.0", "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -1584,9 +1586,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enigo" @@ -1661,9 +1663,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -1781,15 +1783,15 @@ checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1965,9 +1967,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2036,9 +2038,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" @@ -2075,12 +2077,11 @@ checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2122,9 +2123,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2142,12 +2143,12 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -2165,15 +2166,18 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "instability" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ "darling", "indoc", @@ -2184,9 +2188,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2223,22 +2227,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -2279,9 +2283,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2306,9 +2310,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libredox" @@ -2359,9 +2363,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -2424,9 +2428,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -2458,14 +2462,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2518,9 +2522,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" dependencies = [ "num-traits", "pxfm", @@ -2818,9 +2822,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onig" @@ -2846,9 +2850,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags 2.10.0", "cfg-if", @@ -2878,9 +2882,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3023,9 +3027,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", "ucd-trie", @@ -3033,9 +3037,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" dependencies = [ "pest", "pest_generator", @@ -3043,9 +3047,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" dependencies = [ "pest", "pest_meta", @@ -3056,9 +3060,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.3" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" dependencies = [ "pest", "sha2", @@ -3066,11 +3070,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", + "hashbrown 0.15.5", "indexmap", ] @@ -3226,9 +3231,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3254,20 +3259,19 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] [[package]] name = "pyo3" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" dependencies = [ - "cfg-if", "indoc", "libc", "memoffset", @@ -3281,19 +3285,18 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" dependencies = [ "libc", "pyo3-build-config", @@ -3301,9 +3304,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -3313,9 +3316,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" dependencies = [ "heck", "proc-macro2", @@ -3368,9 +3371,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3607,6 +3610,15 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustfft" version = "6.4.1" @@ -3647,20 +3659,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "zeroize", ] @@ -3757,6 +3760,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -3895,9 +3904,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -3906,18 +3915,18 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" @@ -4032,9 +4041,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.107" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -4299,9 +4308,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap", "toml_datetime", @@ -4326,9 +4335,9 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4349,9 +4358,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -4360,9 +4369,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -4381,9 +4390,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -4409,13 +4418,12 @@ dependencies = [ [[package]] name = "tree_magic_mini" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ "memchr", - "nom 7.1.3", - "once_cell", + "nom 8.0.0", "petgraph", ] @@ -4476,9 +4484,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization-alignments" @@ -4532,16 +4540,15 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" [[package]] name = "ureq" -version = "3.1.2" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" dependencies = [ "base64 0.22.1", "der", "log", "native-tls", "percent-encoding", - "rustls-pemfile", "rustls-pki-types", "socks", "ureq-proto", @@ -4551,9 +4558,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" dependencies = [ "base64 0.22.1", "http", @@ -4575,13 +4582,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -4664,9 +4671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -4675,25 +4682,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -4704,9 +4697,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4714,22 +4707,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -4806,9 +4799,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -4826,18 +4819,18 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" dependencies = [ "rustls-pki-types", ] [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "winapi" @@ -5271,9 +5264,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5394,9 +5387,9 @@ dependencies = [ [[package]] name = "zbus-lockstep" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e96e38ded30eeab90b6ba88cb888d70aef4e7489b6cd212c5e5b5ec38045b6" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" dependencies = [ "zbus_xml", "zvariant", @@ -5404,9 +5397,9 @@ dependencies = [ [[package]] name = "zbus-lockstep-macros" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6821851fa840b708b4cbbaf6241868cabc85a2dc22f426361b0292bfc0b836" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" dependencies = [ "proc-macro2", "quote", @@ -5458,18 +5451,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index ed1728ea..ffc65db9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ColdVox > ⚠️ **Internal Alpha** - This project is in early development and not ready for production use. + +> **⚠️ CRITICAL**: Documentation is out of sync with code. Whisper STT has been removed; Parakeet doesn't compile. See [`criticalActionPlan.md`](criticalActionPlan.md) for current status. **Only Moonshine STT works** (requires `uv sync` first). ## Development - Install Rust (stable) and required system dependencies for your platform. - Use the provided scripts in `scripts/` to help with local environment setup. diff --git a/agentsDocResearch.md b/agentsDocResearch.md new file mode 100644 index 00000000..117cb895 --- /dev/null +++ b/agentsDocResearch.md @@ -0,0 +1,371 @@ +# Research: Evolving AGENTS.md / CLAUDE.md for Modern LLM Agents + +## Problem Statement + +Documentation written for LLM agents a year ago assumed less capability. Agents needed explicit instructions for things they now know intrinsically. This creates noise that obscures actually useful project-specific information. + +## What Used to Be Useful (2023-2024) But Isn't Now + +### Standard Tooling +- "Run `cargo test` to run tests" +- "Use `cargo clippy` for linting" +- "Run `cargo fmt` to format code" +- "`-p ` targets a specific package" + +**Why obsolete:** Modern agents know standard Rust/Python/JS tooling. They've seen millions of repos. + +### File Structure Narration +- "Source code lives in `src/`" +- "`main.rs` is the entry point" +- "Tests are in `tests/` or inline with `#[cfg(test)]`" + +**Why obsolete:** Agents understand standard project layouts. They can `ls` and read `Cargo.toml`. + +### Language Basics +- "Rust uses Cargo.toml for dependencies" +- "Python uses requirements.txt or pyproject.toml" +- "This is a workspace with multiple crates" + +**Why obsolete:** Agents know language ecosystems. Workspace structure is in `Cargo.toml`. + +### Generic Workflow Instructions +- "Create a branch before making changes" +- "Run tests before committing" +- "Use meaningful commit messages" + +**Why obsolete:** These are universal practices, not project-specific knowledge. + +## Examples From This Repo (ColdVox) + +### Likely Obsolete + +```markdown +# From AGENTS.md - probably unnecessary: + +# Type check single crate +cargo check -p coldvox-stt + +# Clippy single crate +cargo clippy -p coldvox-audio + +# Test single crate +cargo test -p coldvox-text-injection + +# Format check (always full, it's fast) +cargo fmt --all -- --check +``` + +**Why:** Any agent working in Rust knows these commands. Crate names are discoverable. + +### Probably Still Useful + +```markdown +# Use UV for Python, never pip +uv sync +uv run + +# Self-hosted runner labels +runs-on: [self-hosted, Linux, X64, fedora, nobara] +``` + +**Why:** These are project-specific decisions that contradict common patterns or aren't discoverable. + +## What SHOULD Be In Agent Documentation + +### 1. Deviations From Standard Patterns +- "We use X instead of the typical Y" +- "This looks wrong but is intentional because Z" + +### 2. Hidden Requirements +- Environment variables needed +- External services required +- Platform-specific setup + +### 3. Non-Obvious Architecture Decisions +- Why certain patterns were chosen +- Gotchas that would trip up someone unfamiliar + +### 4. Project-Specific Workflows +- CI/CD quirks +- Release processes +- Feature flag conventions + +### 5. Things That Would Waste Time Discovering +- "Don't bother with X, it's deprecated" +- "Y is the source of truth, not Z" + +## Research Questions + +1. **What in our current AGENTS.md/CLAUDE.md is actually useful?** +2. **What's noise that modern agents don't need?** +3. **What non-obvious things about this repo would help an agent that aren't documented?** +4. **What's the minimal effective documentation for a 2025 agent?** + +--- + +## Agent Research Findings + +*The sections below were added by agents exploring the repository:* + +### Agent 1: Non-Obvious Patterns and Gotchas + +#### Feature Flag Architecture +- **`whisper` feature is a placeholder** — defined but stubbed out. Actual STT: `parakeet` (GPU-only) or `moonshine` (CPU via PyO3). Agents enabling `whisper` get nothing. +- **`no-stt` feature exists but doesn't gate anything** — misleading if an agent tries to build without STT. +- **Default features include `silero` VAD but no STT backend** — transcription fails silently if agent doesn't explicitly enable `parakeet` or `moonshine`. + +#### Build-Time Display Detection +- **`crates/app/build.rs` reads env vars at compile time** — sets `kde_globalaccel`, `wayland_session`, `x11_session` cfg flags. +- **Gotcha:** Rebuilding after switching X11↔Wayland requires clean rebuild. Stale artifacts have wrong session flags. +- **Intentional:** Missing display vars = no session flags set. Valid for headless CI, but text injection won't work. + +#### Text Injection Backend Detection +- **Runtime detection, not just compile-time** — `BackendDetector::detect_available_backends()` checks for actual binaries. +- **Gotcha:** `kdotool` feature compiled doesn't mean it works — manager checks for `ydotool` binary at runtime. +- **`combo_clip_ydotool` is internal strategy** — clipboard paste falls back to ydotool, not exposed as standalone. + +#### Audio Capture Initialization +- **`AudioCaptureThread::spawn()` initializes `running = true`** — not `false`. Comment explains: "Start in running state so device monitor thread stays alive." +- **Gotcha:** Changing to `false` breaks device monitoring without obvious errors. + +#### State Machine Validation +- **`StateManager` enforces transitions:** `Initializing → Running → {Recovering, Stopping} → Stopped`. +- **Invalid transitions panic** — can't go `Running → Stopped` directly. + +#### PyO3/Moonshine +- **GIL required for all `Py` access** — safety comments mark this. Bypassing causes data races. +- **Parakeet is pure Rust** — no Python, no GIL issues. Different mental model. + +#### Threading Patterns +- **Mixed async/sync:** `tokio::spawn()` for async tasks, `thread::spawn()` for audio capture/device monitoring. +- **Dual channel patterns:** `broadcast` for fanout (VAD events to multiple listeners), `mpsc` for single-receiver (hotkey handling). + +#### Watchdog Timer +- **Uses custom clock abstraction** — `WatchdogTimer::new_with_clock()` accepts `SharedClock`. Tests inject mocks. +- **Standard duration methods won't work with injected clocks.** + +#### Device Monitor Tuning +- **2-second polling interval is intentional** — shorter intervals cause spurious hotplug events from CPAL enumeration glitches. + +#### Ring Buffer Overflow +- **Non-blocking write returns `AudioError::BufferOverflow`** — doesn't block or drop samples. Errors propagate up. + +#### Test Isolation +- **`#[serial]` required for env detection tests** — tests manipulate `std::env` which affects other tests. + +--- + +### Agent 2: Documentation Gaps and Actual Useful Info + +#### Test Categories (Four, Not Two) +1. **Unit/Integration** — run everywhere (`cargo test --workspace`) +2. **Golden Master** — requires Whisper model (`cargo test -p coldvox-app --test golden_master`) +3. **Hardware capability** — marked `#[ignore]`, opt-in via `COLDVOX_E2E_REAL_AUDIO=1`, `COLDVOX_E2E_REAL_INJECTION=1` +4. **Real injection** — backend-specific (`cargo test -p coldvox-text-injection --features real-injection-tests`) + +#### Critical Undocumented Environment Variables +``` +# Test control +COLDVOX_TEST_LOG_LEVEL # default: debug +COLDVOX_TEST_TIMEOUT_SEC # per-test timeout +COLDVOX_E2E_REAL_AUDIO=1 # opt-in hardware tests +COLDVOX_E2E_REAL_INJECTION=1 # opt-in injection tests +COLDVOX_RUN_AUDIO_IT=1 # audio integration tests + +# CI/Runtime +WHISPER_MODEL_PATH # CI sets from cache script +WHISPER_MODEL_SIZE # tiny/base/small/medium/large +DBUS_SESSION_BUS_ADDRESS # required for text injection tests +RUST_TEST_TIME_UNIT=10000 # milliseconds, not seconds! +``` + +#### PyO3 + Python Version Pinning +- **Python 3.12 required** — `.python-version` pins it. Python 3.13 breaks PyO3 0.27 (free-threading incompatibility). +- **`.cargo/config.toml` pins to `./.venv/bin/python`** — PyO3 always uses repo venv, not system Python. +- **Must run `uv sync` before `cargo build --features moonshine`** — installs transformers, torch, librosa. + +#### External Dependencies (Not in Cargo) +- **ydotool daemon** — user systemd service required. `scripts/setup_text_injection.sh` generates it. +- **faster-whisper** — Python package, not Rust. Install via `pip` or `uv sync`. +- **AT-SPI2 libraries** — `libatspi2.0-dev` for text injection on Linux. + +#### Config File Hierarchy +- **`config/plugins.json` is source of truth** — legacy `./plugins.json` ignored with warning. +- **`COLDVOX_CONFIG_PATH`** overrides discovery. `COLDVOX_SKIP_CONFIG_DISCOVERY` disables it. + +#### CI-Specific Behaviors +- **Whisper models cached at runner-specific path** — `/home/coldaine/ActionRunnerCache/whisper/`, symlinked by setup script. +- **sccache enabled only in text injection job** — different caching strategies per job. +- **Headless setup is non-trivial** — `scripts/start-headless.sh` runs Xvfb, dbus-launch, verifies clipboard tools. Tests fail without it. +- **Cleanup runs on failure** — `if: always()` kills Xvfb, fluxbox, dbus-daemon. Without it, subsequent runs hang. + +#### Golden Master Approval Workflow +- **First-time tests FAIL by design** — output says `cp *.received.json *.approved.json`. Not a bug. +- **Artifacts in `crates/app/tests/golden_master_artifacts/`** + +#### Logging Specifics +- **Test logging goes to files** — `target/test-logs/.log`, not console. TUI tests corrupt terminal. +- **Default log level is INFO** (changed from DEBUG). Use `RUST_LOG=debug` for audio processing details. +- **Audio frame dispatch is TRACE** — ~60 frames/sec, extremely noisy. + +#### Common Agent Mistakes +1. Assuming `cargo test` works headless — text injection needs DISPLAY +2. Building Moonshine without `uv sync` — PyO3 fails mysteriously +3. Skipping `--include-ignored` tests — misses hardware issues +4. Golden Master failures = regression — no, it's approval workflow +5. Setting timeouts in seconds — `RUST_TEST_TIME_UNIT` is milliseconds +6. Not clearing CI env vars in tests — `CI=true` affects behavior + +--- + +## Appendix: Agent Prompts Used + +### Agent 1 Prompt (Non-Obvious Patterns) + +``` +You are researching what non-obvious information would be useful for an AI agent working on this codebase. + +Explore the repository looking for: +1. Non-standard patterns or configurations +2. Hidden gotchas that aren't documented +3. Things that look wrong but are intentional +4. Environment variables or setup requirements that aren't obvious +5. Feature flags and when they matter +6. Integration points between crates that aren't self-evident +7. Platform-specific code or requirements +8. Deprecated code paths that still exist + +Focus on things that would WASTE TIME if an agent had to discover them through trial and error. + +DO NOT list obvious things like "it's a Rust workspace" or "use cargo to build." + +After your research, append your findings to `/home/coldaine/_projects/ColdVox/agentsDocResearch.md` under a new section called "### Agent 1: Non-Obvious Patterns and Gotchas" + +Format as bullet points, each with a brief explanation of WHY this is non-obvious and worth documenting. +``` + +### Agent 2 Prompt (Documentation Gaps) + +``` +You are auditing this repository to find information gaps that would help AI agents. + +Explore the repository looking for: +1. Build-time detection or conditional compilation that affects behavior +2. Test categories or test requirements (env vars, hardware, etc.) +3. CI/CD specific behaviors or workarounds +4. Python/Rust interop points (PyO3, feature flags) +5. External dependencies that must be installed outside cargo +6. Configuration files that affect runtime behavior +7. Logging/debugging patterns specific to this project +8. Things in CLAUDE.md or AGENTS.md that are ACTUALLY useful vs noise + +Focus on things a new agent would struggle with or get wrong on first attempt. + +DO NOT include standard Rust knowledge or commands any agent would know. + +After your research, append your findings to `/home/coldaine/_projects/ColdVox/agentsDocResearch.md` under a new section called "### Agent 2: Documentation Gaps and Actual Useful Info" + +Format as bullet points, explaining what's non-obvious and why it matters. +``` + +--- + +## Agent 3: Genuine Confusion Points (Discovered Cold) + +*The sections above were generated by agents explicitly told what to look for. This section documents what an agent ACTUALLY gets wrong when exploring the codebase without guidance.* + +### Critical: Documentation Actively Misleads + +#### 1. `whisper` Feature Is A Lie + +**AGENTS.md says:** `cargo run --features whisper,text-injection # With STT` + +**Reality:** +- `whisper = []` in Cargo.toml — it's an empty stub +- The `whisper_plugin.rs` that had `faster-whisper-rs` is commented out: `// pub mod whisper_plugin;` +- Enabling this feature gives you `whisper_cpp.rs` which is a non-functional stub +- **An agent following AGENTS.md instructions gets zero STT capability** + +#### 2. `parakeet` Feature Doesn't Compile + +**AGENTS.md says:** `Use feature flags: whisper, parakeet, text-injection, silero` + +**Reality:** +``` +cargo build -p coldvox-app --features parakeet +# FAILS with 6 compile errors +# API mismatch with parakeet-rs crate +``` +- The parakeet plugin code is out of sync with the `parakeet-rs = "0.2"` dependency +- `transcribe_samples()` signature wrong, `confidence` field doesn't exist +- **Agent cannot enable GPU STT despite documentation suggesting it works** + +#### 3. Python Version Contradiction + +**mise.toml says:** `python = "3.13"` +**.python-version says:** `3.12` +**Previous agent research says:** "Python 3.13 breaks PyO3 0.27" + +**Reality:** +- System Python: 3.14 +- Venv Python: 3.12 +- Which one you get depends on whether you use mise, direnv, or neither +- **Only 3.12 actually works for moonshine builds** + +#### 4. requirements.txt vs pyproject.toml + +**requirements.txt:** Empty (says "No external dependencies currently required") +**pyproject.toml:** Has `transformers>=4.35.0`, `torch>=2.0.0`, `librosa>=0.10.0` + +**Reality:** +- `requirements.txt` is vestigial — tells you to install nothing +- `pyproject.toml` has actual deps needed for moonshine +- An agent following `uv pip install -r requirements.txt` installs nothing useful + +### What Actually Works (Verified) + +1. **Default build works:** `cargo build -p coldvox-app` ✓ +2. **Moonshine builds:** `cargo build -p coldvox-stt --features moonshine` ✓ (after `uv sync`) +3. **Default tests pass:** `cargo test -p coldvox-app` ✓ +4. **Text injection compiles:** Default features include it + +### What Doesn't Work (Despite Docs) + +1. **Whisper feature:** Stub — no functionality +2. **Parakeet feature:** Compile errors +3. **Coqui/Leopard/Silero-STT features:** Empty stubs (`coqui = []`, etc.) +4. **Golden master tests on hosted CI:** Require `pip install faster-whisper` but the Rust code doesn't use it + +### Stub Features That Waste Agent Time + +These features are defined but do nothing: +- `whisper = []` — placeholder +- `coqui = []` — placeholder +- `leopard = []` — placeholder +- `silero-stt = []` — placeholder +- `no-stt = []` — defined but doesn't gate anything + +### Genuine Gotchas Not In Prior Research + +1. **CI installs `faster-whisper` but code doesn't use it** — The golden master job does `pip install faster-whisper` but the Rust whisper backend is commented out. This is CI cruft that confuses understanding. + +2. **venv is required before moonshine build** — `.cargo/config.toml` sets `PYO3_PYTHON = ./.venv/bin/python`. If venv doesn't exist, PyO3 build fails with cryptic errors. + +3. **justfile has dead whisper commands** — Line 38 says `## Whisper-specific helpers removed pending new backend` but AGENTS.md still references whisper as if it works. + +4. **Build detection happens at compile time** — `build.rs` reads `WAYLAND_DISPLAY`/`DISPLAY` at compile time. Binaries built in CI without these vars have different cfg flags than local builds. + +### Recommended Documentation Changes + +**Remove from AGENTS.md:** +- `cargo run --features whisper,text-injection # With STT` — whisper doesn't work +- References to parakeet as a working feature + +**Add to AGENTS.md:** +- "Only `moonshine` STT backend currently works. Requires `uv sync` first." +- "Default build has NO STT — only VAD. This is intentional for the audio pipeline tests." + +**Fix:** +- Either make parakeet compile or remove it from feature list +- Either implement whisper or remove documentation claims + diff --git a/crates/app/config/plugins.json b/crates/app/config/plugins.json index 829ef1bf..e313217d 100644 --- a/crates/app/config/plugins.json +++ b/crates/app/config/plugins.json @@ -1,4 +1,4 @@ -{ +{ "preferred_plugin": "mock", "fallback_plugins": [ "whisper", @@ -21,4 +21,4 @@ "debug_dump_events": false }, "auto_extract_model": true -} +} \ No newline at end of file diff --git a/crates/app/tests/common/logging.rs b/crates/app/tests/common/logging.rs new file mode 100644 index 00000000..a6e36617 --- /dev/null +++ b/crates/app/tests/common/logging.rs @@ -0,0 +1,170 @@ +//! Shared test logging infrastructure for ColdVox tests. +//! +//! This module provides standardized file-based logging for all tests, +//! ensuring we always have a persistent log trail for debugging failures. +//! +//! ## Usage +//! +//! ```rust,no_run +//! use crate::common::logging::init_test_logging; +//! +//! #[tokio::test] +//! async fn my_test() { +//! let _guard = init_test_logging("my_test"); +//! // ... test code ... +//! } +//! ``` +//! +//! Logs are written to `target/test-logs/.log` with debug level by default. +//! Set `COLDVOX_TEST_LOG_LEVEL` to override (e.g., `trace`, `info`). + +#![allow(dead_code)] // Utility functions may not be used in all test binaries + +use std::path::PathBuf; +use std::sync::Once; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +static INIT: Once = Once::new(); + +/// Directory name for test logs (will be created under workspace root). +const TEST_LOG_DIR_NAME: &str = "test-logs"; + +/// Get the workspace root directory. +/// +/// Uses CARGO_MANIFEST_DIR and walks up to find workspace root. +fn workspace_root() -> PathBuf { + // CARGO_MANIFEST_DIR points to the crate directory during tests + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + // Walk up from crates/app to workspace root + manifest_dir + .parent() // -> crates/ + .and_then(|p| p.parent()) // -> workspace root + .map(|p| p.to_path_buf()) + .unwrap_or(manifest_dir) +} + +/// Get the full path to the test logs directory. +pub fn test_log_dir() -> PathBuf { + workspace_root().join("target").join(TEST_LOG_DIR_NAME) +} + +/// Initialize test logging with file output. +/// +/// Returns a guard that must be held for the duration of the test. +/// Logs are written to `target/test-logs/.log`. +/// +/// The log level defaults to `debug` but can be overridden via: +/// - `COLDVOX_TEST_LOG_LEVEL` environment variable +/// - `RUST_LOG` environment variable (if `COLDVOX_TEST_LOG_LEVEL` is not set) +/// +/// # Arguments +/// * `test_name` - Name of the test, used for the log filename +/// +/// # Returns +/// A `WorkerGuard` that ensures logs are flushed on drop. **You must keep this alive!** +pub fn init_test_logging(test_name: &str) -> WorkerGuard { + // Get workspace-relative log directory + let log_dir = test_log_dir(); + + // Ensure log directory exists + let _ = std::fs::create_dir_all(&log_dir); + + // Determine log level: COLDVOX_TEST_LOG_LEVEL > RUST_LOG > default (debug) + let log_level = std::env::var("COLDVOX_TEST_LOG_LEVEL") + .or_else(|_| std::env::var("RUST_LOG")) + .unwrap_or_else(|_| "debug".to_string()); + + // Build filter with sensible defaults for test debugging + let filter = EnvFilter::try_new(&log_level).unwrap_or_else(|_| { + EnvFilter::new("debug") + .add_directive("coldvox_app=debug".parse().unwrap()) + .add_directive("coldvox_stt=debug".parse().unwrap()) + .add_directive("coldvox_audio=debug".parse().unwrap()) + .add_directive("coldvox_vad=debug".parse().unwrap()) + .add_directive("coldvox_text_injection=debug".parse().unwrap()) + }); + + // Create file appender for this test + let log_filename = format!("{}.log", test_name); + let file_appender = tracing_appender::rolling::never(&log_dir, &log_filename); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + + // Try to initialize the subscriber. If it's already initialized (by another test), + // that's fine - we'll still get our file output. + let _ = tracing_subscriber::registry() + .with(filter) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false) + .with_thread_ids(true) + .with_file(true) + .with_line_number(true), + ) + .with( + tracing_subscriber::fmt::layer() + .with_test_writer() + .with_thread_ids(true), + ) + .try_init(); + + // Log test start marker + tracing::info!("========================================"); + tracing::info!("TEST START: {}", test_name); + tracing::info!("Log file: {}/{}", log_dir.display(), log_filename); + tracing::info!("========================================"); + + guard +} + +/// Initialize test logging once for the entire test suite. +/// +/// This is useful for integration tests that run multiple tests in sequence. +/// Unlike `init_test_logging`, this only initializes once per process. +pub fn init_test_logging_once(suite_name: &str) -> Option { + let mut guard: Option = None; + + INIT.call_once(|| { + guard = Some(init_test_logging(suite_name)); + }); + + guard +} + +/// Get the path to a test's log file. +pub fn test_log_path(test_name: &str) -> PathBuf { + test_log_dir().join(format!("{}.log", test_name)) +} + +/// Append a marker to the current log indicating a test phase. +/// +/// Useful for marking sections in long-running tests. +pub fn log_phase(phase: &str) { + tracing::info!("--- PHASE: {} ---", phase); +} + +/// Log test completion with result summary. +pub fn log_test_end(test_name: &str, success: bool) { + tracing::info!("========================================"); + tracing::info!( + "TEST END: {} - {}", + test_name, + if success { "PASSED" } else { "FAILED" } + ); + tracing::info!("========================================"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_log_path_generation() { + let path = test_log_path("my_test"); + assert!(path.to_string_lossy().contains("test-logs")); + assert!(path.to_string_lossy().ends_with("my_test.log")); + } +} diff --git a/crates/app/tests/common/mod.rs b/crates/app/tests/common/mod.rs index 17bc43d8..100bd3e7 100644 --- a/crates/app/tests/common/mod.rs +++ b/crates/app/tests/common/mod.rs @@ -1,3 +1,4 @@ +pub mod logging; pub mod test_utils; pub mod timeout; pub mod wer; diff --git a/crates/app/tests/common/test_utils.rs b/crates/app/tests/common/test_utils.rs index 4d09f26b..54d739e3 100644 --- a/crates/app/tests/common/test_utils.rs +++ b/crates/app/tests/common/test_utils.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] // Utility functions may not be used in all test binaries + use coldvox_audio::ring_buffer::AudioProducer; /// Write samples into the audio ring buffer producer in fixed-size chunks. diff --git a/crates/app/tests/common/timeout.rs b/crates/app/tests/common/timeout.rs index e072ac93..c038c2aa 100644 --- a/crates/app/tests/common/timeout.rs +++ b/crates/app/tests/common/timeout.rs @@ -4,11 +4,11 @@ use std::time::Duration; use tokio::time::timeout; -/// Default timeout for most test operations (30 seconds) -pub const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(30); +/// Default timeout for most test operations (10 seconds) +pub const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10); -/// Extended timeout for complex operations like STT model loading (60 seconds) -pub const EXTENDED_TEST_TIMEOUT: Duration = Duration::from_secs(60); +/// Extended timeout for complex operations like STT model loading (30 seconds) +pub const EXTENDED_TEST_TIMEOUT: Duration = Duration::from_secs(30); /// Short timeout for quick operations that shouldn't hang (5 seconds) pub const SHORT_TEST_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/crates/app/tests/golden_master.rs b/crates/app/tests/golden_master.rs index ab3551d9..460a4271 100644 --- a/crates/app/tests/golden_master.rs +++ b/crates/app/tests/golden_master.rs @@ -174,10 +174,13 @@ pub mod harness { } } +mod common; + #[cfg(test)] mod tests { use super::harness::assert_golden; use super::test_utils::MockInjectionSink; + use crate::common::logging::init_test_logging; use coldvox_app::audio::wav_file_loader::WavFileLoader; use coldvox_app::runtime::{start, ActivationMode, AppRuntimeOptions}; use coldvox_audio::DeviceConfig; @@ -216,15 +219,8 @@ mod tests { #[tokio::test] async fn test_short_phrase_pipeline() { - // Initialize tracing for better debugging - let _ = tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - .add_directive("coldvox_app=debug".parse().unwrap()) - .add_directive("coldvox_stt=debug".parse().unwrap()), - ) - .with_test_writer() - .try_init(); + // Initialize comprehensive file-based logging + let _guard = init_test_logging("golden_master_short_phrase"); let test_name = "short_phrase"; let wav_path = "test_data/test_11.wav"; @@ -328,46 +324,74 @@ mod tests { // 6. Collect VAD events until SpeechEnd (and injection if whisper enabled) let start_wait = std::time::Instant::now(); - loop { - tokio::select! { - evt = vad_rx.recv() => { - match evt { - Ok(e) => { - let ser = SerializableVadEvent::from(e); - tracing::info!("VAD event captured: {:?}", ser); - vad_events.lock().await.push(ser); - } - Err(e) => { - tracing::warn!("VAD channel closed or error: {}", e); - break; + + // Wrap the entire wait loop in a 30-second timeout to prevent hangs + let vad_collection_result = tokio::time::timeout(Duration::from_secs(30), async { + let mut no_events_warning_printed = false; + loop { + tokio::select! { + evt = vad_rx.recv() => { + match evt { + Ok(e) => { + let ser = SerializableVadEvent::from(e); + tracing::info!("VAD event captured: {:?}", ser); + vad_events.lock().await.push(ser); + no_events_warning_printed = false; // Reset warning flag on new event + } + Err(e) => { + tracing::warn!("VAD channel closed or error: {}", e); + break; + } } } + _ = tokio::time::sleep(Duration::from_millis(250)) => { + // Periodic completion check + } } - _ = tokio::time::sleep(Duration::from_millis(250)) => { - // Periodic completion check + + let events = vad_events.lock().await; + let has_speech_end = events.iter().any(|e| e.kind == "SpeechEnd"); + #[cfg(feature = "whisper")] + let has_injection = !mock_sink.injected_text.lock().unwrap().is_empty(); + #[cfg(not(feature = "whisper"))] + let has_injection = true; // Ignore injection for mock-only runs + drop(events); + + if has_speech_end && has_injection { + tracing::info!("Pipeline completion detected!"); + break; } - } - let events = vad_events.lock().await; - let has_speech_end = events.iter().any(|e| e.kind == "SpeechEnd"); - #[cfg(feature = "whisper")] - let has_injection = !mock_sink.injected_text.lock().unwrap().is_empty(); - #[cfg(not(feature = "whisper"))] - let has_injection = true; // Ignore injection for mock-only runs - drop(events); - - if has_speech_end && has_injection { - tracing::info!("Pipeline completion detected!"); - break; - } - if start_wait.elapsed() > Duration::from_secs(60) { - let vad_lock = vad_events.lock().await; - let injection_lock = mock_sink.injected_text.lock().unwrap(); - panic!( - "Test timed out waiting for completion.\nVAD events: {:?}\nInjections: {:?}", - *vad_lock, *injection_lock - ); + // Early warning if no events after 5 seconds + if start_wait.elapsed() > Duration::from_secs(5) && !no_events_warning_printed { + let vad_lock = vad_events.lock().await; + tracing::warn!( + "No VAD events after 5 seconds. VAD events so far: {:?}", + *vad_lock + ); + no_events_warning_printed = true; + } + + // Fail fast with detailed output after 10 seconds + if start_wait.elapsed() > Duration::from_secs(10) { + let vad_lock = vad_events.lock().await; + let injection_lock = mock_sink.injected_text.lock().unwrap(); + panic!( + "Test timed out waiting for completion after 10 seconds.\nVAD events: {:?}\nInjections: {:?}", + *vad_lock, *injection_lock + ); + } } + }).await; + + // Handle timeout case + if vad_collection_result.is_err() { + let vad_lock = vad_events.lock().await; + let injection_lock = mock_sink.injected_text.lock().unwrap(); + panic!( + "Test hung and timed out after 30 seconds waiting for completion.\nVAD events: {:?}\nInjections: {:?}", + *vad_lock, *injection_lock + ); } // Wait a bit longer to ensure all events are processed @@ -419,10 +443,8 @@ pub mod test_utils { text: &str, _context: Option<&InjectionContext>, ) -> InjectionResult<()> { - if !text.trim().is_empty() { - let mut guard = self.injected_text.lock().unwrap(); - guard.push(text.to_string()); - } + tracing::info!("Mock injection sink received text: {}", text); + self.injected_text.lock().unwrap().push(text.to_string()); Ok(()) } diff --git a/crates/app/tests/hardware_check.rs b/crates/app/tests/hardware_check.rs new file mode 100644 index 00000000..dc1b4abb --- /dev/null +++ b/crates/app/tests/hardware_check.rs @@ -0,0 +1,87 @@ +//! Hardware capability tests for the self-hosted runner. +//! +//! These tests verify that the runner environment has access to the necessary +//! hardware resources (audio input, display server, etc.) to run the full pipeline. +//! They are not "Golden Master" tests because they are non-deterministic. + +mod common; + +#[cfg(test)] +mod hardware_tests { + use crate::common::logging::init_test_logging; + use coldvox_foundation::AudioConfig; + + fn should_skip(env_var: &str) -> bool { + match std::env::var(env_var) { + Ok(v) => matches!( + v.to_ascii_lowercase().as_str(), + "0" | "false" | "off" | "no" + ), + Err(_) => false, // Default to running (opt-out only) + } + } + + /// Verifies that we can open the default audio input device. + #[test] + #[ignore = "Requires real audio hardware"] + fn test_audio_capture_device_open() { + let _guard = init_test_logging("hardware_check_audio"); + + // Skip only if explicitly opted out + if should_skip("COLDVOX_E2E_REAL_AUDIO") { + println!("Skipping audio hardware test: COLDVOX_E2E_REAL_AUDIO set to false/0"); + return; + } + + println!("Attempting to open default audio capture device..."); + + // We just want to see if it panics or errors out immediately. + let config = AudioConfig { + silence_threshold: 100, + capture_buffer_samples: 1024, + }; + + // This is a bit tricky because AudioCapture might not expose a simple "check" method + // without starting the stream. We'll try to instantiate the ring buffer and capture. + let ring_buffer = coldvox_audio::AudioRingBuffer::new(config.capture_buffer_samples); + let (producer, _consumer) = ring_buffer.split(); + let _producer = std::sync::Arc::new(std::sync::Mutex::new(producer)); + + // Use cpal directly to verify hardware access + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let device = host.default_input_device(); + + assert!(device.is_some(), "No default input device found!"); + let device = device.unwrap(); + println!( + "Found default input device: {}", + device.name().unwrap_or_default() + ); + } + + /// Verifies that the text injection subsystem is available. + #[tokio::test] + #[ignore = "Requires display server"] + async fn test_text_injector_availability() { + let _guard = init_test_logging("hardware_check_injection"); + + if should_skip("COLDVOX_E2E_REAL_INJECTION") { + println!("Skipping injection hardware test: COLDVOX_E2E_REAL_INJECTION set to false/0"); + return; + } + + // Check for display server + let has_display = + std::env::var("DISPLAY").is_ok() || std::env::var("WAYLAND_DISPLAY").is_ok(); + if !has_display { + panic!("No display server detected (DISPLAY or WAYLAND_DISPLAY missing)."); + } + + println!("Display server detected."); + + // We can't easily access the internal test harness from here, + // but verifying the environment variables is a good first step. + // The actual injection test is covered by `coldvox-text-injection` crate tests. + } +} diff --git a/crates/app/tests/integration/capture_integration_test.rs b/crates/app/tests/integration/capture_integration_test.rs index fbc254f6..895d7b5b 100644 --- a/crates/app/tests/integration/capture_integration_test.rs +++ b/crates/app/tests/integration/capture_integration_test.rs @@ -1,3 +1,4 @@ + use tracing_appender::rolling; #[cfg(test)] mod tests { use coldvox_audio::{AudioCapture, AudioConfig, SharedAudioFrame as AudioFrame}; @@ -9,6 +10,16 @@ mod tests { use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; #[test] + // Initialize file logging for test + let _ = std::fs::create_dir_all("target/test_logs"); + let file_appender = rolling::never("target/test_logs", "capture_integration.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking).with_ansi(false)) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + tracing::info!("Starting test_end_to_end_capture_pipewire"); #[cfg(feature = "live-hardware-tests")] fn test_end_to_end_capture_pipewire() { std::env::set_var("COLDVOX_STT_PREFERRED", "whisper"); // Force Faster-Whisper for integration @@ -59,6 +70,7 @@ mod tests { } #[test] + tracing::info!("Starting test_frame_flow"); #[cfg(feature = "live-hardware-tests")] fn test_frame_flow() { let config = AudioConfig::default(); @@ -69,13 +81,20 @@ mod tests { let mut frames_received = 0; let start = std::time::Instant::now(); - while start.elapsed() < Duration::from_secs(1) { - if let Ok(frame) = capture.try_recv_timeout(Duration::from_millis(100)) { - frames_received += 1; - assert_eq!(frame.sample_rate, 16000, "Frame should have correct sample rate"); - // SharedAudioFrame is always mono - assert!(!frame.samples.is_empty(), "Frame should contain samples"); + // Add outer timeout to prevent hanging + let frame_collection_result = tokio::time::timeout(Duration::from_secs(10), async { + while start.elapsed() < Duration::from_secs(1) { + if let Ok(frame) = capture.try_recv_timeout(Duration::from_millis(100)) { + frames_received += 1; + assert_eq!(frame.sample_rate, 16000, "Frame should have correct sample rate"); + tracing::info!("Frame collection completed, frames_received: {}", frames_received); + assert!(frames_received > 0, "Should receive frames from capture"); + } } + }).await; + + if let Err(_) = frame_collection_result { + panic!("Test frame collection timed out after 10 seconds"); } assert!(frames_received > 0, "Should receive frames from capture"); @@ -105,9 +124,16 @@ mod tests { ).expect("Failed to start"); // Simulate Ctrl+C after 1 second - thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); - shutdown_flag.store(true, Ordering::SeqCst); + // Add outer timeout to prevent hanging on shutdown signal + let shutdown_wait_result = tokio::time::timeout(Duration::from_secs(10), async { + while !shutdown_flag.load(Ordering::SeqCst) { + thread::sleep(Duration::from_millis(10)); + } + }).await; + + if let Err(_) = shutdown_wait_result { + panic!("Test shutdown wait timed out after 10 seconds"); + } }); // Wait for shutdown signal diff --git a/crates/app/tests/integration/mock_injection_tests.rs b/crates/app/tests/integration/mock_injection_tests.rs index a255ef75..e08251e4 100644 --- a/crates/app/tests/integration/mock_injection_tests.rs +++ b/crates/app/tests/integration/mock_injection_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod mock_injection_tests { + use tracing_appender::rolling; use coldvox_text_injection::manager::StrategyManager; use coldvox_text_injection::types::{InjectionConfig, InjectionMetrics}; use std::process::{Command, Stdio}; @@ -94,14 +95,39 @@ mod mock_injection_tests { } #[tokio::test] + tracing::info!("Starting test_injection_with_focused_mock_app"); + let _ = std::fs::create_dir_all("target/test_logs"); + let file_appender = rolling::never("target/test_logs", "mock_injection.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking).with_ansi(false)) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + // Initialize file logging for test + let _ = std::fs::create_dir_all"); + let("target/test_logs file_appender = rolling::never("target/test_logs", "mock_injection.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking).with_ansi(false)) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); async fn test_injection_with_focused_mock_app() { + tracing::info!("Starting test_injection_with_focused_mock_app"); // Start mock application + let _ = tracing::span!(tracing::Level::INFO, "test_mock_app_start").entered(); let mut mock_app = match MockTestApp::start().await { Ok(app) => app, + tracing::info!("Starting test_injection_with_focused_mock_app"); Err(e) => { + tracing::info!("Starting test_injection_with_focused_mock_app"); println!("Skipping test: Could not start mock application: {}", e); + tracing::info!("Starting test_injection_with_focused_mock_app"); return; + tracing::info!("Starting test_injection_with_focused_mock_app"); } + tracing::info!("Starting test_injection_with_focused_mock_app"); }; // Focus the application @@ -120,11 +146,10 @@ mod mock_injection_tests { cooldown_initial_ms: 100, ..Default::default() }; - - // Create shared metrics - let metrics = Arc::new(Mutex::new(InjectionMetrics::default())); - - // Create strategy manager + tracing::info!("Starting text injection test"); + let test_text = "Mock injection test"; + let _ = tracing::span!(tracing::Level::INFO, "test_injection").entered(); + let result = timeout( let mut manager = StrategyManager::new(config, metrics.clone()).await; // Test injection diff --git a/crates/app/tests/integration/text_injection_integration_test.rs b/crates/app/tests/integration/text_injection_integration_test.rs index bb971ca3..17a51d2e 100644 --- a/crates/app/tests/integration/text_injection_integration_test.rs +++ b/crates/app/tests/integration/text_injection_integration_test.rs @@ -1,11 +1,21 @@ #[cfg(test)] mod tests { + use tracing_appender::rolling; use coldvox_text_injection::manager::StrategyManager; use coldvox_text_injection::types::{InjectionConfig, InjectionMetrics}; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; + tracing::info!("Starting test_text_injection_end_to_end"); + let _ = std::fs::create_dir_all("target/test_logs"); + let file_appender = rolling::never("target/test_logs", "text_injection.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer().with_writer(non_blocking).with_ansi(false)) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); #[tokio::test] async fn test_text_injection_end_to_end() { std::env::set_var("COLDVOX_STT_PREFERRED", "whisper"); // Force Faster-Whisper for integration @@ -18,8 +28,8 @@ mod tests { // Create strategy manager let mut manager = StrategyManager::new(config, metrics.clone()).await; - - // Test with normal text + tracing::info!("Testing normal text injection"); + let _ = tracing::span!(tracing::Level::INFO, "test_normal_injection").entered(); let result = manager.inject("Hello world").await; assert!(result.is_ok(), "Should successfully inject text"); diff --git a/crates/coldvox-foundation/src/env.rs b/crates/coldvox-foundation/src/env.rs index 56fb8e1f..23ae1fb5 100644 --- a/crates/coldvox-foundation/src/env.rs +++ b/crates/coldvox-foundation/src/env.rs @@ -975,8 +975,20 @@ mod tests { #[test] #[serial] fn test_detect_development_environment() { - // Clear environment variables - for var in ["CI", "DEBUG", "RUST_BACKTRACE", "XDG_SESSION_TYPE"] { + // Clear ALL CI-related environment variables (GitHub Actions, GitLab CI, etc.) + for var in [ + "CI", + "CONTINUOUS_INTEGRATION", + "GITHUB_ACTIONS", + "GITLAB_CI", + "TRAVIS", + "CIRCLECI", + "JENKINS_URL", + "BUILDKITE", + "DEBUG", + "RUST_BACKTRACE", + "XDG_SESSION_TYPE", + ] { env::remove_var(var); } diff --git a/crates/coldvox-stt/Cargo.toml b/crates/coldvox-stt/Cargo.toml index d399bd96..7a6772b3 100644 --- a/crates/coldvox-stt/Cargo.toml +++ b/crates/coldvox-stt/Cargo.toml @@ -20,8 +20,8 @@ coldvox-telemetry = { path = "../coldvox-telemetry" } # Parakeet STT via pure Rust ONNX Runtime parakeet-rs = { version = "0.2", optional = true } -# Moonshine STT via PyO3/HuggingFace -pyo3 = { version = "0.24.1", optional = true, features = ["auto-initialize"] } +# Moonshine STT via PyO3/HuggingFace - Fixed auto-initialize +pyo3 = { version = "0.27", optional = true, features = ["auto-initialize"] } tempfile = { version = "3.8", optional = true } hound = { version = "3.5", optional = true } @@ -38,3 +38,8 @@ silero-stt = [] tokio = { version = "1.35", features = ["rt-multi-thread", "macros"] } serial_test = "3.2" hound = "3.5" + +# Gate moonshine example behind feature flag +[[example]] +name = "verify_moonshine" +required-features = ["moonshine"] diff --git a/crates/coldvox-stt/examples/verify_moonshine.rs b/crates/coldvox-stt/examples/verify_moonshine.rs new file mode 100644 index 00000000..093144e4 --- /dev/null +++ b/crates/coldvox-stt/examples/verify_moonshine.rs @@ -0,0 +1,68 @@ +use coldvox_stt::plugin::{SttPlugin, SttPluginFactory}; +use coldvox_stt::plugins::moonshine::MoonshinePluginFactory; +use coldvox_stt::types::TranscriptionConfig; +use hound; +use std::time::Instant; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 1. Setup minimal logging (removed tracing-subscriber dependency) + println!("🚀 Starting Moonshine Verification..."); + + // 2. Create Plugin Factory + // This will check requirement (Python deps) + println!("🔍 Checking requirements..."); + let factory = MoonshinePluginFactory::new(); + factory.check_requirements()?; + + // 3. Create Plugin Instance + println!("📦 Creating plugin instance..."); + let mut plugin = factory.create()?; + + // 4. Initialize (loads model) + println!("⏳ Initializing model (this uses PyO3 and might take a moment)..."); + let start = Instant::now(); + plugin.initialize(TranscriptionConfig::default()).await?; + println!("✅ Model loaded in {:.2?}", start.elapsed()); + + // 5. Generate Test Audio (1s of 440Hz sine wave @ 16kHz) + println!("🎵 Generating test audio..."); + let sample_rate = 16000; + let duration_secs = 2; + let spec = hound::WavSpec { + channels: 1, + sample_rate, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + // We create a buffer of samples + let mut samples = Vec::new(); + for t in (0..sample_rate * duration_secs).map(|x| x as f32 / sample_rate as f32) { + let sample = (t * 440.0 * 2.0 * std::f32::consts::PI).sin(); + let amplitude = i16::MAX as f32 * 0.5; + samples.push((sample * amplitude) as i16); + } + + // 6. Process Audio + println!("🗣️ Processing audio ({} samples)...", samples.len()); + let process_start = Instant::now(); + + // Feed audio in chunks + let chunk_size = 4000; + for chunk in samples.chunks(chunk_size) { + plugin.process_audio(chunk).await?; + } + + // 7. Finalize (Trigger transcription) + println!("📝 Finalizing and transcribing..."); + if let Some(event) = plugin.finalize().await? { + println!("🎉 Transcription Result: {:?}", event); + } else { + println!("⚠️ No transcription result returned."); + } + + println!("⏱️ Total processing time: {:.2?}", process_start.elapsed()); + + Ok(()) +} diff --git a/crates/coldvox-stt/src/plugins/moonshine.rs b/crates/coldvox-stt/src/plugins/moonshine.rs index f41fd527..420fc181 100644 --- a/crates/coldvox-stt/src/plugins/moonshine.rs +++ b/crates/coldvox-stt/src/plugins/moonshine.rs @@ -131,7 +131,7 @@ impl MoonshinePlugin { #[cfg(feature = "moonshine")] fn verify_python_environment() -> Result<(), ColdVoxError> { - Python::with_gil(|py| { + Python::attach(|py| { PyModule::import(py, "transformers").map_err(|_| { SttError::LoadFailed( "transformers not installed. Run: pip install transformers>=4.35.0".to_string(), @@ -167,7 +167,7 @@ impl MoonshinePlugin { .and_then(|p| p.to_str()) .unwrap_or_else(|| self.model_size.model_identifier()); - Python::with_gil(|py| { + Python::attach(|py| { let locals = PyDict::new(py); locals .set_item("model_id", model_id) @@ -198,7 +198,7 @@ _processor = AutoProcessor.from_pretrained(model_id) None, Some(&locals), ) - .map_err(|e| SttError::LoadFailed(format!("Failed to load model: {}", e)))?; + .map_err(|e| SttError::LoadFailed(format!("Failed to load model: {}", e)))?; // Extract model and processor from locals dict let model = locals @@ -238,7 +238,7 @@ _processor = AutoProcessor.from_pretrained(model_id) .as_ref() .ok_or_else(|| SttError::TranscriptionFailed("Processor not loaded".to_string()))?; - Python::with_gil(|py| { + Python::attach(|py| { let locals = PyDict::new(py); // SECURITY: Pass variables via locals dict, not string interpolation @@ -281,7 +281,7 @@ _transcription = processor.batch_decode(generated_ids, skip_special_tokens=True) None, Some(&locals), ) - .map_err(|e| SttError::TranscriptionFailed(format!("Python error: {}", e)))?; + .map_err(|e| SttError::TranscriptionFailed(format!("Python error: {}", e)))?; let result = locals .get_item("_transcription") @@ -611,7 +611,7 @@ impl SttPluginFactory for MoonshinePluginFactory { #[cfg(feature = "moonshine")] fn check_moonshine_available() -> bool { - Python::with_gil(|py| { + Python::attach(|py| { PyModule::import(py, "transformers").is_ok() && PyModule::import(py, "torch").is_ok() && PyModule::import(py, "librosa").is_ok() diff --git a/crates/coldvox-stt/tests/moonshine_e2e.rs b/crates/coldvox-stt/tests/moonshine_e2e.rs index d3b35096..e223dc11 100644 --- a/crates/coldvox-stt/tests/moonshine_e2e.rs +++ b/crates/coldvox-stt/tests/moonshine_e2e.rs @@ -281,7 +281,10 @@ async fn test_buffer_overflow_protection() { // Reset clears the buffer - if we got here without panic, the limit worked plugin.reset().await.expect("Reset failed"); - println!("Buffer overflow protection verified (limit: {} samples)", MAX_SAMPLES); + println!( + "Buffer overflow protection verified (limit: {} samples)", + MAX_SAMPLES + ); } /// Test that common::load_test_audio validates audio format correctly diff --git a/crates/coldvox-text-injection/Cargo.toml b/crates/coldvox-text-injection/Cargo.toml index 18f055ac..78ffff8e 100644 --- a/crates/coldvox-text-injection/Cargo.toml +++ b/crates/coldvox-text-injection/Cargo.toml @@ -40,6 +40,7 @@ tokio-test = "0.4" rand = "0.9" arboard = "3.2" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" serial_test = "3.2" [features] diff --git a/crates/coldvox-text-injection/build.rs b/crates/coldvox-text-injection/build.rs index 1fa1a509..5b679bc7 100644 --- a/crates/coldvox-text-injection/build.rs +++ b/crates/coldvox-text-injection/build.rs @@ -63,16 +63,19 @@ fn build_gtk_test_app() { fn build_terminal_test_app() { println!("cargo:rerun-if-changed=test-apps/terminal-test-app/src/main.rs"); println!("cargo:rerun-if-changed=test-apps/terminal-test-app/Cargo.toml"); + println!("cargo:rerun-if-changed=test-apps/terminal-test-app/Cargo.lock"); let out_dir = env::var("OUT_DIR").unwrap(); let target_dir = Path::new(&out_dir).join("terminal-test-app-target"); // Build the terminal test app using `cargo build`. + // Note: the test app is NOT part of the main workspace, so we must specify --manifest-path. let status = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())) .arg("build") - .arg("--package") - .arg("terminal-test-app") + .arg("--manifest-path") + .arg("test-apps/terminal-test-app/Cargo.toml") .arg("--release") // Build in release mode for faster startup. + .arg("--locked") .arg("--target-dir") .arg(&target_dir) .status() diff --git a/crates/coldvox-text-injection/src/confirm.rs b/crates/coldvox-text-injection/src/confirm.rs index 54e0d7b8..6755c6b8 100644 --- a/crates/coldvox-text-injection/src/confirm.rs +++ b/crates/coldvox-text-injection/src/confirm.rs @@ -54,11 +54,14 @@ //! - Future: Could extend to clipboard/enigo fallbacks for cross-method validation use crate::types::{InjectionConfig, InjectionResult}; +#[cfg(feature = "atspi")] use coldvox_foundation::error::InjectionError; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::Mutex; -use tracing::{debug, error, info, trace, warn}; +#[cfg(feature = "atspi")] +use tracing::{debug, error, trace}; +use tracing::{info, warn}; use unicode_segmentation::UnicodeSegmentation; /// Confirmation result for text injection @@ -136,7 +139,9 @@ pub async fn text_changed( prefix: &str, window: &str, ) -> InjectionResult { + #[allow(unused)] let start_time = Instant::now(); + #[allow(unused)] let timeout_duration = Duration::from_millis(75); info!( diff --git a/crates/coldvox-text-injection/src/injectors/atspi.rs b/crates/coldvox-text-injection/src/injectors/atspi.rs index 78f8a43b..d227ebbb 100644 --- a/crates/coldvox-text-injection/src/injectors/atspi.rs +++ b/crates/coldvox-text-injection/src/injectors/atspi.rs @@ -5,7 +5,9 @@ //! while providing the new TextInjector trait interface. use crate::confirm::{create_confirmation_context, ConfirmationContext}; +#[cfg(feature = "atspi")] use crate::log_throttle::log_atspi_connection_failure; +#[cfg(feature = "atspi")] use crate::logging::utils; use crate::types::{ InjectionConfig, InjectionContext, InjectionMethod, InjectionMode, InjectionResult, @@ -43,7 +45,12 @@ impl AtspiInjector { } /// Insert text directly using AT-SPI EditableText interface - pub async fn insert_text(&self, text: &str, context: &InjectionContext) -> InjectionResult<()> { + pub async fn insert_text( + &self, + text: &str, + #[allow(unused)] context: &InjectionContext, + ) -> InjectionResult<()> { + #[allow(unused)] let start_time = Instant::now(); trace!("AT-SPI insert_text starting for {} chars", text.len()); @@ -246,7 +253,12 @@ impl AtspiInjector { } /// Paste text using AT-SPI clipboard operations - pub async fn paste_text(&self, text: &str, context: &InjectionContext) -> InjectionResult<()> { + pub async fn paste_text( + &self, + text: &str, + #[allow(unused)] context: &InjectionContext, + ) -> InjectionResult<()> { + #[allow(unused)] let start_time = Instant::now(); trace!("AT-SPI paste_text starting for {} chars", text.len()); diff --git a/crates/coldvox-text-injection/src/prewarm.rs b/crates/coldvox-text-injection/src/prewarm.rs index 58621832..0635edd9 100644 --- a/crates/coldvox-text-injection/src/prewarm.rs +++ b/crates/coldvox-text-injection/src/prewarm.rs @@ -9,11 +9,14 @@ use crate::types::{InjectionConfig, InjectionMethod, InjectionResult}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{Mutex, RwLock}; -use tracing::{debug, info, trace, warn}; +#[cfg(feature = "atspi")] +use tracing::trace; +use tracing::{debug, info, warn}; /// TTL for cached pre-warmed data (3 seconds) const CACHE_TTL: Duration = Duration::from_secs(3); /// Tiny timeout for individual pre-warming steps (50ms) +#[cfg(feature = "atspi")] const STEP_TIMEOUT: Duration = Duration::from_millis(50); /// Pre-warmed data with TTL caching @@ -161,6 +164,7 @@ impl PrewarmController { /// Pre-warm AT-SPI connection and snapshot focused element async fn prewarm_atspi(&self) -> Result { + #[allow(unused)] let start_time = Instant::now(); debug!("Starting AT-SPI pre-warming"); @@ -246,6 +250,7 @@ impl PrewarmController { /// Arm the event listener for text change confirmation async fn arm_event_listener(&self) -> Result { + #[allow(unused)] let start_time = Instant::now(); debug!("Arming event listener for text change confirmation"); diff --git a/crates/coldvox-text-injection/src/tests/mod.rs b/crates/coldvox-text-injection/src/tests/mod.rs index b576f697..8d005c47 100644 --- a/crates/coldvox-text-injection/src/tests/mod.rs +++ b/crates/coldvox-text-injection/src/tests/mod.rs @@ -1,6 +1,8 @@ //! Test modules for coldvox-text-injection -// pub mod real_injection; +pub mod real_injection; +#[cfg(feature = "real-injection-tests")] +pub mod test_harness; pub mod test_utils; pub mod wl_copy_basic_test; pub mod wl_copy_simple_test; diff --git a/crates/coldvox-text-injection/src/tests/real_injection.rs.disabled b/crates/coldvox-text-injection/src/tests/real_injection.rs similarity index 68% rename from crates/coldvox-text-injection/src/tests/real_injection.rs.disabled rename to crates/coldvox-text-injection/src/tests/real_injection.rs index 82d5b5c1..a68404a1 100644 --- a/crates/coldvox-text-injection/src/tests/real_injection.rs.disabled +++ b/crates/coldvox-text-injection/src/tests/real_injection.rs @@ -7,6 +7,8 @@ //! To run these tests, use the following command: //! `cargo test -p coldvox-text-injection --features real-injection-tests` +#![cfg(feature = "real-injection-tests")] + // NOTE: Using modular injectors from the injectors module #[cfg(feature = "wl_clipboard")] use crate::clipboard_paste_injector::ClipboardPasteInjector; @@ -17,16 +19,47 @@ use crate::injectors::atspi::AtspiInjector; #[cfg(feature = "ydotool")] use crate::ydotool_injector::YdotoolInjector; // Bring trait into scope so async trait methods (inject_text, is_available) resolve. +#[cfg(any( + feature = "atspi", + feature = "enigo", + feature = "wl_clipboard", + feature = "ydotool" +))] use crate::TextInjector; -use crate::tests::test_harness::{verify_injection, TestApp, TestAppManager, TestEnvironment}; +use crate::tests::test_harness::{TestApp, TestAppManager, TestEnvironment}; + +#[cfg(any( + feature = "atspi", + feature = "enigo", + feature = "wl_clipboard", + feature = "ydotool" +))] +use crate::tests::test_harness::verify_injection; use std::time::Duration; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; /// A placeholder test to verify that the test harness, build script, and /// environment detection are all working correctly. #[tokio::test] async fn harness_self_test_launch_gtk_app() { + // Setup logging + let _ = std::fs::create_dir_all("target/logs"); + let file_appender = tracing_appender::rolling::never("target/logs", "text_injection_tests.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + let env = TestEnvironment::current(); if !env.can_run_real_tests() { eprintln!("Skipping real injection test: no display server found."); @@ -70,24 +103,44 @@ async fn wait_for_app_ready(app: &TestApp) { /// Helper function to run a complete injection and verification test for the AT-SPI backend. async fn run_atspi_test(test_text: &str) { - let env = TestEnvironment::current(); - if !env.can_run_real_tests() { - // This check is technically redundant if the tests are run with the top-level skip, - // but it's good practice to keep it for clarity and direct execution. - eprintln!("Skipping AT-SPI test: no display server found."); + // Early return if atspi feature is not enabled - before launching any GTK apps + #[cfg(not(feature = "atspi"))] + { + let _ = test_text; + println!("Skipping AT-SPI test: atspi feature not enabled"); return; } - let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); - - // Allow time for the app to initialize and for the AT-SPI bus to register it. - // This is a common requirement in UI testing. - tokio::time::sleep(Duration::from_millis(500)).await; - // Wait for the app to be fully initialized before interacting with it. - wait_for_app_ready(&app).await; - #[cfg(feature = "atspi")] { + // Setup logging + let _ = std::fs::create_dir_all("target/logs"); + let file_appender = + tracing_appender::rolling::never("target/logs", "text_injection_tests.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + + let env = TestEnvironment::current(); + if !env.can_run_real_tests() { + eprintln!("Skipping AT-SPI test: no display server found."); + return; + } + + let app = TestAppManager::launch_gtk_app().expect("Failed to launch GTK app."); + + // Allow time for the app to initialize and for the AT-SPI bus to register it. + tokio::time::sleep(Duration::from_millis(500)).await; + wait_for_app_ready(&app).await; + let injector = AtspiInjector::new(Default::default()); if !injector.is_available().await { println!( @@ -96,24 +149,22 @@ async fn run_atspi_test(test_text: &str) { return; } - injector.inject_text(test_text).await.unwrap_or_else(|e| { - panic!("AT-SPI injection failed for text '{}': {:?}", test_text, e) - }); - } + injector + .inject_text(test_text, None) + .await + .unwrap_or_else(|e| { + panic!("AT-SPI injection failed for text '{}': {:?}", test_text, e) + }); - #[cfg(not(feature = "atspi"))] - { - println!("Skipping AT-SPI test: atspi feature not enabled"); + verify_injection(&app.output_file, test_text) + .await + .unwrap_or_else(|e| { + panic!( + "Verification failed for AT-SPI with text '{}': {}", + test_text, e + ) + }); } - - verify_injection(&app.output_file, test_text) - .await - .unwrap_or_else(|e| { - panic!( - "Verification failed for AT-SPI with text '{}': {}", - test_text, e - ) - }); } #[tokio::test] @@ -151,6 +202,21 @@ async fn test_atspi_special_chars() { /// Helper function to run a complete injection and verification test for the ydotool backend. /// This test involves setting the clipboard, as ydotool's primary injection method is paste. async fn run_ydotool_test(test_text: &str) { + // Setup logging + let _ = std::fs::create_dir_all("target/logs"); + let file_appender = tracing_appender::rolling::never("target/logs", "text_injection_tests.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + let env = TestEnvironment::current(); if !env.can_run_real_tests() { eprintln!("Skipping ydotool test: no display server found."); @@ -184,7 +250,7 @@ async fn run_ydotool_test(test_text: &str) { // The inject_text for ydotool will trigger a paste (Ctrl+V). injector - .inject_text(test_text) + .inject_text(test_text, None) .await .unwrap_or_else(|e| panic!("ydotool injection failed for text '{}': {:?}", test_text, e)); @@ -233,6 +299,21 @@ async fn test_ydotool_special_chars() { /// Helper to test clipboard injection followed by a paste action. /// This simulates a realistic clipboard workflow. async fn run_clipboard_paste_test(test_text: &str) { + // Setup logging + let _ = std::fs::create_dir_all("target/logs"); + let file_appender = tracing_appender::rolling::never("target/logs", "text_injection_tests.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + let env = TestEnvironment::current(); if !env.can_run_real_tests() { eprintln!("Skipping clipboard test: no display server found."); @@ -257,7 +338,7 @@ async fn run_clipboard_paste_test(test_text: &str) { // Perform clipboard+paste using the combined injector (it will try AT-SPI first then ydotool). clipboard_paste - .inject_text(test_text) + .inject_text(test_text, None) .await .expect("Clipboard+paste injection failed."); @@ -274,6 +355,8 @@ async fn run_clipboard_paste_test(test_text: &str) { #[cfg(not(all(feature = "wl_clipboard", feature = "enigo")))] { + // Suppress unused variable warning when features are disabled + let _ = test_text; println!("Skipping clipboard test: required features (wl_clipboard, enigo) not enabled"); } } @@ -294,6 +377,21 @@ async fn test_clipboard_unicode_text() { /// Helper to test the direct typing capability of the Enigo backend. async fn run_enigo_typing_test(test_text: &str) { + // Setup logging + let _ = std::fs::create_dir_all("target/logs"); + let file_appender = tracing_appender::rolling::never("target/logs", "text_injection_tests.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + + let _ = tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with( + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false), + ) + .with(tracing_subscriber::fmt::layer().with_test_writer()) + .try_init(); + let env = TestEnvironment::current(); if !env.can_run_real_tests() { eprintln!("Skipping enigo typing test: no display server found."); @@ -330,6 +428,8 @@ async fn run_enigo_typing_test(test_text: &str) { #[cfg(not(feature = "enigo"))] { + // Suppress unused variable warning when feature is disabled + let _ = test_text; println!("Skipping enigo typing test: enigo feature not enabled"); } } diff --git a/crates/coldvox-text-injection/test-apps/gtk_test_app.c b/crates/coldvox-text-injection/test-apps/gtk_test_app.c index 49f6d29e..e38ff786 100644 --- a/crates/coldvox-text-injection/test-apps/gtk_test_app.c +++ b/crates/coldvox-text-injection/test-apps/gtk_test_app.c @@ -19,9 +19,24 @@ static void on_text_changed(GtkEditable *editable, gpointer user_data) { fclose(f); } +// Create a ready file to signal that the app has started. +// This allows tests to detect when the app is ready without relying on text changes. +static void create_ready_file(void) { + char filepath[256]; + snprintf(filepath, sizeof(filepath), "/tmp/coldvox_gtk_test_%d.txt", getpid()); + FILE *f = fopen(filepath, "w"); + if (f != NULL) { + // Write empty content - file existence is the signal + fclose(f); + } +} + int main(int argc, char *argv[]) { gtk_init(&argc, &argv); + // Create the ready file immediately so tests know the app is running + create_ready_file(); + // Create the main window GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "GTK Test App"); diff --git a/crates/coldvox-text-injection/test-apps/terminal-test-app/Cargo.lock b/crates/coldvox-text-injection/test-apps/terminal-test-app/Cargo.lock new file mode 100644 index 00000000..8c75eb7a --- /dev/null +++ b/crates/coldvox-text-injection/test-apps/terminal-test-app/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "terminal-test-app" +version = "0.1.0" diff --git a/criticalActionPlan.md b/criticalActionPlan.md new file mode 100644 index 00000000..375d6463 --- /dev/null +++ b/criticalActionPlan.md @@ -0,0 +1,174 @@ +# Critical Action Plan + +> **Created**: 2025-12-14 +> **Status**: ACTIVE +> **Priority**: P0 - Documentation is actively misleading agents and developers + +This document tracks critical issues where documentation claims functionality that doesn't exist or is broken. These are not "nice to haves" — they cause immediate failures for anyone following the docs. + +--- + +## P0: Documentation Claims Broken Features Work + +### Issue 1: `whisper` Feature is Non-Functional + +**Docs claim** (AGENTS.md, README.md, CLAUDE.md): +```bash +cargo run --features whisper,text-injection # With STT +``` + +**Reality**: +- `whisper = []` in `crates/app/Cargo.toml` — empty stub +- `whisper_plugin.rs` is commented out: `// pub mod whisper_plugin;` +- Enabling the feature only exposes `whisper_cpp.rs` which is a non-functional stub +- **Result**: Zero STT capability despite docs promising it + +**Decision**: Whisper backend was intentionally removed. It should stay gone unless a pure-Rust replacement is implemented. + +**Action Required**: +- [ ] Remove all references to `--features whisper` from docs +- [ ] Remove `whisper` feature from `Cargo.toml` entirely (it's misleading) +- [ ] Update README Quick Start to show moonshine as the STT option +- [ ] Update AGENTS.md feature flags section +- [ ] Update CLAUDE.md STT integration section +- [ ] Remove `whisper_plugin.rs` and `whisper_cpp.rs` dead code + +**Files to update**: +- `README.md` (lines 28, 34, 55, 62-71) +- `AGENTS.md` (lines 62, 69, 107) +- `CLAUDE.md` (STT Integration section) + +--- + +### Issue 2: `parakeet` Feature Doesn't Compile + +**Docs claim** (AGENTS.md): +``` +Use feature flags: whisper, parakeet, text-injection, silero +``` + +**Reality**: +```bash +cargo build -p coldvox-app --features parakeet +# FAILS with 6 compile errors +# - transcribe_samples() signature mismatch +# - confidence field doesn't exist on TimedToken +``` + +**Root cause**: `parakeet-rs = "0.2"` API changed, plugin code not updated. + +**Decision**: Parakeet will be fixed, but is low priority. For now, remove from docs as "working" and mark as "planned". + +**Action Required**: +- [ ] Remove parakeet from AGENTS.md "Use feature flags" list (immediate) +- [ ] Add note that parakeet is planned but not yet working +- [ ] Fix `crates/coldvox-stt/src/plugins/parakeet.rs` to match parakeet-rs 0.2 API (low priority) +- [ ] Add CI job that builds with `--features parakeet` once fixed + +**Files to fix**: +- `crates/coldvox-stt/src/plugins/parakeet.rs` (low priority) + +--- + +### Issue 3: Python Version Nightmare + +**The chaos**: +- `mise.toml`: `python = "3.13"` +- `.python-version`: `3.12` +- System Python: 3.14 +- PyO3 0.27: Only works with `<= 3.12` + +**What is mise?** mise (formerly rtx) is a polyglot version manager — like asdf/nvm/pyenv unified. It reads `mise.toml` and installs toolchains. The problem: it's configured for 3.13 which breaks PyO3. + +**Why can't UV just own everything?** It can! The `.envrc` already creates a UV-managed venv with the correct Python. The issue is: +1. `mise.toml` contradicts `.python-version` +2. Developers who run `mise install` get 3.13 which breaks the build +3. There's no enforcement that UV is the only path + +**Resolution**: Make UV the single source of truth. Remove Python from mise.toml entirely. + +**Action Required**: +- [ ] Remove `python = "3.13"` from `mise.toml` (let UV own Python) +- [ ] Keep `.python-version = 3.12` for UV and other tools that respect it +- [ ] Document: "All Python flows through UV. Run `uv sync` before building moonshine." +- [ ] Add CI check that moonshine builds with UV-managed Python + +**Files to update**: +- `mise.toml` (remove line 11: `python = "3.13"`) +- Add to README/AGENTS.md: "Python is managed by UV only" + +--- + +### Issue 4: requirements.txt vs pyproject.toml Confusion + +**requirements.txt**: Empty ("No external dependencies currently required") +**pyproject.toml**: Has `transformers`, `torch`, `librosa` + +**Result**: `uv pip install -r requirements.txt` installs nothing useful. + +**Action Required**: +- [ ] Delete `requirements.txt` (it's vestigial) +- [ ] OR populate it correctly +- [ ] Document that `uv sync` is the correct command + +**Files to update**: +- `requirements.txt` (delete or fix) + +--- + +## P1: Stub Features Waste Developer Time + +These features are defined in Cargo.toml but do nothing: + +| Feature | Status | Action | +|---------|--------|--------| +| `whisper` | Empty stub | Remove or implement | +| `coqui` | Empty stub | Remove or implement | +| `leopard` | Empty stub | Remove or implement | +| `silero-stt` | Empty stub | Remove or implement | +| `no-stt` | Defined but doesn't gate anything | Remove or implement | + +**Action Required**: +- [ ] Remove stub features from Cargo.toml +- [ ] OR add `compile_error!()` that explains they're not implemented +- [ ] Remove from all documentation + +--- + +## P2: CI/Code Mismatch + +### Golden Master installs faster-whisper but code doesn't use it + +**CI does**: `pip install faster-whisper` +**Code does**: Nothing with it (whisper backend commented out) + +**Action Required**: +- [ ] Remove `pip install faster-whisper` from CI +- [ ] OR re-enable whisper backend +- [ ] Clarify what golden master tests actually test + +--- + +## What Actually Works (Verified 2025-12-14) + +| Feature | Status | Notes | +|---------|--------|-------| +| Default build | ✅ Works | `cargo build -p coldvox-app` | +| Moonshine STT | ✅ Works | Requires `uv sync` first | +| Text injection | ✅ Works | Default feature | +| Silero VAD | ✅ Works | Default feature | +| Tests | ✅ Works | `cargo test -p coldvox-app` | + +--- + +## Tracking + +- [ ] All P0 issues resolved +- [ ] All P1 issues resolved +- [ ] README accurately reflects working features +- [ ] AGENTS.md accurately reflects working features +- [ ] CI verifies all documented features compile + +--- + +*This document should be deleted once all issues are resolved and docs are accurate.* diff --git a/docs/architecture.md b/docs/architecture.md index 631447d7..6a79f848 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -9,10 +9,13 @@ last_reviewed: 2025-10-19 # ColdVox Architecture & Future Vision +> **⚠️ CRITICAL**: STT backend status has changed. See [`../criticalActionPlan.md`](../criticalActionPlan.md) for current working features. + ## Navigation - [Architecture Roadmap](./architecture/roadmap.md) - [Architecture Decisions](./architecture/adr/) +- [Critical Action Plan](../criticalActionPlan.md) - Current broken features tracking diff --git a/docs/dependencies.md b/docs/dependencies.md index 14f9aced..06bb1442 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -4,7 +4,7 @@ subsystem: general version: 1.0.0 status: draft owners: Documentation Working Group -last_reviewed: 2025-12-03 +last_reviewed: 2025-12-12 --- # Dependency Overview @@ -71,3 +71,46 @@ The `deny.toml` file in the repository root configures cargo-deny: - **rustfmt**: Code formatting (`cargo fmt`) - **clippy**: Linting (`cargo clippy`) - **uv**: Python dependency management for STT plugins + +## Mixed Rust + Python Tooling (Dec 2025) + +- **uv (Python 3.13-ready)**: Use `uv` for Python env + lockfiles; leverage its global cache to keep CI downloads minimal across runners. Treat `uv lock` as the single source of truth for Python deps; prefer `uv tool install` for CLI tools (ruff, maturin). +- **maturin for packaging**: Build PyO3 wheels with `maturin build -r` and install in CI with `maturin develop` when you need editable bindings. Prefer `uv tool run maturin ...` so we do not rely on system pip. Follow PyO3 0.27 guidance for free-threaded Python 3.13 by enabling the `abi3-py313` or interpreter-specific feature in the binding crates as needed. +- **Rust/Python interface hygiene**: Avoid `pyo3` debug builds in CI; set `PYO3_CONFIG_FILE` only when linking against non-system Python. For embedded Python, ensure `python3-devel` is present on self-hosted runners and keep `extension-module` + `auto-initialize` features constrained to the crates that need them. +- **Shared caching**: Keep `target/` out of VCS; rely on Swatinem `rust-cache` in GitHub Actions plus `uv` global cache for Python wheels. + +### sccache (Rust Build Cache) + +sccache caches Rust compilation artifacts across builds, significantly reducing rebuild times for unchanged code. + +**Installation (one-time on self-hosted runner)**: +```bash +just setup-sccache +# Or manually: cargo install sccache --locked +``` + +**CI Integration**: The CI workflow automatically: +1. Checks if sccache is available on the runner +2. If found, starts the sccache server and sets `RUSTC_WRAPPER` +3. If not found, builds proceed normally (no failure) + +**Local Development**: +```bash +# Add to ~/.bashrc or ~/.zshrc +export RUSTC_WRAPPER=sccache +export SCCACHE_DIR=~/.cache/sccache + +# Check stats +sccache --show-stats +``` + +**Cache Location**: `~/.cache/sccache` (configurable via `SCCACHE_DIR`) + +**Expected Savings**: 30-60% reduction in incremental build times on the self-hosted runner. + +## CI Gating Expectations + +- **Security gates on every PR**: Run `cargo deny check` and `cargo audit` as blocking jobs (they are already configured in CI—make them non-optional locally via `just lint` or `scripts/local_ci.sh`). +- **Non-dummy end-to-end**: Hardware-backed E2E jobs (audio/VAD/STT/text injection) must execute on the self-hosted runner with real devices; remove or avoid placeholders. Use sharded CI so only that job targets the self-hosted runner while unit/integration suites run on hosted Linux. +- **Cache-aware sharding**: Reuse the cargo cache populated on the self-hosted runner across the E2E job; keep other jobs on hosted runners to reduce queue time. If possible, warm the cache with a `cargo build --locked` step before running E2E to minimize device occupancy. +- **Python/Rust lock discipline**: Keep `uv.lock` and `Cargo.lock` in sync with feature flags and PyO3 ABI choices; treat lock drift as a failing check in pre-commit/CI. diff --git a/docs/issues/pyo3_instability.md b/docs/issues/pyo3_instability.md new file mode 100644 index 00000000..0f70a1ea --- /dev/null +++ b/docs/issues/pyo3_instability.md @@ -0,0 +1,36 @@ +--- +doc_type: troubleshooting +subsystem: stt +version: 1.0.0 +status: draft +owners: Documentation Working Group +last_reviewed: 2025-12-12 +--- + +# Issue: PyO3 0.27 Instability on Python 3.13 (Moonshine Backend) + +**Status**: DRAFT (Local) +**Created**: 2025-12-10 +**Priority**: High (Blocks stable build on modern Linux distros) + +## Problem +PyO3 0.27 introduces breaking changes and strict requirements for Python 3.13 compatibility, specifically regarding free-threaded builds (GIL removal). This impacts the `moonshine` STT plugin in ColdVox. + +## Symptoms +- Build errors on systems with Python 3.13 default (e.g., Arch, Fedora Rawhide). +- Potential runtime panics if `#[pyclass]` structs do not implement `Sync`. +- API deprecations/renames (`Python::with_gil` replaced by `Python::attach`). + +## Findings from Research +1. **Free-Threading (3.13t)**: Python 3.13 supports experimental free-threading. PyO3 0.27 requires `Sync` implementation for all `#[pyclass]` types to support this. +2. **API Change**: `Python::with_gil` is replaced by `Python::attach` in PyO3 0.27 for improved compatibility with free-threaded Python. +3. **Build Tooling**: Attempting to build against Python 3.13 with older versions (or mismatched feature flags) fails. +4. **Current Config**: `coldvox-stt` uses `pyo3 = "0.27"`. + +## Impact on ColdVox +`moonshine.rs` uses `Python::with_gil` extensively. If the system Python is 3.13, the build may produce unstable binaries or fail link checks because our `MoonshinePlugin` struct holds `Py` fields that might need `Sync` guards in the new model. + +## Recommendation +1. **Short Term**: Pin Python to 3.12 for stability via `.python-version` or `pyenv`. +2. **Code Change**: Audit all `Py` usage in `moonshine.rs` for `Sync` compliance. +3. **Configuration**: Consider enabling `abi3-py313` feature in `Cargo.toml` or setting `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`. diff --git a/docs/playbooks/organizational/ci_cd_playbook.md b/docs/playbooks/organizational/ci_cd_playbook.md index ae0592f8..9c8cc68c 100644 --- a/docs/playbooks/organizational/ci_cd_playbook.md +++ b/docs/playbooks/organizational/ci_cd_playbook.md @@ -86,7 +86,59 @@ When cargo-audit or cargo-deny reports a new advisory: ``` 4. **Document**: Add to CHANGELOG.md under Security & Tooling +## Build Caching + +### sccache + +The self-hosted runner uses sccache for Rust compilation caching. This is set up automatically via CI but can be pre-installed: + +```bash +# One-time setup on runner +just setup-sccache + +# Or manually +cargo install sccache --locked +``` + +The CI workflow (`text_injection_tests` job) will: +1. Run `just setup-sccache` to ensure sccache is installed +2. Start the sccache server +3. Set `RUSTC_WRAPPER=sccache` for all subsequent cargo commands + +**Expected impact**: 30-60% faster incremental builds. + +### Swatinem/rust-cache + +Standard cargo cache action. Works on both hosted and self-hosted runners. + +## AI-Powered CI Failure Analysis + +When CI fails on a PR, the `ci-failure-analysis.yml` workflow automatically: +1. Verifies the failure (handles race conditions) +2. Gets PR number via SHA lookup (works for fork PRs) +3. Fetches logs from failed jobs +4. Analyzes the failure using **Gemini 2.5 Flash** AI (with thinking/reasoning) +5. Posts a comment on the PR with root cause analysis and fix suggestions + +**Requirements**: +- `GEMINI_API_KEY` secret must be set in repository settings +- Get a free key from https://aistudio.google.com/app/apikey + +**Configuration**: `.github/workflows/ci-failure-analysis.yml` + +**Design Decisions** (based on research): +- Uses `ubuntu-latest` (not self-hosted) for security - auxiliary workflows handling untrusted PR data should run on ephemeral GitHub-hosted runners +- Uses SHA-based PR lookup via `listPullRequestsAssociatedWithCommit` API (GitHub bug: `workflow_run.pull_requests` is empty for fork PRs) +- Verifies `conclusion` via API call before proceeding (race condition workaround) +- Uses Gemini 2.5 Flash (`/v1beta` endpoint for latest models) with exponential backoff retry +- Limits log size to 50KB to stay within API token limits + +**Cost**: Free tier (250 requests/day) is sufficient for most projects. Estimated ~$0.78/month if exceeding free tier. + +This runs only on PR failures (not push failures) to avoid noise. + ## Related Documentation - [docs/dependencies.md](../../dependencies.md) - Dependency overview and tooling docs - [deny.toml](../../../deny.toml) - cargo-deny configuration +- [runner_setup.md](./runner_setup.md) - Self-hosted runner setup guide diff --git a/docs/playbooks/organizational/runner_setup.md b/docs/playbooks/organizational/runner_setup.md index 3dcdb062..b81b052f 100644 --- a/docs/playbooks/organizational/runner_setup.md +++ b/docs/playbooks/organizational/runner_setup.md @@ -117,6 +117,43 @@ xorg-x11-server-Xvfb fluxbox dbus-x11 at-spi2-core wl-clipboard xclip ydotool xorg-x11-utils wmctrl gtk3-devel ``` +### Build Tooling + +#### sccache (Rust Build Cache) + +sccache caches Rust compilation artifacts, reducing incremental build times by 30-60%. + +**Installation** (run once on runner): +```bash +cd /path/to/ColdVox +just setup-sccache +``` + +**Verification**: +```bash +sccache --version +sccache --show-stats +``` + +**Location**: `~/.cargo/bin/sccache` +**Cache**: `~/.cache/sccache` + +The CI workflow automatically detects and uses sccache if available. No additional configuration needed after installation. + +#### just (Command Runner) + +The project uses `just` as a command runner for development tasks. + +**Installation**: +```bash +cargo install just --locked +``` + +**One-time setup** (installs all dev tools including sccache): +```bash +just setup +``` + --- ## Complete Workflow Files diff --git a/justfile b/justfile index 69cd6268..20370f4e 100644 --- a/justfile +++ b/justfile @@ -15,11 +15,13 @@ ci: check: pre-commit run --all-files -# Quick development checks (format, clippy, check) +# Quick development checks (format, clippy, check, security) lint: cargo fmt --all - cargo clippy --fix --all-targets --locked --allow-dirty --allow-staged -- -D warnings + cargo clippy --fix --all-targets --locked --allow-dirty --allow-staged cargo check --workspace --all-targets --locked + cargo deny check + cargo audit # Auto-fix linter issues where possible fix: @@ -83,3 +85,38 @@ tui: # Run mic probe utility mic-probe duration="30": cd crates/app && cargo run --bin mic_probe -- --duration {{duration}} + +# Setup sccache for faster Rust builds (idempotent - safe to run multiple times) +setup-sccache: + #!/usr/bin/env bash + set -euo pipefail + if command -v sccache >/dev/null 2>&1; then + echo "✓ sccache already installed: $(command -v sccache)" + sccache --version + elif [[ -x "$HOME/.cargo/bin/sccache" ]]; then + echo "✓ sccache found at ~/.cargo/bin/sccache" + "$HOME/.cargo/bin/sccache" --version + else + echo "Installing sccache..." + cargo install sccache --locked + echo "✓ sccache installed" + fi + # Export for current shell (caller may need to source or re-eval) + echo "To enable: export RUSTC_WRAPPER=sccache" + +# Setup all development tools (run once after clone) +setup: setup-hooks setup-sccache + @echo "✓ Development environment ready" + +# Install Moonshine Python dependencies (transformers, torch, librosa via uv) +setup-moonshine: + ./scripts/install-moonshine-deps.sh + +# Build with Moonshine STT backend enabled +build-moonshine: setup-moonshine + cargo build --workspace --locked --features moonshine + +# Run Moonshine verification example +verify-moonshine: setup-moonshine + cargo run -p coldvox-stt --example verify_moonshine --features moonshine + diff --git a/mise.toml b/mise.toml index c6763d3a..886e9c09 100644 --- a/mise.toml +++ b/mise.toml @@ -41,8 +41,7 @@ description = "Format & Lint Rust (Cargo)" run = """ cargo fmt -- # --allow-dirty is required because hooks run on dirty states by definition -cargo clippy --fix --allow-dirty --allow-staged --all-targets --locked -- -D warnings -""" +cargo clippy --fix --allow-dirty --allow-staged --all-targets --locked """ [tasks."check:lockfiles"] description = "Prevent Lockfile Drift" diff --git a/scripts/local_ci.sh b/scripts/local_ci.sh index 7264ec34..9bc299f8 100755 --- a/scripts/local_ci.sh +++ b/scripts/local_ci.sh @@ -44,7 +44,7 @@ fi # 2. Run clippy print_step "Running Clippy lints..." -if cargo clippy --all-targets --locked -- -D warnings; then +if cargo clippy --all-targets --locked; then print_success "Clippy checks passed" else print_error "Clippy checks failed" @@ -60,6 +60,15 @@ else exit 1 fi +# 3.5 Security checks +print_step "Running security checks..." +if cargo deny check && cargo audit; then + print_success "Security checks passed" +else + print_error "Security checks failed" + exit 1 +fi + # 4. Build print_step "Building workspace..." if cargo build --workspace --locked; then diff --git a/toolEditResearch.md b/toolEditResearch.md new file mode 100644 index 00000000..4fb19d6b --- /dev/null +++ b/toolEditResearch.md @@ -0,0 +1,55 @@ +# Research: Change-Scoped File Editing for AI Agents + +## Problem Statement + +Current AI agent file editing tools are **file-scoped**: they operate on one file at a time. Many editing operations are **change-scoped**: a single logical change that applies across multiple files. + +This mismatch forces N sequential tool calls for what is conceptually one operation. + +## Example + +Remove the string `-- -D warnings` from 5 files: +- justfile +- mise.toml +- .pre-commit-config.yaml +- scripts/local_ci.sh +- AGENTS.md + +**Current approach:** 5 Edit tool calls, each requiring the agent to specify file path, old string, new string. + +**Desired approach:** 1 operation specifying the change, applied wherever it matches. + +## Research Questions + +1. **What tools exist today** that support change-scoped editing (one command, multiple files)? + - CLI tools (sd, sed, fastmod, codemod, ast-grep, etc.) + - IDE features (VSCode multi-file replace, JetBrains structural replace) + - MCP servers or agent plugins + +2. **What do agents already have access to?** + - Most agents can execute shell commands + - What's the lowest-friction path to change-scoped editing using existing capabilities? + +3. **What's missing?** + - Is the gap tooling (need new tools)? + - Or awareness (tools exist, agents don't use them)? + - Or instructions (agents are told to use file-scoped tools)? + +4. **What would a good solution look like?** + - Single operation interface + - Works across file types + - Provides preview/dry-run + - Reports what changed + - Portable across agents (not tied to one vendor) + +## Constraints + +- Should work for any MCP-capable agent, not just Claude Code +- Prefer existing tools over building new ones +- Must be trivially installable or already present on dev machines + +## Deliverables + +1. Ranked list of existing solutions with pros/cons +2. Recommendation for the simplest path forward +3. If new tooling is needed: minimal spec for an MCP server or CLI wrapper