diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 98f9b315..ed925dfb 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,24 +1,28 @@ # Fortress Rollback Development Container # -# This container includes all verification tools: +# Optimized for fast builds using cargo-binstall (pre-built binaries). +# Full build: ~3-5 min (vs ~25 min compiling from source). +# With pre-built image from GHCR: ~30 seconds (pull only). +# +# Tools included: # - TLA+ model checker # - Kani bounded model checker # - Miri (undefined behavior detection) # - Z3 theorem prover -# - Loom (concurrency testing via Cargo) # - Various cargo tools for testing, coverage, and profiling FROM mcr.microsoft.com/devcontainers/rust:2-1-trixie # Use bash for all RUN commands to ensure consistent shell behavior -# This prevents issues with bash-specific syntax (like process substitution) SHELL ["/bin/bash", "-c"] # Avoid prompts from apt ENV DEBIAN_FRONTEND=noninteractive -# Install system dependencies -RUN apt-get update && apt-get install -y \ +# ============================================================================ +# Layer 1: System dependencies (apt) +# ============================================================================ +RUN apt-get update && apt-get install -y --no-install-recommends \ # Build essentials build-essential \ cmake \ @@ -32,7 +36,6 @@ RUN apt-get update && apt-get install -y \ # Java (for TLA+) default-jdk \ # macroquad example dependencies (graphics, audio, input) - # See: https://github.com/not-fl3/macroquad and examples/README.md libasound2-dev \ libx11-dev \ libxi-dev \ @@ -67,38 +70,61 @@ RUN apt-get update && apt-get install -y \ htop \ ncdu \ silversearcher-ag \ - # Note: tldr is not available in Debian Trixie, installed via cargo below - # Note: eza, git-delta, dust, duf, procs, sd, etc. are installed via cargo below && rm -rf /var/lib/apt/lists/* # Install linux-perf if available (architecture dependent) -RUN apt-get update && apt-get install -y linux-perf 2>/dev/null || true && rm -rf /var/lib/apt/lists/* +RUN apt-get update && \ + if apt-get install -y --no-install-recommends linux-perf; then \ + echo "linux-perf: installed"; \ + else \ + echo "linux-perf: skipped (not available for this architecture)"; \ + fi && \ + rm -rf /var/lib/apt/lists/* -# Set up TLA+ tools +# ============================================================================ +# Layer 2: Direct downloads (TLA+, actionlint, Vale) +# ============================================================================ RUN mkdir -p /opt/tla && \ - curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o /opt/tla/tla2tools.jar + curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar \ + -o /opt/tla/tla2tools.jar +# Note: devcontainer.json containerEnv overrides this to the workspace copy ENV TLA_TOOLS_JAR=/opt/tla/tla2tools.jar # Install Z3 and yamllint via pip -RUN pip3 install --break-system-packages z3-solver yamllint +RUN pip3 install --no-cache-dir --break-system-packages z3-solver yamllint # Install actionlint (GitHub Actions linter) -# Download the script first, then run with bash (avoids process substitution issues with /bin/sh) -RUN curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o /tmp/download-actionlint.bash && \ +RUN curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash \ + -o /tmp/download-actionlint.bash && \ bash /tmp/download-actionlint.bash latest /usr/local/bin && \ rm /tmp/download-actionlint.bash && \ chmod +x /usr/local/bin/actionlint -# Install Vale (prose linter) -# Vale is used to check documentation for prose quality +# Install Vale (prose linter) - architecture aware RUN VALE_VERSION="3.9.3" && \ - curl -sL "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_64-bit.tar.gz" -o /tmp/vale.tar.gz && \ + ARCH="$(uname -m)" && \ + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then VALE_ARCH="arm64"; else VALE_ARCH="64-bit"; fi && \ + curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_${VALE_ARCH}.tar.gz" \ + -o /tmp/vale.tar.gz && \ tar -xzf /tmp/vale.tar.gz -C /usr/local/bin vale && \ rm /tmp/vale.tar.gz && \ chmod +x /usr/local/bin/vale -# Switch to vscode user for remaining installations +# ============================================================================ +# Layer 3: Node.js and linting tools +# ============================================================================ +RUN curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh && \ + bash /tmp/nodesource_setup.sh && \ + rm /tmp/nodesource_setup.sh && \ + apt-get install -y --no-install-recommends nodejs && \ + npm install -g markdownlint-cli markdown-link-check && \ + pip3 install --no-cache-dir --break-system-packages pre-commit && \ + rm -rf /var/lib/apt/lists/* + +# ============================================================================ +# Layer 4: Rust toolchains and components (as vscode user) +# ============================================================================ USER vscode # Install Rust nightly toolchain (required for Miri and some Kani features) @@ -109,57 +135,76 @@ RUN rustup install nightly && \ # Install Miri (undefined behavior detector) RUN rustup +nightly component add miri -# Install Kani verifier -# Note: Kani requires its own setup step which downloads CBMC +# ============================================================================ +# Layer 5: cargo-binstall (enables fast pre-built binary downloads) +# ~10 seconds instead of ~18 minutes compiling from source +# ============================================================================ +ENV BINSTALL_DISABLE_TELEMETRY=true +RUN set -e && \ + ARCH="$(uname -m)" && \ + CARGO_BIN_DIR="${CARGO_HOME:-$HOME/.cargo}/bin" && \ + if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then \ + BINSTALL_ARCH="aarch64-unknown-linux-musl"; \ + else \ + BINSTALL_ARCH="x86_64-unknown-linux-musl"; \ + fi && \ + mkdir -p "$CARGO_BIN_DIR" && \ + echo "Installing cargo-binstall for ${BINSTALL_ARCH}" && \ + curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 "https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-${BINSTALL_ARCH}.tgz" \ + -o /tmp/cargo-binstall.tgz && \ + tar -xzf /tmp/cargo-binstall.tgz -C "$CARGO_BIN_DIR" && \ + rm /tmp/cargo-binstall.tgz && \ + cargo-binstall -V + +# ============================================================================ +# Layer 6: Verification & testing tools (via binstall - pre-built binaries) +# These are the most critical tools, installed first for cache stability +# ============================================================================ +RUN for tool in cargo-nextest cargo-audit cargo-deny cargo-tarpaulin cargo-llvm-cov cargo-mutants cargo-careful flamegraph; do \ + cargo binstall --no-confirm "$tool" \ + || echo "$tool: failed to install"; \ + done + +# ============================================================================ +# Layer 7: Development workflow tools (via binstall) +# ============================================================================ +RUN for tool in cargo-watch sccache cargo-shear; do \ + cargo binstall --no-confirm "$tool" \ + || echo "$tool: failed to install"; \ + done + +# ============================================================================ +# Layer 8: High-performance CLI tools (via binstall) +# These are optional -- each tool installs independently so one failure +# does not block the others. +# ============================================================================ +RUN for tool in eza git-delta du-dust duf procs sd hyperfine tokei zoxide tealdeer bottom; do \ + cargo binstall --no-confirm "$tool" \ + || echo "$tool: skipped (no prebuilt binary)"; \ + done + +# ============================================================================ +# Layer 9: Kani verifier (compiled from source - no pre-built binary available) +# This is the heaviest layer (~3-4 min). Cached unless kani version changes. +# ============================================================================ RUN cargo install --locked kani-verifier && \ - cargo kani setup || echo "Kani setup may need to complete on first use" - -# Install cargo tools for testing and verification -RUN cargo install cargo-audit && \ - cargo install cargo-deny && \ - cargo install cargo-tarpaulin && \ - cargo install cargo-llvm-cov && \ - cargo install cargo-mutants && \ - cargo install cargo-fuzz || echo "cargo-fuzz requires nightly" && \ - cargo install flamegraph && \ - cargo install cargo-nextest && \ - cargo install cargo-watch && \ - cargo install sccache && \ - # CI/CD linting and quality tools - cargo install cargo-shear && \ - cargo install cargo-spellcheck && \ - cargo install cargo-geiger && \ - cargo install cargo-careful && \ - # High-performance CLI tools (Rust-based, not available in Debian apt) - cargo install eza && \ - cargo install git-delta && \ - cargo install du-dust && \ - cargo install duf && \ - cargo install procs && \ - cargo install sd && \ - cargo install hyperfine && \ - cargo install tokei && \ - cargo install zoxide && \ - cargo install tealdeer && \ - cargo install bottom || echo "bottom (btm) optional" - -# Install Node.js and markdown/linting tools (for CI/CD and pre-commit hooks) -# Note: Download and run as separate steps to avoid shell compatibility issues -USER root -RUN curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh && \ - bash /tmp/nodesource_setup.sh && \ - rm /tmp/nodesource_setup.sh && \ - apt-get install -y nodejs && \ - npm install -g markdownlint-cli markdown-link-check && \ - pip3 install --break-system-packages pre-commit && \ - rm -rf /var/lib/apt/lists/* -USER vscode + (cargo kani setup || echo "Kani setup may need to complete on first use") + +# ============================================================================ +# Layer 10: Remaining source-compiled tools (each isolated for cache stability) +# ============================================================================ +RUN cargo install cargo-fuzz || echo "cargo-fuzz requires nightly" + +RUN cargo install cargo-geiger || echo "cargo-geiger optional" + +RUN cargo install cargo-spellcheck || echo "cargo-spellcheck optional" -# Set environment variables +# ============================================================================ +# Layer 11: Environment and helper scripts +# ============================================================================ ENV RUST_BACKTRACE=1 ENV CARGO_INCREMENTAL=0 -# Reset to root for any final setup USER root # Create helper script for running TLA+ @@ -173,13 +218,13 @@ RUN echo '#!/bin/bash\njava -cp /opt/tla/tla2tools.jar tla2sany.SANY "$@"' > /us # Switch back to vscode user USER vscode -WORKDIR /workspaces/ggrs +WORKDIR /workspaces/fortress-rollback # Set up aliases for high-performance tools # Note: fd-find installs as 'fdfind' on Debian, bat as 'batcat' -# eza, delta, dust, duf, procs are installed via cargo and have standard names +# eza, delta, dust, duf, procs are installed via binstall and have standard names # tealdeer installs as 'tldr' -RUN echo '\n# High-performance tool aliases\nalias fd="fdfind"\nalias bat="batcat"\nalias ls="eza"\nalias ll="eza -la"\nalias la="eza -la"\nalias tree="eza --tree"\nalias cat="batcat --paging=never"\nalias diff="delta"\nalias du="dust"\nalias df="duf"\nalias ps="procs"\nalias top="htop"\nalias sed="sd"\n\n# Initialize zoxide (smart cd)\neval "$(zoxide init bash)"\n\n# Conditionally enable sccache if available\nif command -v sccache &> /dev/null; then\n export RUSTC_WRAPPER=sccache\nfi\n' >> ~/.bashrc +RUN echo '\n# High-performance tool aliases\nalias fd="fdfind"\nalias bat="batcat"\nalias cat="batcat --paging=never"\nalias top="htop"\n\n# Optional tool aliases (installed via binstall; may not be present)\ncommand -v eza >/dev/null 2>&1 && alias ls="eza"\ncommand -v eza >/dev/null 2>&1 && alias ll="eza -la"\ncommand -v eza >/dev/null 2>&1 && alias la="eza -la"\ncommand -v eza >/dev/null 2>&1 && alias tree="eza --tree"\ncommand -v delta >/dev/null 2>&1 && alias diff="delta"\ncommand -v dust >/dev/null 2>&1 && alias du="dust"\ncommand -v duf >/dev/null 2>&1 && alias df="duf"\ncommand -v procs >/dev/null 2>&1 && alias ps="procs"\n\n# Initialize zoxide (smart cd) if available\ncommand -v zoxide >/dev/null 2>&1 && eval "$(zoxide init bash)"\n\n# Conditionally enable sccache if available\nif command -v sccache >/dev/null 2>&1; then\n export RUSTC_WRAPPER=sccache\nfi\n' >> ~/.bashrc # Print tool versions on first login (via .bashrc) -RUN echo '\n# Fortress Rollback Development Environment\nif [ -f /workspaces/ggrs/.devcontainer/welcome.sh ]; then\n source /workspaces/ggrs/.devcontainer/welcome.sh\nfi' >> ~/.bashrc +RUN echo '\n# Fortress Rollback Development Environment\nif [ -f /workspaces/fortress-rollback/.devcontainer/welcome.sh ]; then\n source /workspaces/fortress-rollback/.devcontainer/welcome.sh\nfi' >> ~/.bashrc diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 1a1f6a9a..0363394f 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -59,6 +59,38 @@ After the container starts, verify all tools: ./scripts/check-tools.sh ``` +## Build Performance + +The Dockerfile uses **cargo-binstall** to download pre-built binaries instead of +compiling tools from source, reducing build time from ~25 minutes to ~3-5 minutes. + +| Method | Build Time | When to Use | +|--------|-----------|-------------| +| **Pre-built image (GHCR)** | ~30 seconds | Optional fast path when the image is available | +| **Local Dockerfile build** | ~3-5 min | When modifying the Dockerfile | +| **Without binstall (old)** | ~25-30 min | N/A (deprecated) | + +### Using the pre-built image (fastest) + +The local Dockerfile build remains the default path until the published image has been validated across the environments this project supports. + +In `devcontainer.json`, comment out the `"build"` block and uncomment the `"image"` line: + +```jsonc +// "build": { ... }, +"image": "ghcr.io/wallstop/fortress-rollback/devcontainer:latest", +``` + +The image is automatically rebuilt weekly and on changes to `.devcontainer/` files. + +> **Note:** The pre-built GHCR image is x86_64 (amd64) only. Apple Silicon (arm64) +> users should use the local Dockerfile build, which includes architecture detection. + +### Building locally + +The default `devcontainer.json` builds from the Dockerfile. Docker layer caching +means subsequent rebuilds only reprocess changed layers. + ## Running Verification ### All Verifiers @@ -129,7 +161,7 @@ npm install -g markdownlint-cli markdown-link-check # actionlint (GitHub Actions linter) # Download the script first, then run with bash (avoids shell issues) -curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o /tmp/download-actionlint.bash +curl --proto '=https' --tlsv1.2 -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash -o /tmp/download-actionlint.bash bash /tmp/download-actionlint.bash latest /tmp sudo mv /tmp/actionlint /usr/local/bin/ rm /tmp/download-actionlint.bash @@ -151,10 +183,10 @@ If you see this error in other scripts, ensure they are run with bash explicitly ```bash # Instead of piping to sh -curl -sL https://example.com/script.sh | bash +curl --proto '=https' --tlsv1.2 -sSfL https://example.com/script.sh | bash # Download first, then run with bash -curl -sL https://example.com/script.sh -o /tmp/script.sh +curl --proto '=https' --tlsv1.2 -sSfL https://example.com/script.sh -o /tmp/script.sh bash /tmp/script.sh rm /tmp/script.sh ``` @@ -173,7 +205,7 @@ Try removing old containers and images: ```bash # List and remove devcontainer images -docker images | grep vsc-ggrs | awk '{print $3}' | xargs -r docker rmi -f +docker images | grep fortress-rollback | awk '{print $3}' | xargs -r docker rmi -f # Or prune all unused images docker system prune -a @@ -202,7 +234,7 @@ Download manually: ```bash mkdir -p .tla-tools -curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .tla-tools/tla2tools.jar +curl --proto '=https' --tlsv1.2 -sSfL https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .tla-tools/tla2tools.jar ``` ## Files diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c0caac0a..6d2cc07d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,11 +2,20 @@ // README at: https://github.com/devcontainers/templates/tree/main/src/rust { "name": "Fortress Rollback - Full Verification Environment", - // Use our custom Dockerfile with all verification tools pre-installed + + // ===== BUILD OPTIONS (choose one) ===== + // + // Option A: Build locally from Dockerfile (first build ~3-5 min, rebuilds use cache) "build": { "dockerfile": "Dockerfile", "context": ".." }, + // + // Option B: Use a pre-built image from GHCR when available (~30 seconds, pull only) + // Local Dockerfile builds remain the default until the registry path is validated across environments. + // Uncomment the line below and comment out the "build" block above to use it. + // "image": "ghcr.io/wallstop/fortress-rollback/devcontainer:latest", + // Additional features (on top of what's in Dockerfile) "features": { "ghcr.io/devcontainers/features/powershell:1": {} @@ -18,8 +27,8 @@ "type": "volume" } ], - // Post-create setup (ensure everything is ready) - "postCreateCommand": "bash -c '\n set -e\n echo \"=== Setting up Fortress Rollback Development Environment ===\"\n \n # Ensure TLA+ tools are in place\n mkdir -p .tla-tools\n if [ -f /opt/tla/tla2tools.jar ]; then\n cp /opt/tla/tla2tools.jar .tla-tools/\n elif [ ! -f .tla-tools/tla2tools.jar ]; then\n echo \"Downloading TLA+ tools...\"\n curl -L https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .tla-tools/tla2tools.jar\n fi\n \n # Sync Vale styles (prose linter)\n if command -v vale > /dev/null 2>&1 && [ -f .vale.ini ]; then\n echo \"Syncing Vale packages...\"\n vale sync || echo \"Vale sync failed, run vale sync manually\"\n fi\n \n # Install Rust nightly if missing\n if ! rustup run nightly rustc --version > /dev/null 2>&1; then\n echo \"Installing Rust nightly...\"\n rustup install nightly\n rustup component add rust-src --toolchain nightly\n rustup component add llvm-tools-preview --toolchain nightly\n fi\n \n # Install Miri if missing\n if ! rustup +nightly which miri > /dev/null 2>&1; then\n echo \"Installing Miri...\"\n rustup +nightly component add miri || true\n fi\n \n # Install Kani if missing (this can take a while)\n if ! command -v cargo-kani > /dev/null 2>&1; then\n echo \"Installing Kani verifier (this may take a few minutes)...\"\n cargo install --locked kani-verifier || echo \"Kani install failed, install manually with: cargo install --locked kani-verifier && cargo kani setup\"\n cargo kani setup || echo \"Run cargo kani setup to complete Kani installation\"\n fi\n \n # Install cargo tools if missing\n command -v cargo-nextest > /dev/null 2>&1 || cargo install cargo-nextest || true\n command -v cargo-audit > /dev/null 2>&1 || cargo install cargo-audit || true\n command -v cargo-deny > /dev/null 2>&1 || cargo install cargo-deny || true\n command -v cargo-llvm-cov > /dev/null 2>&1 || cargo install cargo-llvm-cov || true\n command -v cargo-mutants > /dev/null 2>&1 || cargo install cargo-mutants || true\n \n # Install Z3 if missing\n if ! python3 -c \"import z3\" 2>/dev/null; then\n echo \"Installing Z3...\"\n pip3 install --break-system-packages z3-solver || pip3 install z3-solver || true\n fi\n \n # Build project to cache dependencies\n cargo build 2>/dev/null || true\n \n echo \"=== Development environment ready! ===\"\n echo \"Run ./scripts/check-tools.sh to verify all tools are installed.\"\n'", + // Post-create setup: only workspace-specific tasks (tools are pre-installed in image) + "postCreateCommand": "bash -c '\n set -e\n echo \"=== Setting up Fortress Rollback Development Environment ===\"\n \n # Ensure TLA+ tools are in the workspace\n mkdir -p .tla-tools\n if [ -f /opt/tla/tla2tools.jar ]; then\n cp /opt/tla/tla2tools.jar .tla-tools/\n elif [ ! -f .tla-tools/tla2tools.jar ]; then\n echo \"Downloading TLA+ tools...\"\n curl --proto \"=https\" --tlsv1.2 -fsSL --retry 5 --retry-delay 2 --retry-all-errors --max-time 120 https://github.com/tlaplus/tlaplus/releases/download/v1.8.0/tla2tools.jar -o .tla-tools/tla2tools.jar\n fi\n \n # Sync Vale styles (prose linter)\n if command -v vale >/dev/null 2>&1 && [ -f .vale.ini ]; then\n echo \"Syncing Vale packages...\"\n vale sync || echo \"Vale sync failed, run vale sync manually\"\n fi\n \n # Build project to warm dependency cache\n echo \"Warming dependency cache...\"\n if ! cargo build; then\n echo \"WARNING: Initial cargo build failed. Run cargo build manually to see errors.\"\n fi\n \n echo \"=== Development environment ready! ===\"\n echo \"Run ./scripts/check-tools.sh to verify all tools are installed.\"\n'", // Environment variables for tools "containerEnv": { "TLA_TOOLS_JAR": "${containerWorkspaceFolder}/.tla-tools/tla2tools.jar", diff --git a/.devcontainer/welcome.sh b/.devcontainer/welcome.sh index b4552626..b1a468f9 100755 --- a/.devcontainer/welcome.sh +++ b/.devcontainer/welcome.sh @@ -3,7 +3,7 @@ # Only run once per terminal session if [ -n "$FORTRESS_WELCOME_SHOWN" ]; then - return 2>/dev/null || exit 0 + return 2>/dev/null || exit 0 # /dev/null needed: suppress error when executed (not sourced) fi export FORTRESS_WELCOME_SHOWN=1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3a3d084c..70d3b3de 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,36 +1,3 @@ # GitHub Copilot Instructions for Fortress Rollback -**Read [`.llm/context.md`](../.llm/context.md)** — the canonical source of truth for all project context, development policies, testing guidelines, and coding standards. - -## Critical: Zero-Panic Policy - -**All production code must follow defensive programming practices.** See [`.llm/skills/defensive-programming.md`](../.llm/skills/defensive-programming.md) for the complete guide. - -Key requirements: - -- **Never panic** — No `unwrap()`, `expect()`, `panic!()`, `todo!()` -- **Return `Result`** — All fallible operations must return errors, not panic -- **Never swallow errors** — Propagate or explicitly handle, never ignore -- **Validate everything** — Don't assume inputs or internal state are valid - -## Quick Commands - -```bash -cargo fmt && cargo clippy --all-targets && cargo nextest run --no-capture - -# Or use the convenient aliases defined in .cargo/config.toml -cargo c && cargo t - -# Markdown linting -npx markdownlint '**/*.md' --config .markdownlint.json --fix -``` - -**Always use `--no-capture`** (nextest) or `-- --nocapture` (cargo test) so that test output is visible immediately when failures occur. The aliases include this by default. - -## Changelog Reminder - -After code changes, ask: **Does this affect `pub` items or user-observable behavior?** - -- If YES → Add entry to `CHANGELOG.md` under `## [Unreleased]` (use `**Breaking:**` prefix for API changes) -- If NO (`pub(crate)`, private, tests, CI) → No changelog needed -- See [changelog-practices.md](../.llm/skills/changelog-practices.md) for detailed guidance +**Read and follow [`.llm/context.md`](../.llm/context.md)** — the canonical source of truth for all project context, development policies, testing guidelines, and coding standards. You must read it before making any changes. diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 392d676d..fba0a0e4 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -12,6 +12,7 @@ on: - 'scripts/check-wiki-consistency.py' - 'scripts/validate-wiki-output.py' - 'scripts/tests/test_sync_wiki.py' + - 'scripts/tests/test_check_wiki_consistency.py' - '.markdownlint.json' - '.markdown-link-check.json' - '.lychee.toml' @@ -32,6 +33,7 @@ on: - 'scripts/check-wiki-consistency.py' - 'scripts/validate-wiki-output.py' - 'scripts/tests/test_sync_wiki.py' + - 'scripts/tests/test_check_wiki_consistency.py' - '.markdownlint.json' - '.markdown-link-check.json' - '.lychee.toml' @@ -195,6 +197,9 @@ jobs: - name: Run wiki validation tests run: python -m pytest scripts/tests/test_validate_wiki_output.py -v + - name: Run wiki consistency tests + run: python -m pytest scripts/tests/test_check_wiki_consistency.py -v + - name: Generate wiki (dry-run) run: python scripts/sync-wiki.py --dest wiki-test-output diff --git a/.github/workflows/ci-lint.yml b/.github/workflows/ci-lint.yml index a5c4f28b..7267d0cb 100644 --- a/.github/workflows/ci-lint.yml +++ b/.github/workflows/ci-lint.yml @@ -8,6 +8,8 @@ on: - 'docker/docker-compose.yml' - '.pre-commit-config.yaml' - '.github/workflows/ci-lint.yml' + - '**/Dockerfile*' + - '.devcontainer/devcontainer.json' pull_request: branches: [main] paths: @@ -15,6 +17,8 @@ on: - 'docker/docker-compose.yml' - '.pre-commit-config.yaml' - '.github/workflows/ci-lint.yml' + - '**/Dockerfile*' + - '.devcontainer/devcontainer.json' workflow_dispatch: jobs: @@ -27,7 +31,7 @@ jobs: - uses: actions/checkout@v6 - name: Install yamllint - run: pip install yamllint + run: pip install --no-cache-dir yamllint - name: Validate YAML files run: | @@ -43,3 +47,6 @@ jobs: - name: Validate GitHub Actions workflows run: actionlint + + - name: Check Dockerfile best practices + run: python scripts/hooks/check-dockerfile.py diff --git a/.github/workflows/ci-llm-lint.yml b/.github/workflows/ci-llm-lint.yml new file mode 100644 index 00000000..4a496e28 --- /dev/null +++ b/.github/workflows/ci-llm-lint.yml @@ -0,0 +1,23 @@ +name: LLM Context Lint + +on: + push: + paths: ['.llm/**'] + pull_request: + paths: ['.llm/**'] + +jobs: + llm-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Both scripts use stdlib only (pathlib, re, sys) — no pip install needed + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Check .llm file line limit (300 max) + run: python scripts/hooks/check-llm-line-limit.py + - name: Verify skills index is up-to-date + run: python scripts/hooks/regenerate-skills-index.py --check + - name: Check skill code example quality + run: bash scripts/check-llm-skills.sh diff --git a/.github/workflows/devcontainer-build.yml b/.github/workflows/devcontainer-build.yml new file mode 100644 index 00000000..f5a45c9e --- /dev/null +++ b/.github/workflows/devcontainer-build.yml @@ -0,0 +1,75 @@ +# Pre-build the devcontainer image and push to GitHub Container Registry (GHCR). +# +# This allows developers to pull a pre-built image (~30 seconds) instead of +# building locally (~3-5 min with binstall, or ~25 min without). +# +# To use the pre-built image, update devcontainer.json: +# "image": "ghcr.io/wallstop/fortress-rollback/devcontainer:latest" +# +# The image is rebuilt: +# - On pushes to main that change .devcontainer/ files +# - Weekly (Sundays at 2am UTC) to pick up tool updates +# - Manually via workflow_dispatch + +name: Build DevContainer Image + +on: + push: + branches: [main] + paths: + - '.devcontainer/**' + - '.github/workflows/devcontainer-build.yml' + schedule: + # Weekly rebuild on Sundays at 2am UTC + - cron: '0 2 * * 0' + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/devcontainer + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix= + type=schedule,pattern={{date 'YYYYMMDD'}} + + - name: Build and push image + uses: docker/build-push-action@v7 + with: + context: . + file: .devcontainer/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + # Build for x86_64 (primary CI/Codespaces arch) + # Add linux/arm64 if needed for Apple Silicon users + platforms: linux/amd64 diff --git a/.llm/context.md b/.llm/context.md index f001d9ce..3e0e0fe5 100644 --- a/.llm/context.md +++ b/.llm/context.md @@ -1,332 +1,178 @@ -# Fortress Rollback — LLM Development Guide +# Fortress Rollback -- LLM Development Guide -> **This is the canonical source of truth** for project context. All other LLM instruction files (CLAUDE.md, AGENTS.md, copilot-instructions.md) point here. +> **Canonical source of truth** for project context. All other LLM instruction files point here. -## TL;DR — What You Need to Know +## TL;DR -**Fortress Rollback** is a correctness-first fork of GGRS (Good Game Rollback System), written in 100% safe Rust. It provides peer-to-peer rollback networking for deterministic multiplayer games. +**Fortress Rollback** is a correctness-first fork of GGRS, written in 100% safe Rust. Peer-to-peer rollback networking for deterministic multiplayer games. -### The Five Pillars +### Five Pillars -1. **Zero-panic production code** — All errors returned as `Result`, never panic -2. **>90% test coverage** — All code must be thoroughly tested -3. **Formal verification** — TLA+, Z3, and Kani for critical components -4. **Enhanced usability** — Intuitive, type-safe, hard-to-misuse APIs -5. **Code clarity** — Readable, maintainable, well-documented +1. **Zero-panic production code** -- All errors returned as `Result`, never panic +2. **>90% test coverage** -- All code must be thoroughly tested +3. **Formal verification** -- TLA+, Z3, and Kani for critical components +4. **Enhanced usability** -- Intuitive, type-safe, hard-to-misuse APIs +5. **Code clarity** -- Readable, maintainable, well-documented -### Quick Commands +## Quick Commands ```bash -# Run after every change -cargo fmt && cargo clippy --all-targets && cargo nextest run --no-capture - -# Aliases from .cargo/config.toml -cargo c && cargo t - -# Additional checks -typos # Spell check (CI enforced) +cargo fmt && cargo clippy --all-targets && cargo nextest run --no-capture # Pre-commit +cargo c && cargo t # Aliases from .cargo/config.toml +typos # Spell check (CI enforced) cargo test --features z3-verification -- --nocapture # Z3 proofs (slow) ``` -**Always use `--no-capture`** (nextest) or `-- --nocapture` (cargo test) so that test output is visible immediately when failures occur. This avoids having to re-run tests to see what went wrong. - ---- +Always use `--no-capture` (nextest) or `-- --nocapture` (cargo test) so test output is visible on failure. ## CLI Tools (Dev Container) -Use modern tools instead of traditional counterparts. Shell aliases are pre-configured. - | Use | Instead of | Key flags | |-----|------------|-----------| | `rg` | `grep` | `-l` list files, `-C 3` context, `--type rust` | -| `fd` | `find` | `-e toml` extension, `--type d` dirs, `-x cmd {}` exec | -| `bat --paging=never` | `cat` | `-n` line numbers, `-r 10:20` range, `-l rust` language | +| `fd` | `find` | `-e toml` extension, `--type d` dirs | +| `bat --paging=never` | `cat` | `-n` line numbers, `-r 10:20` range | | `eza` | `ls` | `-la`, `--tree`, `--git` | | `sd` | `sed` | `sd 'old' 'new' file`, `-F` literal | -| `dust` | `du` | `-d 2` depth, `-n 20` top entries | -| `procs` | `ps` | `--tree`, `procs cargo` filter | +| `dust` | `du` | `-d 2` depth | | `tokei` | `wc -l` | Code stats by language | | `hyperfine` | `time` | Statistical benchmarking | -**Critical rules:** - -- Always use `bat --paging=never` (bare `bat` blocks) -- Never redirect to `/dev/null` — hides errors -- Use `rg` with `--no-ignore` to include gitignored files - ---- - -## How to Approach Development +Rules: always `bat --paging=never` (bare `bat` blocks); never redirect to `/dev/null`; use `rg --no-ignore` for gitignored files. -### Before Writing Any Code +## Non-Negotiable Requirements -1. **Understand the context** — Read relevant source files and tests -2. **Check for similar patterns** — See how existing code handles similar cases -3. **Consider the impact** — Will this change affect other components? -4. **Plan tests first** — What tests will verify correctness? +- **100% safe Rust** -- `#![forbid(unsafe_code)]` +- **ZERO-PANIC POLICY** -- Production code must NEVER panic; all errors as `Result` +- **All clippy lints pass** -- `clippy::all`, `clippy::pedantic`, `clippy::nursery` +- **No broken doc links** -- All intra-doc links must resolve +- **Public items documented** -- Rustdoc with examples +- **Overflow checks in release** -- Integer overflow caught at runtime +- **Deterministic behavior** -- Same inputs must always produce same outputs -### When Implementing Features - -1. **Write tests first** (TDD) — Define expected behavior before implementing -2. **Keep functions focused** — Single responsibility, clear intent -3. **Handle all errors** — No panics, use `Result` -4. **Document as you go** — Rustdoc with examples for all public items -5. **Consider edge cases** — Zero values, max values, empty collections -6. **Update changelog** — Only for user-facing changes (see Changelog Policy below) - -### When Fixing Bugs - -1. **Reproduce first** — Write a failing test that demonstrates the bug -2. **Root cause analysis** — Understand *why* it fails, not just *what* fails -3. **Fix at the right level** — Production bug vs test bug (see below) -4. **Add regression tests** — Ensure the bug can't return -5. **Check for similar issues** — Are there related bugs elsewhere? - ---- +```rust +// FORBIDDEN in production: value.unwrap(), .expect(), array[i], panic!(), todo!() +// REQUIRED: value.ok_or(FortressError::MissingValue)?, array.get(i).ok_or(...)?, +// if !valid { return Err(FortressError::InvalidState); } +``` -## Code Quality Standards +Zero-panic key principles: never swallow errors (use `?`), validate all inputs, prefer `.get()` over indexing, exhaustive `match` (no `_ =>` on enums), enums over booleans, doc examples must use `?` and `Result`. See [defensive-programming.md](skills/defensive-programming.md) for the complete guide. -### Non-Negotiable Requirements +## Code Design Principles -- **100% safe Rust** — `#![forbid(unsafe_code)]` -- **ZERO-PANIC POLICY** — Production code must NEVER panic; all errors as `Result` -- **All clippy lints pass** — `clippy::all`, `clippy::pedantic`, `clippy::nursery` -- **No broken doc links** — All intra-doc links must resolve -- **Public items documented** — Rustdoc with examples -- **Overflow checks in release** — Integer overflow is caught at runtime -- **Deterministic behavior** — Same inputs must always produce same outputs +Follow SOLID, DRY, and Clean Architecture. Rely on descriptive names; comment only the "why." Prefer zero-cost abstractions (generics/traits over dynamic dispatch), value types, and minimal allocations. -### Code Design Principles +Design patterns used: **Builder** (SessionBuilder), **State Machine** (protocol/connection states), **Strategy** (input prediction), **Iterator** (combinators over manual loops). -These principles apply to **all code** — production, tests, CI/CD, documentation, and examples. +Before writing new code, search for similar existing patterns. Extract shared utilities; avoid copy-paste. -#### Minimal Comments +## Skills Reference -- **Rely on descriptive names** — Function, variable, and type names should be self-documenting -- **Comment only the "why"** — Explain non-obvious design decisions, not what the code does -- **Avoid redundant comments** — If the code is clear, don't add noise -- **Rustdoc is different** — Public API documentation is mandatory and valuable +See [`.llm/skills/_index.md`](skills/_index.md) for the categorized index of deep-dive guides covering: defensive programming, testing (unit/property/mutation/fuzz/chaos), formal verification (Kani/TLA+/Z3/Loom/Miri), rollback netcode, determinism, performance, WASM, CI/CD, API design, documentation, and more. -```rust -// ❌ Avoid: Redundant comment -// Increment the frame counter -frame_counter += 1; +## Project Architecture -// ✅ Prefer: Self-documenting code, no comment needed -frame_counter += 1; +### Repository Structure -// ✅ Good: Explains non-obvious "why" -// Skip checksum validation for spectators to reduce bandwidth -if player.is_spectator() { return Ok(()); } ``` - -#### SOLID Principles - -- **Single Responsibility** — Each module, struct, and function does one thing well -- **Open/Closed** — Extend behavior through traits and generics, not modification -- **Liskov Substitution** — Trait implementations must honor the trait's contract -- **Interface Segregation** — Prefer small, focused traits over large monolithic ones -- **Dependency Inversion** — Depend on abstractions (traits), not concrete types - -#### DRY (Don't Repeat Yourself) - -- **Extract common patterns** — If code appears twice, consider abstracting it -- **Prefer composition** — Build complex behavior from simple, reusable pieces -- **Centralize constants** — Magic numbers and strings belong in named constants -- **Share test utilities** — Common test setup belongs in shared modules - -```rust -// ❌ Avoid: Duplicated validation logic -fn process_input(input: Input) -> Result<(), Error> { - if input.frame < 0 { return Err(Error::InvalidFrame); } - // ... process -} -fn validate_input(input: Input) -> Result<(), Error> { - if input.frame < 0 { return Err(Error::InvalidFrame); } - // ... validate -} - -// ✅ Prefer: Single source of truth -impl Input { - fn validate(&self) -> Result<(), Error> { - if self.frame < 0 { return Err(Error::InvalidFrame); } - Ok(()) - } -} +src/ + lib.rs # Public API entry point + error.rs # FortressError types + frame_info.rs / hash.rs / rle.rs / rng.rs # Core utilities + time_sync.rs / sync.rs / checksum.rs / telemetry.rs + input_queue/ + mod.rs # Input buffering + prediction.rs # Input prediction strategies + sync_layer/ + mod.rs # Core synchronization (SyncLayer) + game_state_cell.rs # Thread-safe game state + saved_states.rs # Circular buffer for rollback + network/ + compression.rs / messages.rs / network_stats.rs + chaos_socket.rs / udp_socket.rs / codec.rs / tokio_socket.rs + protocol/ + mod.rs / event.rs / input_bytes.rs / state.rs + sessions/ + builder.rs # SessionBuilder pattern + p2p_session.rs # P2P gameplay + p2p_spectator_session.rs # Spectator mode + sync_test_session.rs # Determinism testing + config.rs / player_registry.rs / sync_health.rs ``` -#### Clean Architecture +### Session Types -- **Separate concerns** — Keep business logic independent of I/O and frameworks -- **Layer dependencies inward** — Core logic shouldn't know about network or storage details -- **Define clear boundaries** — Use traits to define interfaces between layers +- **P2PSession** -- Standard peer-to-peer gameplay +- **SpectatorSession** -- Observe but don't participate +- **SyncTestSession** -- Verify determinism by running simulation twice -#### Design Patterns +### Critical Determinism Rules -Use established patterns where appropriate: +1. **No `HashMap` iteration** -- Use `BTreeMap` or sort before iterating +2. **Control floating-point** -- Use `libm` feature or fixed-point math +3. **Seeded RNG only** -- `rand_pcg` or `rand_chacha` with shared seed +4. **Frame counters, not time** -- Never use `Instant::now()` in simulation +5. **Sort ECS queries** -- Bevy queries are non-deterministic; sort by stable ID +6. **Pin toolchain** -- Use `rust-toolchain.toml` for reproducible builds +7. **Audit features** -- Check for `ahash`, `const-random` leaks with `cargo tree -f "{p} {f}"` -- **Builder** — For complex object construction (see `SessionBuilder`) -- **State Machine** — For protocol and connection state management -- **Strategy** — For swappable algorithms (e.g., input prediction) -- **Factory** — For creating related objects with consistent configuration -- **Iterator** — Leverage Rust's iterator combinators over manual loops +## Kani Essentials -#### Lightweight Abstractions +**The #1 cause of Kani CI failures:** All loops with symbolic bounds require `#[kani::unwind(N)]` where N = max_iterations + 1. CI uses `--default-unwind 8` via `--quick` mode. -When creating abstractions for common patterns: +**The #2 cause:** Proofs that assert the wrong thing (e.g., wrong enum variant). -- **Prefer value types** — Use `Copy` types and stack allocation when possible -- **Minimize allocations** — Avoid `Box`, `Vec`, `String` in hot paths unless necessary -- **Use zero-cost abstractions** — Generics and traits over dynamic dispatch -- **Function-based over object-based** — Simple functions often beat complex types +**The #3 cause:** `format!()` inside macros (e.g., `report_violation!`) creating explosive CBMC state space. The `report_violation!` macro handles `cfg(kani)` internally (uses `let _ = (args...)` to suppress unused warnings without `format!()`). No additional gating needed when calling it. See [kani.md](skills/kani.md#common-timeout-causes) for details. -```rust -// ❌ Avoid: Unnecessary allocation for simple abstraction -struct FrameValidator { - valid_range: Box bool>, -} +```bash +cargo kani --harness proof_function_name # Run specific proof +./scripts/verify-kani.sh --tier 1 --quick # Fast proofs (~15 min) +./scripts/verify-kani.sh --list # List all proofs and tiers +./scripts/check-kani-coverage.sh # Validate proof registration +``` -// ✅ Prefer: Zero-cost, value-typed abstraction -#[derive(Clone, Copy)] -struct FrameRange { min: Frame, max: Frame } +New proofs must be registered in `scripts/verify-kani.sh`: -impl FrameRange { - const fn contains(self, frame: Frame) -> bool { - frame >= self.min && frame <= self.max - } -} -``` +- **Tier 1:** Fast (<30s) -- simple property checks +- **Tier 2:** Medium (30s-2min) -- moderate complexity +- **Tier 3:** Slow (>2min) -- complex state verification -#### Code Consolidation - -- **Look for patterns first** — Before writing new code, search for similar existing code -- **Extract shared utilities** — Test helpers, validation logic, formatting -- **Avoid copy-paste** — If tempted to copy code, create a shared abstraction instead -- **Refactor proactively** — When adding features, improve structure of touched code - -> **See also:** Performance and code quality guides in `.llm/skills/`: -> -> - [high-performance-rust.md](skills/high-performance-rust.md) — Performance optimization patterns and build configuration -> - [rust-binary-size-optimization.md](skills/rust-binary-size-optimization.md) — Minimizing binary size for WASM, embedded, and containers -> - [rust-refactoring-guide.md](skills/rust-refactoring-guide.md) — Safe code transformation patterns with verification -> - [rust-idioms-patterns.md](skills/rust-idioms-patterns.md) — Idiomatic Rust patterns and best practices -> - [clippy-configuration.md](skills/clippy-configuration.md) — Clippy lint configuration and enforcement -> - [zero-copy-memory-patterns.md](skills/zero-copy-memory-patterns.md) — Zero-copy and memory efficiency patterns -> - [async-rust-best-practices.md](skills/async-rust-best-practices.md) — Async Rust patterns for concurrent code -> - [rust-compile-time-optimization.md](skills/rust-compile-time-optimization.md) — Build and compile time optimization -> -> **Crate publishing and organization guides:** -> -> - [crate-publishing-guide.md](skills/crate-publishing-guide.md) — Publishing crates to crates.io with best practices -> - [workspace-organization.md](skills/workspace-organization.md) — Workspace structure, module organization, when to split crates -> - [public-api-design.md](skills/public-api-design.md) — Designing stable, ergonomic public APIs -> - [dependency-management.md](skills/dependency-management.md) — Evaluating, managing, and securing dependencies - -### Safety-Focused CI Checks (ci-safety.yml) - -The project runs comprehensive safety checks beyond standard linting: +Pre-commit validates registration only, NOT that proofs pass. Run affected proofs locally before committing. + +## Safety CI Checks | Check | Purpose | |-------|---------| -| **Cargo Careful** | Extra runtime safety checks using nightly | +| **Cargo Careful** | Extra runtime safety checks (nightly) | | **Overflow Checks** | Release builds with `-C overflow-checks=on` | | **Debug Assertions** | Release builds with `-C debug-assertions=on` | -| **Panic Patterns** | Counts `unwrap`, `expect`, `panic!`, `todo!` usage | -| **Strict Clippy** | Nursery lints enabled for experimental checks | -| **Documentation** | Doc build with `-D warnings` (warnings as errors) | - -See also: `ci-rust.yml` (Miri UB detection), `ci-security.yml` (cargo-geiger, cargo-deny) - -> **See also:** CI/CD guides in `.llm/skills/`: -> -> - [github-actions-best-practices.md](skills/github-actions-best-practices.md) — Workflow linting, shellcheck, Miri CI, timeout values, cross-platform scripts -> - [cross-platform-ci-cd.md](skills/cross-platform-ci-cd.md) — Multi-platform build strategies and release workflows -> - [ci-cd-debugging.md](skills/ci-cd-debugging.md) — Reproducing and debugging CI failures locally - -### Documentation Template - -```rust -/// Brief one-line description ending with a period. -/// -/// Longer explanation if needed, explaining the "why" not just "what". -/// -/// # Arguments -/// -/// * `param1` - What this parameter represents -/// -/// # Returns -/// -/// What the function returns and when. -/// -/// # Errors -/// -/// * [`FortressError::Variant`] - When this specific error occurs -/// -/// # Examples -/// -/// ``` -/// # use fortress_rollback::*; -/// let result = function(arg)?; -/// assert_eq!(result, expected); -/// # Ok::<(), FortressError>(()) -/// ``` -/// -/// [`FortressError::Variant`]: crate::error::FortressError::Variant -pub fn function(param1: Type) -> Result { - // Implementation -} -``` +| **Panic Patterns** | Counts `unwrap`, `expect`, `panic!`, `todo!` | +| **Strict Clippy** | Nursery lints enabled | +| **Documentation** | Doc build with `-D warnings` | -**Intra-doc link best practices:** +Also: `ci-rust.yml` (Miri), `ci-security.yml` (cargo-geiger, cargo-deny). -- Use shorthand `[`TypeName`]` when link text matches the final path segment -- Place link reference definitions at the end of doc blocks -- Run `cargo doc --no-deps` after documentation changes to verify links +**CI fails on:** unformatted code, clippy warnings, broken doc links, markdown lint errors, workflow syntax errors, unregistered Kani proofs. -### Test Writing Best Practices +## Development Workflow -> **See also:** Complete testing guides in `.llm/skills/`: -> -> - [rust-testing-guide.md](skills/rust-testing-guide.md) — Comprehensive testing best practices and patterns -> - [testing-tools-reference.md](skills/testing-tools-reference.md) — Tool ecosystem reference (nextest, proptest, mockall, etc.) -> - [property-testing.md](skills/property-testing.md) — Property-based testing to find edge cases automatically -> - [mutation-testing.md](skills/mutation-testing.md) — Mutation testing for test quality verification -> - [rust-fuzzing-guide.md](skills/rust-fuzzing-guide.md) — Fuzz testing with cargo-fuzz, LibAFL, and structured fuzzing -> - [network-chaos-testing.md](skills/network-chaos-testing.md) — Network chaos testing, sync preset selection, diagnosing sync failures -> - [cross-platform-ci-cd.md](skills/cross-platform-ci-cd.md) — CI/CD workflows for multi-platform builds -> - [ci-cd-debugging.md](skills/ci-cd-debugging.md) — Reproducing and debugging CI failures locally +### Before Writing Code -#### Test Organization +1. Read relevant source files and tests for context +2. Check existing patterns for consistency +3. Consider impact on other components +4. Plan tests first -- define expected behavior -| Location | Use Case | -|----------|----------| -| `src/*.rs` with `#[cfg(test)] mod tests` | Unit tests (access private functions) | -| `tests/it/*.rs` (single crate) | Integration tests (public API only) | -| `tests/common/mod.rs` | Shared test utilities | - -**Critical:** Integration tests in `tests/` should be consolidated into a single crate (`tests/it/main.rs`) to avoid slow compilation. - -#### Test Structure (Arrange-Act-Assert) +### When Fixing Bugs -```rust -#[test] -fn descriptive_name_explaining_what_is_tested() { - // Arrange: Set up test conditions - let mut session = create_test_session(); - let input = prepare_test_input(); - - // Act: Execute the behavior being tested - let result = session.some_operation(input); - - // Assert: Verify expected outcomes - assert!(result.is_ok()); - assert_eq!(result.unwrap(), expected_value); -} -``` +1. Write a failing test that reproduces the bug +2. Root-cause analysis -- understand *why*, not just *what* +3. Fix at the right level (production bug vs test bug) +4. Add regression tests; check for similar issues elsewhere -#### The `check` Helper Pattern (Recommended) +## Test Writing -Decouple tests from API changes with helper functions: +Use **Arrange-Act-Assert** pattern. Name tests: `what_condition_expected_behavior` (e.g., `parse_empty_input_returns_none`). ```rust #[track_caller] // Shows actual test location on failure @@ -334,645 +180,75 @@ fn check_parse(input: &str, expected: Option) { let actual = parse(input).ok(); assert_eq!(actual, expected, "parse({:?})", input); } - -#[test] -fn parse_empty_returns_none() { - check_parse("", None); -} - -#[test] -fn parse_valid_expression() { - check_parse("1 + 2", Some(expected_ast())); -} -``` - -#### Test Naming Convention - -Names should describe: **what** + **condition** + **expected behavior** - -```rust -// ❌ BAD -fn test1() { } -fn it_works() { } - -// ✅ GOOD -fn parse_empty_input_returns_none() { } -fn session_with_zero_players_returns_error() { } -fn rollback_preserves_confirmed_frames() { } ``` ---- +Consolidate integration tests into a single crate (`tests/it/main.rs`). Anti-patterns: `assert!(result.is_ok())` (use `assert_eq!`), sleep-based synchronization, testing implementation details. -## Root Cause Analysis — When Tests Fail +## Changelog Policy -**CRITICAL: The goal is NOT to make the test pass — it's to understand and fix the underlying issue.** +**Quick decision:** "Does this affect `pub` items or user-observable behavior?" -### Investigation Steps +- **YES** -- Add entry (use **Breaking:** prefix if API signature changed) +- **NO** (pub(crate), private, tests, CI) -- Skip -1. **Reproduce** — Is it consistent or flaky? Under what conditions? -2. **Understand** — What property is the test verifying? Why should it hold? -3. **Trace** — Add logging, use debugger, examine state at failure -4. **Hypothesize** — What could cause this specific failure mode? -5. **Verify** — Confirm understanding before implementing any fix -6. **Scope** — Are there similar issues elsewhere? +**Include:** new features/APIs, user-visible bug fixes, breaking changes (with migration guidance), performance improvements, dependency updates affecting compatibility. -### Production Bug vs Test Bug +**Exclude:** internal refactoring, test improvements, doc-only changes, CI/tooling, lint fixes. -**It's a production bug if:** +## Mandatory Linting -- Test expectations align with documented behavior -- Multiple tests depend on the same behavior -- The test logic is simple and clearly correct +- **After Rust changes:** `cargo fmt && cargo clippy --all-targets` (or `cargo c`) +- **After workflow changes:** `actionlint` (no exceptions) +- **After doc changes:** `cargo doc --no-deps` +- **After markdown changes:** `npx markdownlint 'file.md' --config .markdownlint.json --fix` +- **After `.llm/` changes:** All `.md` files under `.llm/` must be **300 lines or fewer** (enforced by pre-commit hook `llm-line-limit`) +- **Link validation:** `./scripts/check-links.sh` +- **Spell check:** `typos` +- **Vale (advisory):** `vale docs/` -- checks prose quality, non-blocking in CI +- **Full pre-commit:** `cargo fmt && cargo clippy --all-targets && cargo nextest run --no-capture` -**It's a test bug if:** +## Skill Code Examples -- Test makes assumptions not guaranteed by the API -- Test has inherent race conditions or timing issues -- Test expectations contradict documentation +Code examples in `.llm/skills/` must follow zero-panic rules with these exceptions: -### Strictly Forbidden "Fixes" +- **`build.rs`:** `.unwrap()` OK (comment `// build.rs:`) +- **Test code:** `.unwrap()` OK (comment `// test:` or `// In tests:`) +- **Fuzz targets:** `.expect()` OK (comment `// Fuzz target:`) +- **Loom tests:** `.unwrap()` on `.join()` OK (comment `// Loom test:`) +- **`#[allow]` examples:** showing lint suppression is the point +- Also accepted: `// proptest:`, `// allowed:`, `// SAFETY:`, `#[test]`, `#[fixture]`, `#[cfg(test)]` attributes -- ❌ Commenting out or weakening assertions -- ❌ Adding `Thread::sleep()` to "fix" timing -- ❌ Catching and swallowing errors -- ❌ `#[ignore]` without a documented fix plan -- ❌ Relaxing tolerances without understanding why -- ❌ Changing expected values to match actual without analysis +Additional rules: `catch_unwind` closures must use `AssertUnwindSafe`; fully qualify ambiguous types (e.g., `arbitrary::Result` not bare `Result`); no `2>/dev/null` in shell examples. Run `scripts/check-llm-skills.sh` after modifying `.llm/` files (also enforced in CI via `ci-llm-lint.yml`). ---- - -## Formal Verification Philosophy - -> **See also:** Complete guides in `.llm/skills/`: -> -> - [tla-plus-modeling.md](skills/tla-plus-modeling.md) — TLA+ specification patterns -> - [kani-verification.md](skills/kani-verification.md) — Kani proof harnesses -> - [z3-verification.md](skills/z3-verification.md) — Z3 SMT solver proofs -> - [loom-testing.md](skills/loom-testing.md) — Loom concurrency testing -> - [miri-verification.md](skills/miri-verification.md) — Miri UB detection -> - [mutation-testing.md](skills/mutation-testing.md) — Test quality verification -> - [property-testing.md](skills/property-testing.md) — Property-based testing - -**Core principles:** - -- **Specs model production** — TLA+/Kani/Z3 specs represent real code behavior -- **When verification fails, assume production bug first** — Investigate before relaxing specs -- **Never "fix" specs just to make them pass** — That defeats the purpose -- **Invariants represent real safety properties** — Only relax with strong justification - -### Quick Commands - -```bash -# Kani proofs -cargo kani -cargo kani --harness proof_specific_function - -# Kani with tiered execution -./scripts/verify-kani.sh --tier 1 --quick # Fast proofs (~15 min) -./scripts/verify-kani.sh --list # List all proofs and tiers - -# Validate Kani proof coverage (pre-commit runs this) -./scripts/check-kani-coverage.sh - -# TLA+ verification -./scripts/verify-tla.sh - -# Z3 proofs -cargo test --features z3-verification - -# Loom concurrency tests -cd loom-tests && RUSTFLAGS="--cfg loom" cargo test --release - -# Miri UB detection -cargo +nightly miri test - -# Mutation testing -cargo mutants -f src/module.rs --timeout 30 --jobs 4 -``` - -### CRITICAL: Kani Proof Changes - -**Pre-commit only validates that proofs are registered, NOT that they pass.** - -When modifying Kani proofs or code verified by them: - -1. **Run the affected proof locally** before committing: - - ```bash - cargo kani --harness proof_function_name - ``` - -2. **Ensure new proofs are registered** in `scripts/verify-kani.sh`: - - Tier 1: Fast proofs (<30s) — simple property checks - - Tier 2: Medium proofs (30s-2min) — moderate complexity - - Tier 3: Slow proofs (>2min) — complex state verification - -3. **Validate coverage** before pushing: - - ```bash - ./scripts/check-kani-coverage.sh - ``` - -**Why?** Kani verification is too slow for pre-commit (15+ minutes), so CI catches failures. Running affected proofs locally prevents CI surprises. - -**Remember:** - -- All loops with symbolic bounds require `#[kani::unwind(N)]` where N = max_iterations + 1. This is the #1 cause of Kani CI failures. -- **Verify proof assertions match actual implementation.** The #2 cause of failures is proofs that assert the wrong thing (e.g., asserting `ConnectionStatus::Connected` when the code returns `ConnectionStatus::Disconnected`). - -See [kani-verification.md](skills/kani-verification.md) for details. - -### After Finding a Bug via Verification - -1. **Direct reproduction** — Cover the exact discovered scenario -2. **Edge cases** — Zero, max, boundary conditions -3. **Chained operations** — Sequential calls that might compound -4. **Lifecycle tests** — Create-use-modify-destroy cycles -5. **Negative tests** — Ensure violations are detected - ---- - -## Rollback Netcode Development - -> **See also:** The rollback netcode guides in `.llm/skills/`: -> -> - [rollback-netcode-conversion.md](skills/rollback-netcode-conversion.md) — Complete guide to converting games to rollback netcode -> - [rollback-engine-integration.md](skills/rollback-engine-integration.md) — Patterns for Bevy and custom engine integration -> - [determinism-guide.md](skills/determinism-guide.md) — Achieving and verifying determinism in Rust games (includes reproducible builds, WASM, float handling, crate recommendations) -> - [deterministic-simulation-testing.md](skills/deterministic-simulation-testing.md) — DST frameworks (madsim, turmoil), failure injection, controlled concurrency -> - [cross-platform-games.md](skills/cross-platform-games.md) — Cross-platform game development (WASM, mobile, desktop) -> - [cross-platform-rust.md](skills/cross-platform-rust.md) — Multi-platform project architecture and tooling -> - [rust-binary-size-optimization.md](skills/rust-binary-size-optimization.md) — Minimizing binary size for WASM, embedded, and containers -> - [rust-ffi-best-practices.md](skills/rust-ffi-best-practices.md) — Hybrid Rust/C/C++ applications and FFI patterns -> - [wasm-rust-guide.md](skills/wasm-rust-guide.md) — Rust to WebAssembly compilation and toolchain -> - [no-std-guide.md](skills/no-std-guide.md) — `no_std` patterns for WASM and embedded -> - [wasm-threading.md](skills/wasm-threading.md) — Threading and concurrency in WebAssembly -> - [wasm-portability.md](skills/wasm-portability.md) — WASM determinism and sandboxing - -### Essential Rollback Concepts - -| Concept | Description | -|---------|-------------| -| **Determinism** | Same inputs MUST produce identical outputs on all machines | -| **State Serialization** | Must save/restore complete game state efficiently | -| **Input Prediction** | Guess remote inputs and continue simulation without waiting | -| **Rollback** | Restore saved state when prediction was wrong, resimulate | -| **Desync Detection** | Compare checksums between peers to catch divergence | -| **DST** | Deterministic Simulation Testing — control time, I/O, and concurrency for reproducible tests | - -### Critical Determinism Rules - -1. **No `HashMap` iteration** — Use `BTreeMap` or sort before iterating -2. **Control floating-point** — Use `libm` feature or fixed-point math -3. **Seeded RNG only** — `rand_pcg` or `rand_chacha` with shared seed -4. **Frame counters, not time** — Never use `Instant::now()` in simulation -5. **Sort ECS queries** — Bevy queries are non-deterministic; sort by stable ID -6. **Pin toolchain** — Use `rust-toolchain.toml` for reproducible builds -7. **Audit features** — Check for `ahash`, `const-random` feature leaks with `cargo tree -f "{p} {f}"` - ---- - -## Defensive Programming Patterns - -> **See [defensive-programming.md](skills/defensive-programming.md)** for the complete zero-panic guide with all patterns. - -### Zero-Panic Policy (CRITICAL) - -**Production code must NEVER panic.** This is non-negotiable. - -```rust -// ❌ FORBIDDEN in production code -value.unwrap() // Panics on None -value.expect("msg") // Panics with message -array[index] // Panics on out-of-bounds -panic!("something went wrong") // Explicit panic -todo!() // Panics as placeholder - -// ✅ REQUIRED - Return Results, let caller decide -value.ok_or(FortressError::MissingValue)? // Convert Option to Result -array.get(index).ok_or(FortressError::OutOfBounds)? // Safe indexing -if !valid { return Err(FortressError::InvalidState); } // Explicit error -``` - -### Key Principles - -- **Never swallow errors** — Use `?` to propagate, never `let _ = result` -- **Validate all inputs** — Don't assume internal state is valid -- **Prefer pattern matching** — Use `match` and `.get()` over indexing -- **Exhaustive matches** — Never use `_ =>` wildcards on enums (except `#[non_exhaustive]`) -- **Enums over booleans** — `Compression::Enabled` not `true` -- **Type safety** — Make invalid states unrepresentable -- **Doc examples too** — Rustdoc examples must use `?` and `Result`, never `panic!` or `unwrap()` -- **Verify doc examples** — Always verify error variants/types used in examples actually exist in source code - -**See also:** [type-driven-design.md](skills/type-driven-design.md), [rust-pitfalls.md](skills/rust-pitfalls.md) - ---- - -## Project Architecture - -> **See also:** Organization and publishing guides in `.llm/skills/`: -> -> - [workspace-organization.md](skills/workspace-organization.md) — When to split crates, module patterns, test organization -> - [public-api-design.md](skills/public-api-design.md) — Designing stable, ergonomic public APIs -> - [crate-publishing-guide.md](skills/crate-publishing-guide.md) — Publishing crates to crates.io - -### Repository Structure - -``` -src/ -├── lib.rs # Public API entry point -├── error.rs # FortressError types -├── frame_info.rs # Frame metadata -├── hash.rs # Deterministic FNV-1a hashing -├── rle.rs # Run-length encoding -├── rng.rs # Deterministic PCG32 RNG -├── time_sync.rs # Time synchronization -├── sync.rs # Synchronization primitives (loom-compatible) -├── checksum.rs # State checksum utilities -├── telemetry.rs # Structured telemetry pipeline -│ -├── input_queue/ -│ ├── mod.rs # Input buffering -│ └── prediction.rs # Input prediction strategies -│ -├── sync_layer/ -│ ├── mod.rs # Core synchronization (SyncLayer) -│ ├── game_state_cell.rs # Thread-safe game state -│ └── saved_states.rs # Circular buffer for rollback -│ -├── network/ -│ ├── compression.rs # Message compression -│ ├── messages.rs # Protocol messages -│ ├── network_stats.rs # Statistics tracking -│ ├── chaos_socket.rs # Testing socket with chaos -│ ├── udp_socket.rs # UDP abstraction -│ ├── codec.rs # Binary codec for serialization -│ ├── tokio_socket.rs # Tokio async adapter -│ └── protocol/ -│ ├── mod.rs # UDP protocol implementation -│ ├── event.rs # Protocol events -│ ├── input_bytes.rs # Byte-encoded input data -│ └── state.rs # Protocol state machine -│ -└── sessions/ - ├── builder.rs # SessionBuilder pattern - ├── p2p_session.rs # P2P gameplay - ├── p2p_spectator_session.rs # Spectator mode - ├── sync_test_session.rs # Determinism testing - ├── config.rs # Session configuration presets - ├── player_registry.rs # Player tracking and connection states - └── sync_health.rs # Synchronization health status -``` - -### Key Concepts - -| Concept | Description | -|---------|-------------| -| **Frame** | Discrete time step in game simulation (typically 60 FPS) | -| **Rollback** | Restoring previous state when predictions are wrong | -| **Input Delay** | Buffer frames to reduce network jitter (typically 2-3 frames) | -| **Prediction** | Continue simulation before remote inputs arrive | -| **Prediction Window** | Maximum frames ahead we'll predict (typically 6-8) | -| **Desync** | State divergence between peers (detected via checksums) | -| **Determinism** | Same inputs must always produce same outputs | -| **Checksum** | Hash of game state for desync detection | -| **Confirmed Frame** | Oldest frame where all inputs are known | -| **Resimulation** | Re-running frames with corrected inputs after rollback | - -### Session Types - -- **P2PSession** — Standard peer-to-peer gameplay -- **SpectatorSession** — Observe but don't participate -- **SyncTestSession** — Verify determinism by running simulation twice - ---- - -## Common Code Patterns - -### Session Builder - -```rust -let session = SessionBuilder::::new() - .with_num_players(2) - .with_input_delay(2) - .with_max_prediction(8) - .add_player(PlayerType::Local, PlayerHandle::new(0))? - .add_player(PlayerType::Remote(addr), PlayerHandle::new(1))? - .start_p2p_session(socket)?; -``` - -### Request Handling Loop - -```rust -for request in session.advance_frame()? { - match request { - FortressRequest::SaveGameState { frame, cell } => { - cell.save(frame, Some(game_state.clone()), None); - } - FortressRequest::LoadGameState { cell, frame } => { - // LoadGameState is only requested for previously saved frames. - // Missing state indicates a library bug, but we handle gracefully. - if let Some(loaded) = cell.load() { - game_state = loaded; - } else { - eprintln!("WARNING: LoadGameState for frame {frame:?} but no state found"); - } - } - FortressRequest::AdvanceFrame { inputs } => { - game_state.update(&inputs); - } - } -} -``` - -### Player Types - -```rust -PlayerType::Local // Local player on this device -PlayerType::Remote(addr) // Remote player (SocketAddr) -PlayerType::Spectator(addr) // Observer (no input) -``` - ---- - -## Development Policies - -### Breaking Changes Are Acceptable - -- **API compatibility is NOT required** — This is a correctness-first fork -- **Safety and correctness trump compatibility** — Make breaking changes if they improve quality -- **Document all breaking changes** — Update `CHANGELOG.md` and `docs/migration.md` - -#### Breaking Change Checklist - -Before merging any breaking API change: +## Breaking Changes Checklist - [ ] `CHANGELOG.md` updated with **Breaking:** prefix and migration guidance -- [ ] `docs/migration.md` updated with before/after code examples -- [ ] `README.md` examples updated if affected -- [ ] `docs/user-guide.md` updated if affected -- [ ] All `examples/*.rs` files compile: `cargo build --examples` +- [ ] `docs/migration.md` updated with before/after examples +- [ ] `README.md` and `docs/user-guide.md` updated if affected +- [ ] All `examples/*.rs` compile: `cargo build --examples` - [ ] Rustdoc examples compile: `cargo test --doc` -- [ ] Search for old API usage: `rg 'old_function_name' --type rust --type md` - -### Test Coverage Requirements - -> **See also:** [rust-testing-guide.md](skills/rust-testing-guide.md) for comprehensive testing patterns. - -- All new features must include tests -- Aim for >90% code coverage -- Include positive and negative test cases -- Test edge cases and error conditions -- Use integration tests for cross-component behavior -- Use `cargo nextest run` for faster test execution -- Run mutation testing (`cargo mutants`) to verify test quality - -**Testing anti-patterns to avoid:** - -- `assert!(result.is_ok())` — Use `assert_eq!` with specific values -- Multiple assertions testing different behaviors in one test -- Sleep-based synchronization — Use proper channels/signals -- Testing implementation details instead of behavior -- Ignoring tests without documented fix plan - -### Changelog Policy - -The changelog (`CHANGELOG.md`) is for **users of the library**, not developers. - -> **See also:** [changelog-practices.md](skills/changelog-practices.md) for detailed guidance, examples, and the visibility reference table. +- [ ] Search for old API usage: `rg 'old_name' --type rust --type md` -**Quick Decision:** Ask "Does this affect `pub` items or user-observable behavior?" +## Documentation Sync -- **YES** → Add changelog entry (use **Breaking:** prefix if API signature changed) -- **NO** (pub(crate), private, tests, CI) → Skip changelog - -**Include in changelog:** - -- New features, APIs, or configuration options -- Bug fixes that affect user-visible behavior -- Breaking changes (with migration guidance) -- New enum variants on exhaustively matchable enums (**Breaking:** — see skill doc) -- Performance improvements users would notice -- Dependency updates that affect compatibility - -**Do NOT include in changelog:** - -- Internal refactoring (module splits, code reorganization) -- Test improvements or new tests -- Documentation-only changes -- CI/CD or tooling changes -- Code style or lint fixes - -**Exception:** If a release contains *only* internal work (no user-facing changes), add a single summary line like: -> "Internal: Improved test coverage and code organization" - -This keeps the changelog focused and useful for library consumers. - ---- - -## Resources - -| Resource | Link | -|----------|------| -| Original GGPO | | -| GGPO Discord | | -| Bevy GGRS Plugin | | -| TLA+ Resources | | -| Z3 Prover | | - ---- +When changing public APIs, update: rustdoc comments (source of truth), README.md, docs/user-guide.md, examples/, CHANGELOG.md. Search with `rg 'function_name|StructName' --type rust --type md`. ## Quality Checklist -Before submitting code: - -- [ ] `cargo fmt` run (no formatting changes) -- [ ] `cargo clippy --all-targets` passes with no warnings -- [ ] All tests pass (`cargo nextest run` or `cargo test`) -- [ ] Includes tests for new functionality +- [ ] `cargo fmt` run +- [ ] `cargo clippy --all-targets` passes +- [ ] All tests pass (`cargo nextest run`) +- [ ] Tests for new functionality included - [ ] Rustdoc comments with examples - [ ] 100% safe Rust (no unsafe) -- [ ] Handles all error cases -- [ ] **No duplicate methods:** If implementing `Display`/`Debug`/`Hash`/etc., don't add separate methods duplicating that functionality -- [ ] **Feature-dependent APIs documented:** If `#[cfg(feature = ...)]` affects trait bounds or available methods, document it in rustdoc -- [ ] **Changelog reviewed:** Asked "Does this affect `pub` items or user-observable behavior?" — if yes, added entry to CHANGELOG.md (including new trait impls like `Display`) - ---- - -## Mandatory Linting — Run After EVERY Change - -> **Critical:** Run linters after EVERY code change, not just before committing. This catches errors immediately while context is fresh and prevents accumulating technical debt. - -### Rust Code Changes - -**Run after EVERY Rust file modification:** - -```bash -cargo fmt && cargo clippy --all-targets -``` - -**Or use the alias:** - -```bash -cargo c # Defined in .cargo/config.toml -``` - -This catches formatting issues and lint warnings immediately. Don't wait until you have multiple changes — lint after each edit. - -### GitHub Actions Workflow Changes - -**Run `actionlint` IMMEDIATELY after ANY workflow modification — no exceptions:** - -```bash -actionlint -``` - -Workflow syntax errors are easy to introduce and tedious to debug in CI. Always validate locally first. - -**Workflow reliability:** Workflows that call GitHub APIs (releases, artifact uploads, API queries) should include retry logic. Transient API failures are common; handle them gracefully rather than failing the entire workflow. - -### Documentation Changes - -> **See also:** [documentation-code-consistency.md](skills/documentation-code-consistency.md) — Keeping docs and code in sync, verifying CHANGELOG accuracy, error documentation patterns. - -**Run after modifying any rustdoc comments:** - -```bash -cargo doc --no-deps -``` - -The pre-commit hook runs `cargo doc` with strict `RUSTDOCFLAGS=-D warnings`, matching CI. This catches: - -- Broken intra-doc links -- Invalid HTML in documentation -- Missing documentation for public items (when enabled) - -**Intra-doc link syntax:** Prefer shorthand syntax with link reference definitions: - -```rust -// ❌ Avoid: Inline explicit links (verbose, duplicates path) -/// Returns a [`PlayerHandle`](crate::sessions::PlayerHandle). - -// ✅ Prefer: Shorthand with reference definition -/// Returns a [`PlayerHandle`]. -/// -/// [`PlayerHandle`]: crate::sessions::PlayerHandle -``` - -Use shorthand `[`TypeName`]` when the link text matches the final path segment. Place reference definitions at the end of doc blocks for readability. - -#### Documentation Synchronization - -When changing public APIs, update documentation in ALL locations: - -1. **Rustdoc comments** — The source of truth in `src/` -2. **README.md** — Quick start examples and feature overview -3. **docs/user-guide.md** — Detailed usage examples -4. **examples/** — Runnable example files -5. **CHANGELOG.md** — User-facing change description - -**Search for usages before committing:** - -```bash -# Find all references to the changed API -rg 'function_name|StructName' --type rust --type md - -# Check examples compile after API changes -cargo build --examples -``` - -**Common synchronization failures:** - -- README shows old API signature while code has new one -- Examples use deprecated methods -- User guide references removed configuration options - -### Full Verification (Before Committing) - -**Run the complete check before any commit:** - -```bash -# Format + lint + test (with output capture for debugging) -cargo fmt && cargo clippy --all-targets && cargo nextest run --no-capture -``` - -**Or use the aliases:** - -```bash -cargo c && cargo t # Defined in .cargo/config.toml (includes --no-capture) -``` - -### Additional Checks - -**Spell checking:** Run `typos` before committing. CI enforces this. - -```bash -typos # Spell check -actionlint # GitHub Actions linting -``` - -### Markdown Formatting - -**Always run markdownlint after editing any markdown file.** The `--fix` flag auto-fixes most issues: - -```bash -# Fix a specific file -npx markdownlint 'docs/file.md' --config .markdownlint.json --fix - -# Fix all markdown files in a directory -npx markdownlint 'docs/**/*.md' --config .markdownlint.json --fix - -# Check all workspace markdown -npx markdownlint '**/*.md' --config .markdownlint.json --fix -``` - -**Pre-commit hook:** The hook automatically runs markdownlint with `--fix` on staged markdown files. - -**Link validation:** Run `./scripts/check-links.sh` after editing markdown — CI will fail on broken links. - -**See also:** [markdown-formatting.md](skills/markdown-formatting.md) for complete style rules, common fixes, and CI configuration. - -### Vale Prose Linting (docs/ directory) - -Vale runs on the `docs/` directory to check prose quality. While currently advisory (non-blocking), it catches common writing issues. - -```bash -# Install Vale (if not using dev container) -brew install vale # macOS -# or download from https://vale.sh/docs/vale-cli/installation/ - -# Sync Vale packages (one-time, or when .vale.ini changes) -vale sync - -# Run Vale on docs/ -vale docs/ - -# Check a specific file -vale docs/user-guide.md -``` - -**What Vale checks:** - -- Passive voice usage (suggestion) -- Weasel words like "very", "quite", "really" (suggestion) -- Wordy phrases that could be simplified (suggestion) -- Clichés (warning) -- Word illusions ("the the") (warning) - -**Configuration:** `.vale.ini` controls which rules are enabled. Project-specific vocabulary is in `.vale/styles/config/vocabularies/Fortress/`. - -**CI behavior:** Vale runs in `ci-docs.yml` with `continue-on-error: true` and `fail_on_error: false`. This makes it advisory only, not blocking. - -### For Agents and Sub-Agents - -When spawning sub-agents or using Task tools to make code changes: +- [ ] All error cases handled +- [ ] No duplicate methods (e.g., don't add method duplicating `Display` impl) +- [ ] Feature-dependent APIs documented in rustdoc +- [ ] Changelog reviewed for pub/user-observable changes -1. The sub-agent MUST run `cargo fmt` on any files it modifies -2. The sub-agent MUST verify `cargo clippy --all-targets` passes -3. If the sub-agent cannot run these commands, the parent agent must run them after receiving the changes +## For Agents -**See also:** [GitHub Actions Best Practices](skills/github-actions-best-practices.md), [Markdown Link Validation](skills/markdown-link-validation.md), and [CI/CD Debugging](skills/ci-cd-debugging.md) for detailed guidance. +When spawning sub-agents or using Task tools: the sub-agent MUST run `cargo fmt` and verify `cargo clippy --all-targets` passes on any modified files. If the sub-agent cannot run these, the parent agent must run them after receiving changes. --- diff --git a/.llm/skills/_index.md b/.llm/skills/_index.md new file mode 100644 index 00000000..d6587326 --- /dev/null +++ b/.llm/skills/_index.md @@ -0,0 +1,83 @@ +# Skills Index + + + +## CI/CD & Tooling + +| Skill | When to Use | +|-------|-------------| +| [ci-debugging.md](ci-debugging.md) | Debugging CI failures, reproducing CI issues locally | +| [doc-code-sync.md](doc-code-sync.md) | CHANGELOG verification, error variant matching, struct field checks, doc-code consistency | +| [github-actions.md](github-actions.md) | Writing GitHub Actions workflows, CI debugging, actionlint, caching | +| [markdown.md](markdown.md) | Markdown formatting, markdownlint configuration | +| [scripting.md](scripting.md) | Writing build scripts, Python helpers, shell portability | +| [wiki-sync.md](wiki-sync.md) | Wiki sync, documentation links, markdown anchor validation | + +## Determinism & Rollback + +| Skill | When to Use | +|-------|-------------| +| [determinism.md](determinism.md) | Ensuring determinism, replacing HashMap, float handling, cross-platform determinism | +| [dst.md](dst.md) | Deterministic simulation testing, madsim, turmoil, failure injection | +| [rollback.md](rollback.md) | Converting games to rollback netcode, engine integration, state management | + +## Formal Verification + +| Skill | When to Use | +|-------|-------------| +| [kani.md](kani.md) | Writing Kani proofs, debugging verification failures, unwind configuration | +| [loom.md](loom.md) | Testing concurrent code with Loom, model checking thread interleavings | +| [miri.md](miri.md) | Running Miri, debugging undefined behavior, adapting code for Miri | +| [verification.md](verification.md) | Writing TLA+ specs, Z3 SMT proofs, formal modeling | + +## Performance & Quality + +| Skill | When to Use | +|-------|-------------| +| [binary-size.md](binary-size.md) | Reducing binary size, WASM size optimization, LTO configuration | +| [clippy.md](clippy.md) | Configuring Clippy, lint levels, fortress-specific lint rules | +| [performance.md](performance.md) | Optimizing performance, profiling, build configuration, compile times | + +## Platform + +| Skill | When to Use | +|-------|-------------| +| [cross-platform.md](cross-platform.md) | Cross-platform builds, cfg attributes, platform-specific code, CI matrix | +| [wasm.md](wasm.md) | WASM compilation, wasm-bindgen, web workers, WASM portability | + +## Publishing & Organization + +| Skill | When to Use | +|-------|-------------| +| [changelog.md](changelog.md) | Writing CHANGELOG entries, deciding what to document | +| [dependency-management.md](dependency-management.md) | Evaluating dependencies, supply chain security, cargo-deny | +| [publishing.md](publishing.md) | Publishing to crates.io, version bumps, release checklist | +| [workspace.md](workspace.md) | Organizing workspace, splitting crates, module structure decisions | + +## Rust Language + +| Skill | When to Use | +|-------|-------------| +| [api-design.md](api-design.md) | Designing public APIs, checking semver compliance, reviewing breaking changes | +| [async-rust.md](async-rust.md) | Writing async code, debugging futures, Send/Sync issues | +| [concurrency.md](concurrency.md) | Choosing concurrency primitives, Mutex vs RwLock decisions, channel patterns | +| [defensive-programming.md](defensive-programming.md) | Implementing error handling, ensuring zero-panic compliance, validating inputs | +| [ffi.md](ffi.md) | Writing FFI code, C interop, bindgen usage | +| [no-std.md](no-std.md) | Writing no_std code, embedded targets, core vs alloc decisions | +| [refactoring.md](refactoring.md) | Refactoring code, planning safe transformations, verification after changes | +| [rust-design-patterns.md](rust-design-patterns.md) | Choosing design patterns, implementing builders, state machines, or strategy pattern | +| [rust-idioms.md](rust-idioms.md) | Writing idiomatic Rust, implementing traits, error handling patterns | +| [rust-pitfalls.md](rust-pitfalls.md) | Reviewing code, debugging common Rust mistakes, avoiding pitfalls | +| [text-parsing.md](text-parsing.md) | Parsing text, regex patterns, state machine parsers | +| [type-driven-design.md](type-driven-design.md) | Designing type-safe APIs, using newtypes, encoding state in types | +| [zero-copy.md](zero-copy.md) | Optimizing memory usage, zero-copy patterns, Cow vs clone decisions | + +## Testing + +| Skill | When to Use | +|-------|-------------| +| [fuzzing.md](fuzzing.md) | Fuzz testing, cargo-fuzz, writing fuzz harnesses, crash analysis | +| [mutation-testing.md](mutation-testing.md) | Running mutation tests, verifying test quality, cargo-mutants | +| [network-chaos-testing.md](network-chaos-testing.md) | Testing network resilience, chaos testing, sync failure diagnosis | +| [property-testing.md](property-testing.md) | Writing property-based tests, proptest strategies, custom generators | +| [testing.md](testing.md) | Writing tests, choosing testing tools, test organization, nextest usage | diff --git a/.llm/skills/api-design.md b/.llm/skills/api-design.md new file mode 100644 index 00000000..29576622 --- /dev/null +++ b/.llm/skills/api-design.md @@ -0,0 +1,165 @@ + + +# Public API Design + +## Key Principles + +1. **Minimize surface area** -- expose only what users truly need +2. **Hide implementation** -- newtypes, `pub(crate)`, private fields +3. **Design for stability** -- `#[non_exhaustive]` judiciously +4. **Document everything** -- every public item needs rustdoc +5. **Re-export dependencies** -- users should not hunt for your deps + +## Visibility Rules + +| Level | When | +|-------|------| +| `fn` (private) | Default -- accessible only in module | +| `pub(crate)` | Crate-internal helpers | +| `pub(super)` | Parent module helpers | +| `pub` | Intentional public API -- forever commitment | + +**Never expose struct fields directly** unless fundamental to meaning. Use accessor methods. + +## `#[non_exhaustive]` + +| Use on | When | +|--------|------| +| Error enums | May gain variants in minor releases | +| Config structs | May gain fields | +| **Avoid on** | Fixed-set enums where exhaustive matching catches bugs | + +## Newtype Pattern + +- Wrap external dependency types -- changing deps should not break users +- Create distinct types for values that should not be mixed (`Frame`, `PlayerHandle`, `Port`) +- Derive useful traits: `Clone, Copy, Debug, PartialEq, Eq, Hash` + +## Trait Design + +- **Minimal bounds** -- document why each bound is needed +- **Re-export** traits users need to use your API +- **Associated types** for flexibility over fixed types +- **Combined traits** with blanket impls for common bound sets + +## Error Handling + +### Scoped Error Types +```rust +pub mod session { + #[derive(Debug, thiserror::Error)] + #[non_exhaustive] + pub enum CreateError { /* ... */ } + + #[derive(Debug, thiserror::Error)] + #[non_exhaustive] + pub enum AdvanceError { /* ... */ } +} +``` + +### Error Documentation +```rust +/// # Errors +/// +/// Returns [`CreateError::InvalidConfig`] if config.max_players is zero. +/// Returns [`CreateError::NetworkBind`] if UDP socket cannot bind. +pub fn create(config: SessionConfig) -> Result { /* ... */ } +``` + +## Documentation + +```rust +#![warn(missing_docs)] +#![warn(rustdoc::broken_intra_doc_links)] +``` + +- Crate-level docs with quick-start example +- Every public item documented with examples +- Use intra-doc links: `[`Session::rollback_count`]` +- Document feature flags in crate-level docs + +## Re-exports and Prelude + +```rust +pub use bytes::{Bytes, BytesMut}; // re-export dep types in public API +pub mod prelude; // common imports for convenience +pub type Result = std::result::Result; // result alias +``` + +**Result alias hazard:** Use distinctive names (`FortressResult`) to avoid shadowing `std::result::Result`. + +## Feature Flags + +- **Must be additive** -- enabling multiple features must compile +- **Document all features** in crate-level docs table +- **Document feature-dependent bounds** (e.g., `+ Send + Sync` only with `sync` feature) + +```toml +[features] +default = [] +serde = ["dep:serde"] +async = ["dep:tokio"] +full = ["serde", "async"] +``` + +## Semver Rules + +### Breaking Changes (MAJOR bump, or MINOR pre-1.0) + +- [ ] Removing public items +- [ ] Changing function signatures +- [ ] Adding required struct fields +- [ ] Adding required trait methods (without defaults) +- [ ] Changing enum variants (unless `#[non_exhaustive]`) +- [ ] Tightening trait bounds +- [ ] Removing trait implementations +- [ ] Changing MSRV + +```bash +cargo install cargo-semver-checks +cargo semver-checks check-release +``` + +## API Review Checklist + +### Visibility +- [ ] All items private by default? +- [ ] `pub(crate)` for internal items? +- [ ] Struct fields private with accessors? + +### Types +- [ ] Newtypes wrap external dep types? +- [ ] Similar primitives distinguished by type? +- [ ] Trait bounds minimal and documented? + +### Enums +- [ ] `#[non_exhaustive]` only where catch-all is acceptable? +- [ ] Error enums scoped to specific operations? + +### Errors +- [ ] Distinct operations have distinct error types? +- [ ] Errors include context for debugging? +- [ ] `# Errors` section in docs? + +### Documentation +- [ ] `#![warn(missing_docs)]` enabled? +- [ ] Examples tested via doctests? +- [ ] Intra-doc links to related items? + +### Versioning +- [ ] Semantic versioning followed? +- [ ] `cargo semver-checks` run? +- [ ] Breaking changes documented? + +## Anti-Patterns + +| Anti-Pattern | Solution | +|--------------|----------| +| Public fields | Private fields + accessors | +| Exposing dep types | Newtype wrappers | +| One mega error type | Scoped error types | +| Complex trait bounds | Minimal bounds | +| Missing `#[non_exhaustive]` on errors | Add annotation | +| `#[non_exhaustive]` on fixed enums | Keep exhaustive | +| Missing re-exports | Re-export public deps | +| Breaking changes in minor | cargo-semver-checks | diff --git a/.llm/skills/async-rust-best-practices.md b/.llm/skills/async-rust-best-practices.md deleted file mode 100644 index 16c2b026..00000000 --- a/.llm/skills/async-rust-best-practices.md +++ /dev/null @@ -1,718 +0,0 @@ -# Async Rust Best Practices Guide - -A comprehensive guide to writing efficient, correct, and maintainable async Rust code. - -## Table of Contents - -1. [Fundamental Principles](#fundamental-principles) -2. [Best Practices](#best-practices) -3. [Common Pitfalls to Avoid](#common-pitfalls-to-avoid) -4. [Performance Considerations](#performance-considerations) -5. [Testing Async Code](#testing-async-code) -6. [When to Use Async vs Sync](#when-to-use-async-vs-sync) - ---- - -## Fundamental Principles - -### The Golden Rule - -> **Async code should never spend a long time without reaching an `.await`.** - -Task switching in async Rust only happens at `.await` points. Code that runs for extended periods between `.await`s blocks the entire runtime thread, preventing other tasks from executing. - -### Understanding Futures - -- **Futures are lazy**: They do nothing until polled/awaited -- **Futures are state machines**: The compiler transforms async blocks into efficient state machines -- **Futures must be pinned**: Once polled, a future cannot be moved in memory - -```rust -// ❌ This does nothing - the future is never awaited -async fn fetch_data() -> Data { /* ... */ } -let _ = fetch_data(); // Warning: unused future - -// ✅ Correct: await the future -let data = fetch_data().await; -``` - ---- - -## Best Practices - -### 1. Use Async-Native APIs, Not Blocking Equivalents - -**Bad**: Using `std::thread::sleep` in async code blocks the entire thread: - -```rust -// ❌ Blocks the runtime thread -async fn bad_delay() { - std::thread::sleep(Duration::from_secs(1)); // Blocks! - println!("Done"); -} -``` - -**Good**: Use Tokio's async sleep: - -```rust -// ✅ Yields control back to the runtime -async fn good_delay() { - tokio::time::sleep(Duration::from_secs(1)).await; - println!("Done"); -} -``` - -### 2. Handle Blocking Operations Correctly - -For unavoidable blocking operations, use `spawn_blocking`: - -```rust -// ✅ Run blocking code on dedicated thread pool -let result = tokio::task::spawn_blocking(|| { - // Expensive computation or blocking I/O - expensive_sync_operation() -}).await?; -``` - -**When to use each approach**: - -| Scenario | Solution | -|----------|----------| -| Short blocking (<10-100μs) | Can run inline | -| File system operations | `spawn_blocking` or `tokio::fs` | -| CPU-bound computation | `spawn_blocking` or `rayon` | -| Forever-running blocking | Dedicated `std::thread::spawn` | - -### 3. Choose the Right Concurrency Primitive - -**Sequential Execution** (one after another): - -```rust -let a = fetch_a().await?; -let b = fetch_b().await?; -``` - -**Concurrent Execution** (all at once, wait for all): - -```rust -// Using join! - runs concurrently, returns tuple -let (a, b, c) = tokio::join!(fetch_a(), fetch_b(), fetch_c()); - -// Using try_join! - fails fast on first error -let (a, b) = tokio::try_join!(fetch_a(), fetch_b())?; -``` - -**Concurrent Execution** (first to complete wins): - -```rust -tokio::select! { - result = fetch_a() => { /* a completed first */ } - result = fetch_b() => { /* b completed first */ } - _ = tokio::time::sleep(timeout) => { /* timeout */ } -} -``` - -**Dynamic Number of Futures**: - -```rust -use futures::stream::FuturesUnordered; -use futures::StreamExt; - -let mut futures = FuturesUnordered::new(); -for url in urls { - futures.push(fetch(url)); -} - -while let Some(result) = futures.next().await { - handle(result); -} -``` - -### 4. Use Timeouts for Reliability - -Always add timeouts to operations that could hang: - -```rust -use tokio::time::{timeout, Duration}; - -// ✅ Operation with timeout -match timeout(Duration::from_secs(5), fetch_data()).await { - Ok(result) => handle(result?), - Err(_) => return Err(Error::Timeout), -} -``` - -### 5. Choose the Right Channel Type - -| Channel | Use Case | -|---------|----------| -| `mpsc` | Multiple producers, single consumer | -| `oneshot` | Send exactly one value (result of computation) | -| `broadcast` | Multiple consumers, all receive all messages | -| `watch` | Single value that changes over time | - -```rust -// oneshot: Perfect for returning results from spawned tasks -let (tx, rx) = tokio::sync::oneshot::channel(); -tokio::spawn(async move { - let result = compute().await; - let _ = tx.send(result); -}); -let result = rx.await?; - -// mpsc: Job queue pattern -let (tx, mut rx) = tokio::sync::mpsc::channel(100); -tokio::spawn(async move { - while let Some(job) = rx.recv().await { - process(job).await; - } -}); -``` - -### 6. Implement Graceful Shutdown - -Use cancellation tokens for coordinated shutdown: - -```rust -use tokio_util::sync::CancellationToken; - -let token = CancellationToken::new(); -let cloned_token = token.clone(); - -// Worker task -tokio::spawn(async move { - loop { - tokio::select! { - _ = cloned_token.cancelled() => { - // Cleanup and exit - break; - } - result = do_work() => { - handle(result); - } - } - } -}); - -// Shutdown trigger -tokio::signal::ctrl_c().await?; -token.cancel(); -``` - -Use `TaskTracker` to wait for all tasks to complete: - -```rust -use tokio_util::task::TaskTracker; - -let tracker = TaskTracker::new(); - -for i in 0..10 { - tracker.spawn(async move { process(i).await }); -} - -tracker.close(); -tracker.wait().await; // Wait for all tasks -``` - -### 7. Prefer Owned Data in Spawned Tasks - -Spawned tasks require `'static` bounds. Prefer owned data: - -```rust -// ❌ Won't compile - borrows data -let data = vec![1, 2, 3]; -tokio::spawn(async { - println!("{:?}", data); // Error: data doesn't live long enough -}); - -// ✅ Clone or move owned data -let data = vec![1, 2, 3]; -tokio::spawn(async move { - println!("{:?}", data); // data is moved into the task -}); - -// ✅ Use Arc for shared read-only data -let data = Arc::new(vec![1, 2, 3]); -let data_clone = Arc::clone(&data); -tokio::spawn(async move { - println!("{:?}", data_clone); -}); -``` - -### 8. Use Async-Aware Synchronization Primitives - -**For shared state across `.await` points**, use `tokio::sync::Mutex`: - -```rust -use tokio::sync::Mutex; - -let shared = Arc::new(Mutex::new(HashMap::new())); - -// ✅ Safe to hold across .await -let mut guard = shared.lock().await; -let value = fetch_value().await; -guard.insert(key, value); -``` - -**For quick, synchronous access**, `std::sync::Mutex` is fine: - -```rust -use std::sync::Mutex; - -let shared = Arc::new(Mutex::new(counter)); - -// ✅ OK if lock is brief and no .await while held -{ - let mut guard = shared.lock().unwrap(); - *guard += 1; -} // Lock released before any await -``` - -### 9. Structure Error Handling for Async Code - -Use `Result` types consistently and handle errors at appropriate levels: - -```rust -use thiserror::Error; - -#[derive(Error, Debug)] -enum ServiceError { - #[error("network error: {0}")] - Network(#[from] reqwest::Error), - #[error("timeout after {0:?}")] - Timeout(Duration), - #[error("cancelled")] - Cancelled, -} - -async fn fetch_with_retry(url: &str) -> Result { - for attempt in 0..3 { - match timeout(Duration::from_secs(5), client.get(url).send()).await { - Ok(Ok(response)) => return Ok(response), - Ok(Err(e)) if attempt < 2 => { - tokio::time::sleep(backoff(attempt)).await; - continue; - } - Ok(Err(e)) => return Err(ServiceError::Network(e)), - Err(_) => return Err(ServiceError::Timeout(Duration::from_secs(5))), - } - } - unreachable!() -} -``` - -### 10. Use JoinSet for Managing Multiple Tasks - -For spawning and managing multiple tasks: - -```rust -use tokio::task::JoinSet; - -let mut set = JoinSet::new(); - -for url in urls { - set.spawn(async move { - fetch(url).await - }); -} - -// Process results as they complete -while let Some(result) = set.join_next().await { - match result { - Ok(Ok(data)) => process(data), - Ok(Err(e)) => log::error!("Task failed: {e}"), - Err(e) => log::error!("Task panicked: {e}"), - } -} -``` - -### 11. Configure Runtime Appropriately - -**For applications** - use full features: - -```toml -tokio = { version = "1", features = ["full"] } -``` - -**For libraries** - use minimal features: - -```toml -tokio = { version = "1", features = ["rt", "sync"] } -``` - -**For single-threaded scenarios**: - -```rust -#[tokio::main(flavor = "current_thread")] -async fn main() { - // Simpler, no Send + 'static requirements for local tasks -} -``` - -**For custom configuration**: - -```rust -let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(4) - .enable_all() - .build()?; - -runtime.block_on(async { /* ... */ }); -``` - -### 12. Handle Cancellation Safely - -Remember: dropping a future cancels it. Ensure cleanup happens: - -```rust -async fn safe_operation() { - let temp_file = create_temp_file().await?; - - // Use a guard to ensure cleanup - let _guard = scopeguard::guard((), |_| { - // This runs even if the future is cancelled - let _ = std::fs::remove_file(&temp_file); - }); - - process_file(&temp_file).await?; - // Guard drops here, cleaning up -} -``` - -### 13. Implement Backpressure - -Prevent memory exhaustion with bounded channels: - -```rust -// ❌ Unbounded can grow forever -let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - -// ✅ Bounded provides backpressure -let (tx, rx) = tokio::sync::mpsc::channel(100); - -// Sender will wait when buffer is full -tx.send(item).await?; -``` - -### 14. Use Semaphores for Concurrency Limiting - -Control maximum concurrent operations: - -```rust -use tokio::sync::Semaphore; - -let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent - -for url in urls { - let permit = semaphore.clone().acquire_owned().await?; - tokio::spawn(async move { - let _permit = permit; // Held for duration - fetch(url).await - }); -} -``` - -### 15. Use Streams for Async Iteration - -Process items as they arrive: - -```rust -use tokio_stream::StreamExt; - -let mut stream = tokio_stream::iter(items) - .map(|item| async move { process(item).await }) - .buffer_unordered(10); // Process 10 concurrently - -while let Some(result) = stream.next().await { - handle(result)?; -} -``` - ---- - -## Common Pitfalls to Avoid - -### 1. Blocking the Runtime Thread - -```rust -// ❌ These block the runtime: -std::thread::sleep(duration); -std::fs::read_to_string(path); -mutex.lock().unwrap(); // If held across .await - -// ✅ Use async alternatives: -tokio::time::sleep(duration).await; -tokio::fs::read_to_string(path).await; -async_mutex.lock().await; -``` - -### 2. Forgetting to Await Futures - -```rust -// ❌ Does nothing! -async fn process() { - fetch_data(); // Warning: unused future -} - -// ✅ Await the result -async fn process() { - fetch_data().await; -} -``` - -### 3. Holding Locks Across Await Points - -```rust -// ❌ Can cause deadlocks with std::sync::Mutex -let guard = mutex.lock().unwrap(); -async_operation().await; // Other tasks can't get lock -drop(guard); - -// ✅ Release before await -{ - let mut guard = mutex.lock().unwrap(); - *guard = new_value; -} // Lock released -async_operation().await; - -// ✅ Or use tokio::sync::Mutex -let guard = async_mutex.lock().await; -async_operation().await; // Safe with async mutex -``` - -### 4. Creating Too Many Tasks - -```rust -// ❌ Spawns millions of tasks -for i in 0..1_000_000 { - tokio::spawn(async move { process(i).await }); -} - -// ✅ Use streams with concurrency limit -use futures::stream::{self, StreamExt}; -stream::iter(0..1_000_000) - .for_each_concurrent(100, |i| async move { - process(i).await; - }) - .await; -``` - -### 5. Ignoring Backpressure - -```rust -// ❌ Can exhaust memory -loop { - let data = fetch().await; - tx.send(data).await; // If receiver is slow, queue grows -} - -// ✅ Handle channel full condition -loop { - let data = fetch().await; - if tx.send(data).await.is_err() { - break; // Receiver dropped - } -} -``` - -### 6. Not Handling Task Panics - -```rust -// ❌ Panic goes unnoticed -tokio::spawn(async { panic!("oops") }); - -// ✅ Handle join errors -let handle = tokio::spawn(async { risky_operation().await }); -match handle.await { - Ok(result) => process(result), - Err(e) if e.is_panic() => log::error!("Task panicked: {e}"), - Err(e) => log::error!("Task cancelled: {e}"), -} -``` - ---- - -## Performance Considerations - -### Runtime Selection - -| Runtime | Best For | -|---------|----------| -| Multi-thread (`rt-multi-thread`) | I/O-heavy servers, parallel work | -| Current-thread (`current_thread`) | Simpler apps, embedded, avoiding `Send` bounds | -| `rayon` | CPU-bound parallelism | - -### Memory Efficiency - -- **Avoid unnecessary `Arc` wrapping** - pass by reference when possible -- **Use `Box<[T]>` over `Vec`** for fixed-size allocations -- **Reuse buffers** in hot loops instead of allocating - -### Task Granularity - -- **Too coarse**: Poor utilization, one slow operation blocks others -- **Too fine**: Overhead from scheduling dominates actual work - -Rule of thumb: Tasks should represent logical units of work that can progress independently. - -### Avoiding Allocation in Hot Paths - -```rust -// ❌ Allocates on every iteration -loop { - let buffer = vec![0u8; 1024]; - socket.read(&mut buffer).await?; -} - -// ✅ Reuse buffer -let mut buffer = vec![0u8; 1024]; -loop { - let n = socket.read(&mut buffer).await?; - process(&buffer[..n]); -} -``` - ---- - -## Testing Async Code - -### Use `#[tokio::test]` - -```rust -#[tokio::test] -async fn test_async_function() { - let result = my_async_function().await; - assert_eq!(result, expected); -} -``` - -### Time Manipulation - -Fast-forward time in tests: - -```rust -#[tokio::test(start_paused = true)] -async fn test_timeout_behavior() { - // Time is paused - sleeps complete instantly - let start = std::time::Instant::now(); - tokio::time::sleep(Duration::from_secs(3600)).await; - assert!(start.elapsed() < Duration::from_millis(10)); -} -``` - -### Mocking I/O - -Use `tokio_test::io::Builder` for mocking: - -```rust -#[tokio::test] -async fn test_protocol() { - let reader = tokio_test::io::Builder::new() - .read(b"hello\r\n") - .build(); - let writer = tokio_test::io::Builder::new() - .write(b"world\r\n") - .build(); - - handle_connection(reader, writer).await.unwrap(); -} -``` - -### Testing Cancellation - -```rust -#[tokio::test] -async fn test_cancellation_cleanup() { - let (tx, rx) = oneshot::channel(); - - let handle = tokio::spawn(async move { - // Setup - let _guard = scopeguard::guard(tx, |tx| { - let _ = tx.send(()); // Signal cleanup happened - }); - - tokio::time::sleep(Duration::from_secs(100)).await; - }); - - // Cancel the task - handle.abort(); - - // Verify cleanup occurred - rx.await.expect("cleanup should have run"); -} -``` - ---- - -## When to Use Async vs Sync - -### Use Async When - -- **High concurrency** - Thousands of concurrent connections -- **I/O-bound workloads** - Network servers, database clients -- **External library requires it** - Many modern Rust libraries are async-first -- **Natural fit** - Event-driven architectures, websockets, streaming - -### Use Sync (Threads) When - -- **CPU-bound computation** - Use `rayon` for parallelism -- **Simple I/O patterns** - Reading a few files, one-shot HTTP requests -- **Simpler code matters** - Easier error handling, debugging -- **Blocking is unavoidable** - Legacy APIs, FFI - -### Hybrid Approach - -```rust -// CPU-bound work on rayon, communicate via channel -async fn process_with_rayon(data: Vec) -> Vec { - let (tx, rx) = tokio::sync::oneshot::channel(); - - rayon::spawn(move || { - let results: Vec<_> = data - .par_iter() - .map(|item| heavy_computation(item)) - .collect(); - let _ = tx.send(results); - }); - - rx.await.expect("rayon task completed") -} -``` - ---- - -## Quick Reference - -### Blocking Operations Cheat Sheet - -| Operation | Async Alternative | -|-----------|------------------| -| `std::thread::sleep` | `tokio::time::sleep` | -| `std::fs::*` | `tokio::fs::*` | -| `std::net::*` | `tokio::net::*` | -| `std::sync::Mutex` | `tokio::sync::Mutex` (if held across `.await`) | -| DNS lookup | `tokio::net::lookup_host` | -| Blocking FFI | `spawn_blocking` | - -### Concurrency Patterns Quick Reference - -| Pattern | Primitive | Use Case | -|---------|-----------|----------| -| Run sequentially | `a.await; b.await` | Dependent operations | -| Run concurrently, wait all | `join!` / `try_join!` | Independent operations | -| Run concurrently, first wins | `select!` | Racing, timeouts | -| Spawn independent work | `tokio::spawn` | Fire-and-forget, parallelism | -| Limit concurrency | `Semaphore` | Rate limiting | -| Process stream | `StreamExt::for_each_concurrent` | Bounded parallel processing | - ---- - -## Summary - -1. **Never block the runtime** - Use `spawn_blocking` for unavoidable blocking -2. **Choose concurrency primitives wisely** - `join!`, `select!`, channels, semaphores -3. **Handle errors and cancellation** - Use `Result`, timeouts, cancellation tokens -4. **Test with time control** - Use `start_paused = true` for deterministic tests -5. **Consider if you need async** - Threads are simpler when concurrency is low -6. **Implement graceful shutdown** - Use `CancellationToken` and `TaskTracker` -7. **Apply backpressure** - Use bounded channels to prevent memory exhaustion diff --git a/.llm/skills/async-rust.md b/.llm/skills/async-rust.md new file mode 100644 index 00000000..c3700c5e --- /dev/null +++ b/.llm/skills/async-rust.md @@ -0,0 +1,119 @@ + + +# Async Rust Best Practices + +## Golden Rule + +> Async code should never spend a long time without reaching an `.await`. + +Task switching only happens at `.await` points. Long-running code between awaits blocks the runtime thread. + +## Blocking Operations + +| Scenario | Solution | +|----------|----------| +| Short blocking (<100us) | Run inline | +| File system ops | `spawn_blocking` or `tokio::fs` | +| CPU-bound computation | `spawn_blocking` or `rayon` | +| Forever-running blocking | Dedicated `std::thread::spawn` | + +```rust +let result = tokio::task::spawn_blocking(|| expensive_sync_operation()).await?; +``` + +## Concurrency Patterns + +| Pattern | Primitive | Use Case | +|---------|-----------|----------| +| Sequential | `a.await; b.await` | Dependent operations | +| Concurrent, wait all | `join!` / `try_join!` | Independent operations | +| First wins | `select!` | Racing, timeouts | +| Spawn independent | `tokio::spawn` | Fire-and-forget | +| Limit concurrency | `Semaphore` | Rate limiting | +| Process stream | `StreamExt::for_each_concurrent` | Bounded parallel processing | +| Dynamic futures | `FuturesUnordered` / `JoinSet` | Variable number of tasks | + +## Timeouts + +```rust +match timeout(Duration::from_secs(5), fetch_data()).await { + Ok(result) => handle(result?), + Err(_) => return Err(Error::Timeout), +} +``` + +## Channel Selection + +| Channel | Use Case | +|---------|----------| +| `mpsc` | Multiple producers, single consumer | +| `oneshot` | Send exactly one value | +| `broadcast` | All consumers receive all messages | +| `watch` | Single changing value | + +## Synchronization + +- **Across `.await` points:** Use `tokio::sync::Mutex` +- **Quick synchronous access (no `.await` while held):** `std::sync::Mutex` is fine + +## Graceful Shutdown + +Use `CancellationToken` + `TaskTracker`: + +```rust +let token = CancellationToken::new(); +tokio::spawn(async move { + tokio::select! { + _ = token.cancelled() => { break; } + result = do_work() => { handle(result); } + } +}); +token.cancel(); // trigger shutdown +``` + +## Spawned Tasks Require `'static` + +Prefer owned data or `Arc` for shared read-only data. + +## Backpressure + +Use bounded channels (`mpsc::channel(100)`) to prevent memory exhaustion. Never `unbounded_channel` in production without good reason. + +## Common Pitfalls + +| Pitfall | Fix | +|---------|-----| +| `std::thread::sleep` in async | `tokio::time::sleep().await` | +| Forgetting to `.await` futures | Futures are lazy -- nothing happens without await | +| `std::sync::Mutex` held across `.await` | Use `tokio::sync::Mutex` or release before await | +| Spawning millions of tasks | Use `for_each_concurrent` with limit | +| Ignoring task panics | Check `JoinHandle` result | +| Dropping future cancels it | Use guards for cleanup | + +## Blocking Ops Cheat Sheet + +| std | Async Alternative | +|-----|------------------| +| `std::thread::sleep` | `tokio::time::sleep` | +| `std::fs::*` | `tokio::fs::*` | +| `std::net::*` | `tokio::net::*` | +| `std::sync::Mutex` (across await) | `tokio::sync::Mutex` | +| Blocking FFI | `spawn_blocking` | + +## Testing + +```rust +#[tokio::test] +async fn test_fn() { /* ... */ } + +#[tokio::test(start_paused = true)] +async fn test_timeout() { + // Time is paused -- sleeps complete instantly + tokio::time::sleep(Duration::from_secs(3600)).await; +} +``` + +## When to Use Async vs Sync + +**Async:** High concurrency, I/O-bound, event-driven, library requires it. +**Sync/Threads:** CPU-bound (use rayon), simple I/O, simpler debugging, blocking FFI. diff --git a/.llm/skills/binary-size.md b/.llm/skills/binary-size.md new file mode 100644 index 00000000..2592ba52 --- /dev/null +++ b/.llm/skills/binary-size.md @@ -0,0 +1,202 @@ + + + +# Binary Size Optimization + +--- + +## Quick Profiles + +### Stable Rust -- Balanced + +```toml +[profile.release] +opt-level = "z" # Optimize for size (try "s" too -- sometimes smaller) +lto = true # Link-time optimization +codegen-units = 1 # Better optimization +panic = "abort" # Remove unwinding machinery +strip = true # Strip symbols +``` + +### Nightly -- Maximum Reduction + +```bash +RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none" cargo +nightly build \ + -Z build-std=std,panic_abort \ + -Z build-std-features="optimize_for_size" \ + --target x86_64-unknown-linux-gnu --release +``` + +--- + +## Techniques by Complexity + +| Technique | Rust | Impact | Behavior Change | +|-----------|------|--------|-----------------| +| Release build | 1.0+ | 60-70% | No | +| `strip = true` | 1.59+ | 50-80% | No | +| `opt-level = "z"` or `"s"` | 1.28+ | 10-30% | No | +| `lto = true` ("fat") | 1.0+ | 10-20% | No | +| `codegen-units = 1` | 1.0+ | 5-15% | No | +| `panic = "abort"` | 1.10+ | 10-20% | Yes: no unwinding | +| `-Zlocation-detail=none` | Nightly | 5-15% | No panic location | +| `-Zfmt-debug=none` | Nightly | 5-15% | No `{:?}` output | +| `-Z build-std` | Nightly | 20-50% | No | +| `-Cpanic=immediate-abort` | Nightly | 10-30% | No panic message | +| UPX compression | Post | 50-70% | AV flags, startup | +| `#![no_std]` | 1.30+ | ~10KB possible | Limited API | + +Always try BOTH `"s"` and `"z"` -- depending on code, `"s"` can be smaller. + +--- + +## Dependency Optimization + +### Heavy vs Light Alternatives + +| Category | Heavy | Lighter Alternative | +|----------|-------|---------------------| +| CLI parsing | `clap` | `lexopt`, `pico-args`, `argh` | +| Serialization | `serde` | `miniserde`, `nanoserde` | +| HTTP | `reqwest` | `ureq`, `minreq` | +| Async runtime | `tokio` | `smol`, `embassy` | +| Regex | `regex` | `regex-lite`, `memchr` | +| Error handling | `anyhow` | `thiserror` only | + +### Minimize Features + +```toml +serde = { version = "1.0", default-features = false, features = ["derive"] } +tokio = { version = "1.0", features = ["rt", "net"] } # Not "full" +``` + +### Analysis Tools + +```bash +cargo bloat --release --crates # Size by crate +cargo bloat --release -n 30 # Top 30 functions +cargo llvm-lines --release | head -20 # Generic bloat +# WASM +twiggy top -n 20 target/wasm32-unknown-unknown/release/my_lib.wasm +``` + +--- + +## WASM-Specific + +```toml +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true +``` + +```bash +cargo build --release --target wasm32-unknown-unknown +wasm-opt -Oz -o optimized.wasm target/wasm32-unknown-unknown/release/my_lib.wasm +``` + +--- + +## Platform Notes + +### Windows: MSVC vs GNU + +MSVC produces significantly smaller binaries (~10MB vs ~100MB for GNU/MinGW). Prefer MSVC for releases. + +### Linux: musl vs glibc + +| Target | Trade-off | +|--------|-----------| +| `*-linux-gnu` | Smaller binary, needs glibc at runtime | +| `*-linux-musl` | Larger binary, fully portable static | + +--- + +## Container Optimization + +```dockerfile +FROM rust:1.83-alpine AS builder +RUN apk add --no-cache musl-dev +WORKDIR /app +COPY . . +RUN cargo build --release --target x86_64-unknown-linux-musl && \ + strip target/x86_64-unknown-linux-musl/release/myapp + +FROM scratch +COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp +ENTRYPOINT ["/myapp"] +``` + +| Base Image | Size | +|------------|------| +| `debian:bookworm` | ~130 MB | +| `alpine:3` | ~7 MB | +| `distroless/static` | ~2 MB | +| `scratch` | 0 MB | + +--- + +## Checklist + +### Stable (No Behavior Changes) + +- [ ] Build with `--release` +- [ ] `strip = true` +- [ ] Try both `opt-level = "z"` and `"s"` +- [ ] `lto = true` +- [ ] `codegen-units = 1` + +### Stable (With Behavior Changes) + +- [ ] `panic = "abort"` (if unwinding not needed) + +### Nightly + +- [ ] `-Zlocation-detail=none` +- [ ] `-Zfmt-debug=none` +- [ ] `-Z build-std=std,panic_abort` +- [ ] `-Z build-std-features="optimize_for_size"` + +### Dependencies + +- [ ] `cargo bloat --crates` to audit heavy deps +- [ ] Disable default features +- [ ] Consider lighter alternatives +- [ ] `cargo unused-features analyze` + +### Post-Build + +- [ ] UPX compression (if appropriate) +- [ ] Verify binary runs correctly + +--- + +## Example Journey + +| Step | Size | Reduction | +|------|------|-----------| +| Debug build | 4.2 MB | -- | +| Release | 410 KB | 90% | +| + strip | 310 KB | 93% | +| + opt-level=z | 290 KB | 93% | +| + LTO | 250 KB | 94% | +| + codegen-units=1 | 240 KB | 94% | +| + panic=abort | 180 KB | 96% | +| + build-std (nightly) | 51 KB | 99% | +| + UPX | 24 KB | 99.4% | + +--- + +## Tools + +| Tool | Purpose | +|------|---------| +| `cargo-bloat` | Find what takes space | +| `cargo-llvm-lines` | Find generic bloat | +| `cargo-unused-features` | Find unused features | +| `twiggy` | WASM code size profiler | +| `wasm-opt` | WASM binary optimizer | +| UPX | Executable packer | diff --git a/.llm/skills/changelog-practices.md b/.llm/skills/changelog-practices.md deleted file mode 100644 index 32be4f7f..00000000 --- a/.llm/skills/changelog-practices.md +++ /dev/null @@ -1,435 +0,0 @@ -# Changelog Practices — Documenting User-Observable Changes - -> **When in doubt, add a CHANGELOG entry.** Users benefit from knowing what changed. - -## TL;DR — When to Update CHANGELOG - -**ALWAYS document:** - -- Public API changes (new functions, changed signatures, removed items) -- Behavior changes (different output for same input) -- Default value changes -- Error type/message changes users might match on -- Performance improvements users would notice -- Bug fixes affecting user-visible behavior -- New trait implementations (`Display`, `Debug`, `Hash`, `Serialize`, etc.) - -**NEVER document:** - -- Internal refactoring with no external effect -- `pub(crate)` or private API changes -- Test-only changes -- Documentation typo fixes -- CI/tooling changes (unless affecting published crate) - ---- - -## The Golden Rule - -**If a user's code could behave differently, or they need to change their code, document it.** - -```rust -// ❌ Internal change — NO changelog needed -pub(crate) fn internal_helper() -> Result<(), Error> { ... } - -// ✅ Public API change — MUST document -pub fn public_function() -> Result<(), Error> { ... } -``` - ---- - -## What Requires Documentation - -### Breaking Changes (MUST document) - -Always document under `### Changed` with **Breaking:** prefix: - -```markdown -### Changed - -- **Breaking:** `SessionBuilder::with_input_delay()` now returns `Result` instead of panicking on invalid values -``` - -Examples: - -- Changing function signatures -- Removing public items -- Adding required trait bounds -- Changing return types -- Changing default behavior -- **Adding enum variants to exhaustively matchable enums** (see below) -- **Changing `Display` or `Debug` output format** (see "Output Format Changes" below) - -### Enum Variants Are Breaking Changes (Unless `#[non_exhaustive]`) - -**Critical:** Adding a new variant to a public enum is a **breaking change** if users can exhaustively match on it. - -```rust -// ❌ Exhaustively matchable — adding variants is BREAKING -pub enum ConnectionState { - Disconnected, - Connecting, - Connected, -} - -// ✅ Non-exhaustive — adding variants is NOT breaking -#[non_exhaustive] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected, -} -``` - -**Why it breaks:** Users with exhaustive matches will get compile errors: - -```rust -// User's code that breaks when you add a variant -match state { - ConnectionState::Disconnected => { ... } - ConnectionState::Connecting => { ... } - ConnectionState::Connected => { ... } - // ERROR: non-exhaustive patterns: `NewVariant` not covered -} -``` - -**CHANGELOG entries for new enum variants:** - -```markdown -# ❌ WRONG — Listed as "Added" but it's breaking -### Added -- `ConnectionState::Syncing` variant for synchronization phase - -# ✅ CORRECT — Marked as breaking with migration guidance -### Changed -- **Breaking:** Added `ConnectionState::Syncing` variant. Update exhaustive matches to handle this new state. -``` - -**Prevention:** When creating public enums that may grow, use `#[non_exhaustive]`: - -```rust -/// Connection states for peer sessions. -/// -/// This enum is `#[non_exhaustive]`; new variants may be added -/// in future versions without a breaking change. -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected, -} -``` - -> **See also:** [workspace-organization.md](workspace-organization.md) for `#[non_exhaustive]` best practices. - -### Trait Implementations (SHOULD document) - -New implementations of standard traits on public types **should be documented** under `### Added`: - -```markdown -### Added - -- `Display` implementation for `Frame` and `PlayerHandle` for readable formatting -- `Hash` implementation for `SessionConfig` to enable use in collections -``` - -**Why trait impls matter to users:** - -- `Display` — Users can use `{}` formatting, `to_string()`, and error messages -- `Debug` — Users can use `{:?}` formatting and debugging tools -- `Hash` — Users can use the type as `HashMap`/`HashSet` keys -- `Serialize`/`Deserialize` — Users can persist or transmit values -- `Clone`/`Copy` — Changes how users can work with the type - -### Output Format Changes (MUST document as Breaking) - -**Critical:** Changing the output of `Display` or `Debug` implementations is a **breaking change** if users might depend on the format. - -```rust -// ❌ Format change — BREAKING if users parse/match output -// Before: "Player 0" -// After: "PlayerHandle(0)" -impl Display for PlayerHandle { ... } -``` - -**Why format changes break user code:** - -- Log parsing scripts may match on specific patterns -- Tests may assert on formatted output -- Error messages containing formatted types change -- Serialization or display in UIs may break - -**CHANGELOG entry for format changes:** - -```markdown -### Changed - -- **Breaking:** `PlayerHandle` now displays as `PlayerHandle(N)` instead of `Player N`. Update any code that parses or matches the previous format. -``` - -**When format changes are NOT breaking:** - -- The type is new (no existing users) -- The previous format was explicitly documented as unstable -- The change only affects `Debug` AND the type documents that `Debug` output is not stable - -**Best practice:** If you want flexibility to change formats, document that the format is not stable: - -```rust -/// Note: The exact format of the `Display` output is not stable -/// and may change in future versions. -impl Display for InternalId { ... } -``` - -### New Features (SHOULD document) - -Document under `### Added`: - -```markdown -### Added - -- `ProtocolConfig::deterministic(seed)` preset for reproducible sessions -- `SessionBuilder::with_event_queue_size()` for configurable queue capacity -``` - -### Bug Fixes (SHOULD document) - -Document under `### Fixed`: - -```markdown -### Fixed - -- Fixed crash when misprediction detected at frame 0 -- Fixed sync timeout event flooding under certain conditions -``` - -### Behavioral Changes (MUST document) - -Even if not breaking, document observable changes: - -```markdown -### Changed - -- Desync detection now enabled by default (`DesyncDetection::On { interval: 60 }`) -- Reduced memory allocation in network hot paths -``` - ---- - -## What Does NOT Require Documentation - -### Internal Implementation Changes - -```rust -// Before: assert that panics -pub(crate) fn synchronize(&mut self) { - assert_eq!(self.state, ProtocolState::Initializing); - // ... -} - -// After: returns Result -pub(crate) fn synchronize(&mut self) -> Result<(), FortressError> { - if self.state != ProtocolState::Initializing { - return Err(FortressError::InvalidRequest { ... }); - } - // ... -} -``` - -This is `pub(crate)` — users never see it. No changelog entry needed. - -### Test Changes - -```rust -#[cfg(test)] -mod tests { - // Any changes here — no changelog needed -} -``` - -### CI/Tooling Changes - -Unless they affect the published crate: - -- Workflow updates -- Linter configuration -- Development dependencies - ---- - -## CHANGELOG Format - -Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format: - -```markdown -## [Unreleased] - -### Added -- New features - -### Changed -- Changes to existing functionality -- **Breaking:** prefix for breaking changes - -### Deprecated -- Soon-to-be removed features - -### Removed -- Removed features - -### Fixed -- Bug fixes - -### Security -- Vulnerability fixes -``` - ---- - -## Entry Writing Guidelines - -### Be User-Focused - -```markdown -# ❌ Too technical/internal -- Replaced HashMap with BTreeMap in sync_manager.rs - -# ✅ User-focused -- Improved iteration order determinism in session state -``` - -### Be Concise - -```markdown -# ❌ Too verbose -- Added a new method called `with_event_queue_size` to the `SessionBuilder` struct - that allows users to configure the size of the event queue capacity, which - determines how many events can be buffered before... - -# ✅ Concise -- `SessionBuilder::with_event_queue_size()` for configurable event queue capacity -``` - -### Include Context for Breaking Changes - -```markdown -# ❌ Missing migration guidance -- **Breaking:** Changed `with_input_delay()` return type - -# ✅ Includes guidance -- **Breaking:** `SessionBuilder::with_input_delay()` now returns `Result` instead of silently clamping invalid values -``` - ---- - -## Visibility Reference - -| Visibility | User-Facing? | Changelog Needed? | -|------------|--------------|-------------------| -| `pub` | Yes | Yes, for any change | -| `pub(crate)` | No | No | -| `pub(super)` | No | No | -| `pub(in path)` | No | No | -| private | No | No | - ---- - -## Checklist Before Committing - -When making changes, ask: - -1. **Is this `pub`?** If yes, consider changelog entry -2. **Does behavior change?** If yes, document it -3. **Could user code break?** If yes, mark as **Breaking:** -4. **Adding enum variants?** Check if enum is `#[non_exhaustive]` — if not, it's **Breaking:** -5. **Is this a bug fix users would care about?** If yes, document it -6. **Adding trait impls?** `Display`, `Debug`, `Hash`, `Serialize` — document under Added -7. **Is this purely internal?** If yes, skip changelog - ---- - -## Example: Determining Changelog Need - -```rust -// Change: assert! to Result - -// Case 1: Public method -pub fn connect(&mut self) -> Result<(), Error> { ... } -// → YES, document: "**Breaking:** `connect()` now returns Result" - -// Case 2: pub(crate) method -pub(crate) fn synchronize(&mut self) -> Result<(), Error> { ... } -// → NO, internal only - -// Case 3: Public method, behavior change only -pub fn validate(&self) -> bool { - // Was: only checked field A - // Now: checks fields A and B -} -// → YES, document: "Changed: `validate()` now checks additional fields" -``` - ---- - -## Example Code Maintenance - -When documenting API changes, also update example code: - -### Locations to Check - -| Location | Purpose | -|----------|--------| -| `examples/*.rs` | Runnable standalone examples | -| `README.md` | Quick start code snippets | -| `docs/user-guide.md` | Detailed usage examples | -| Rustdoc `# Examples` | Inline documentation examples | - -### Verification Commands - -```bash -# Ensure all examples compile -cargo build --examples - -# Ensure rustdoc examples compile -cargo test --doc - -# Find references to changed APIs -rg 'changed_function|ChangedStruct' --type rust --type md -``` - -### Why This Matters - -- Outdated examples confuse users and erode trust -- Broken examples in README are often the first impression -- CI may not catch example drift if examples aren't compiled - -**Rule:** If you change a `pub` API, search the codebase for all usages before committing. - ---- - -## Verification Before Committing - -**Always verify CHANGELOG claims match actual code:** - -```bash -# Verify derives exist before claiming them -rg '#\[derive.*Hash' src/lib.rs - -# Verify method/type exists -rg 'pub fn method_name|pub struct TypeName' --type rust - -# Build docs to catch broken links -RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -``` - -> **See also:** [documentation-code-consistency.md](documentation-code-consistency.md) for comprehensive verification commands and common pitfalls. - ---- - -## References - -- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -- [Semantic Versioning](https://semver.org/) -- [Rust API Guidelines - Documentation](https://rust-lang.github.io/api-guidelines/documentation.html) -- [documentation-code-consistency.md](documentation-code-consistency.md) — Keeping docs and code in sync diff --git a/.llm/skills/changelog.md b/.llm/skills/changelog.md new file mode 100644 index 00000000..4b420b05 --- /dev/null +++ b/.llm/skills/changelog.md @@ -0,0 +1,153 @@ + + +# Changelog Practices + +## Decision Table + +| Change Type | Changelog? | Section | Prefix | +|-------------|-----------|---------|--------| +| New public function/type/trait impl | Yes | Added | | +| Changed function signature | Yes | Changed | **Breaking:** | +| Removed public item | Yes | Removed | **Breaking:** | +| New enum variant (exhaustive enum) | Yes | Changed | **Breaking:** | +| New enum variant (`#[non_exhaustive]`) | Yes | Added | | +| Changed `Display`/`Debug` output | Yes | Changed | **Breaking:** | +| Bug fix (user-visible) | Yes | Fixed | | +| Default value change | Yes | Changed | | +| Performance improvement (noticeable) | Yes | Changed | | +| New trait impl (`Display`, `Hash`, etc.) | Yes | Added | | +| `pub(crate)` or private changes | No | -- | -- | +| Test-only changes | No | -- | -- | +| CI/tooling changes | No | -- | -- | +| Internal refactoring | No | -- | -- | +| Doc typo fixes | No | -- | -- | + +**Golden rule:** If a user's code could behave differently or they need to change their code, document it. + +## Visibility Reference + +| Visibility | User-Facing? | Changelog? | +|------------|--------------|------------| +| `pub` | Yes | Yes | +| `pub(crate)` | No | No | +| `pub(super)` | No | No | +| private | No | No | + +## Format (Keep a Changelog) + +```markdown +## [Unreleased] + +### Added +- `ProtocolConfig::deterministic(seed)` preset for reproducible sessions + +### Changed +- **Breaking:** `SessionBuilder::with_input_delay()` now returns `Result` instead of panicking on invalid values + +### Fixed +- Fixed crash when misprediction detected at frame 0 +``` + +Sections: Added, Changed, Deprecated, Removed, Fixed, Security. + +## Writing Guidelines + +### Be User-Focused + +```markdown +# WRONG: too internal +- Replaced HashMap with BTreeMap in sync_manager.rs + +# CORRECT +- Improved iteration order determinism in session state +``` + +### Be Concise + +```markdown +# WRONG: verbose +- Added a new method called `with_event_queue_size` to the `SessionBuilder` struct... + +# CORRECT +- `SessionBuilder::with_event_queue_size()` for configurable event queue capacity +``` + +### Include Migration for Breaking Changes + +```markdown +# WRONG: no guidance +- **Breaking:** Changed `with_input_delay()` return type + +# CORRECT +- **Breaking:** `SessionBuilder::with_input_delay()` now returns `Result` instead of silently clamping invalid values +``` + +## Enum Variants Are Breaking (Unless `#[non_exhaustive]`) + +```rust +// Adding variants to this is BREAKING +pub enum ConnectionState { Disconnected, Connecting, Connected } + +// Adding variants to this is NOT breaking +#[non_exhaustive] +pub enum ConnectionState { Disconnected, Connecting, Connected } +``` + +CHANGELOG entry for new variant on exhaustive enum: + +```markdown +### Changed +- **Breaking:** Added `ConnectionState::Syncing` variant. Update exhaustive matches. +``` + +## Trait Implementations + +Document under Added: + +```markdown +### Added +- `Display` implementation for `Frame` and `PlayerHandle` +- `Hash` implementation for `SessionConfig` +``` + +## Output Format Changes + +Changing `Display`/`Debug` output is **Breaking** if users might depend on format: + +```markdown +### Changed +- **Breaking:** `PlayerHandle` now displays as `PlayerHandle(N)` instead of `Player N` +``` + +## Verification Before Committing + +```bash +# Verify derives exist before claiming them +rg '#\[derive.*Hash' src/lib.rs + +# Verify method/type exists +rg 'pub fn method_name|pub struct TypeName' --type rust + +# Build docs to catch broken links +RUSTDOCFLAGS="-D warnings" cargo doc --no-deps +``` + +## Example Code Maintenance + +When documenting API changes, also update examples: + +```bash +cargo build --examples # Verify examples compile +cargo test --doc # Verify doc examples +rg 'changed_function|ChangedStruct' --type rust --type md # Find stale refs +``` + +Locations: `examples/*.rs`, `README.md`, `docs/user-guide.md`, rustdoc `# Examples`. + +## Internal-Only Release + +If a release has no user-facing changes, add a single summary line: + +```markdown +- Internal: Improved test coverage and code organization +``` diff --git a/.llm/skills/ci-cd-debugging.md b/.llm/skills/ci-cd-debugging.md deleted file mode 100644 index bdf879dd..00000000 --- a/.llm/skills/ci-cd-debugging.md +++ /dev/null @@ -1,542 +0,0 @@ -# CI/CD Debugging Guide - -> **A practical guide to reproducing and debugging CI failures locally.** - -## Overview - -CI failures are frustrating when you cannot reproduce them locally. This guide covers systematic approaches to debug common CI failure categories in this project. - ---- - -## General Debugging Approach - -### Step 1: Read the Full Error Message - -Before attempting to reproduce locally, carefully read the entire CI error output: - -1. **Scroll up** past the obvious error to find root causes -2. **Check for cascading failures** that obscure the original problem -3. **Note the exact command** that failed and any environment variables -4. **Look for timestamps** to identify timeouts vs. immediate failures - -### Step 2: Identify the Failure Category - -| Error Pattern | Category | See Section | -|---------------|----------|-------------| -| `cargo fmt --check` fails | Formatting | [Formatting Failures](#formatting-failures) | -| Clippy warnings | Linting | [Clippy Failures](#clippy-failures) | -| Test assertions fail | Test logic | [Test Failures](#test-failures) | -| `cargo kani` verification fails | Formal verification | [Kani Failures](#kani-failures) | -| Cross-compile errors | Cross-compilation | [Cross-Compilation Failures](#cross-compilation-failures) | -| Docker/container issues | Container builds | [Container Failures](#container-failures) | -| Link errors (undefined reference) | Linker | [Linker Failures](#linker-failures) | -| Timeout | Performance/hang | [Timeout Failures](#timeout-failures) | -| `actionlint` errors | Workflow syntax | [Workflow Failures](#workflow-failures) | -| Vale/markdownlint errors | Documentation | [Documentation Failures](#documentation-failures) | - -### Step 3: Reproduce Locally - -**Golden rule:** Run the EXACT same command as CI, with the same environment. - -```bash -# Check what CI ran in the workflow YAML -cat .github/workflows/ci-*.yml | grep "run:" - -# Run locally with same flags -cargo clippy --all-targets -- -D warnings -RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -``` - ---- - -## Formatting Failures - -### Symptoms - -``` -error: diff in src/lib.rs -``` - -### Local Reproduction - -```bash -# Check what needs formatting -cargo fmt --check - -# Apply fixes -cargo fmt -``` - -### Common Causes - -- Committed without running `cargo fmt` -- Editor auto-format with different settings -- Different Rust toolchain version - ---- - -## Clippy Failures - -### Symptoms - -``` -error: unused variable - --> src/lib.rs:42:9 -``` - -### Local Reproduction - -```bash -# Run exactly as CI does -cargo clippy --all-targets -- -D warnings - -# Or use the project alias -cargo c -``` - -### Common Causes - -- New code triggering lints -- Clippy version difference between local and CI -- Missing `#[allow(...)]` for intentional patterns - -### Fixing - -```bash -# See all available lints for a warning -cargo clippy --explain LINT_NAME - -# Allow specific lint locally if justified -#[allow(clippy::lint_name)] // Reason why this is acceptable -``` - ---- - -## Test Failures - -### Symptoms - -``` -thread 'test_name' panicked at src/lib.rs:42:9 -assertion `left == right` failed - left: 10 - right: 20 -``` - -### Local Reproduction - -```bash -# Run the specific failing test -cargo test test_name -- --nocapture - -# Or with nextest for better output -cargo nextest run test_name --no-capture -``` - -### Common Causes - -- Platform-specific behavior (timing, random order) -- Missing test fixtures or data -- Environment variable differences - -### Debugging Tips - -```bash -# Run tests with verbose output -RUST_BACKTRACE=1 cargo test test_name -- --nocapture - -# Run tests in single-threaded mode (eliminates race conditions) -cargo test -- --test-threads=1 - -# Run with specific seed for deterministic failures -PROPTEST_CASES=100 cargo test -``` - ---- - -## Kani Failures - -### Symptoms - -``` -VERIFICATION RESULT: FAILURE -Check 1: proof_my_function.assertion.1 - - Status: FAILURE - - Description: assertion failed -``` - -### Local Reproduction - -```bash -# Run the specific failing proof -cargo kani --harness proof_function_name - -# Run all proofs -cargo kani -``` - -### Common Causes - -1. **Missing `#[kani::unwind(N)]`** for loops with symbolic bounds -2. **Proof assertions don't match implementation** (most common) -3. **Overflow in arithmetic** not handled -4. **Proof not registered** in tier lists - -### Debugging Tips - -```bash -# Get verbose output -cargo kani --harness proof_name --verbose - -# Generate HTML visualization -cargo kani --harness proof_name --visualize - -# Check if proof is registered -./scripts/check-kani-coverage.sh -``` - -### Critical Check: Assertion Correctness - -When a Kani proof fails, **first verify the assertion itself is correct**: - -```rust -// Read the implementation -impl Default for MyType { - fn default() -> Self { - MyType::VariantA // What does it ACTUALLY return? - } -} - -// Then check if the proof's assertion matches -#[kani::proof] -fn proof_default() { - let x = MyType::default(); - // Is this assertion correct? - assert!(matches!(x, MyType::VariantA)); // Must match impl! -} -``` - ---- - -## Cross-Compilation Failures - -### Symptoms - -``` -error: linker `cc` not found -error: could not find native library `ssl` -``` - -### Local Reproduction - -```bash -# Install cross-rs -cargo install cross --git https://github.com/cross-rs/cross - -# Run same target as CI -cross build --target aarch64-unknown-linux-gnu -``` - -### Common Causes - -1. **Unstable image tags** (`:main`, `:edge`) in Cross.toml -2. **Missing linker** for target architecture -3. **Environment variable passthrough** not configured - -### Debugging Tips - -```bash -# Check Cross.toml configuration -cat Cross.toml - -# Run with verbose Docker output -CROSS_DEBUG=1 cross build --target aarch64-unknown-linux-gnu - -# Override linker via environment -CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ - cross build --target aarch64-unknown-linux-gnu -``` - -### Image Tag Stability - -**Never use `:main` or `:edge` tags.** Use environment variable passthrough instead: - -```toml -# Cross.toml -[build.env] -passthrough = [ - "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER", -] -``` - ---- - -## Container Failures - -### Symptoms - -``` -docker: Error response from daemon: pull access denied -error: failed to run custom build command -``` - -### Local Reproduction - -```bash -# Ensure Docker is running -docker ps - -# Pull the image CI uses -docker pull ghcr.io/cross-rs/aarch64-unknown-linux-gnu:latest - -# Run build in container -cross build --target aarch64-unknown-linux-gnu -``` - -### Common Causes - -- Docker not running locally -- Image tag changed upstream -- Rate limiting on container registry - ---- - -## Linker Failures - -### Symptoms - -``` -error: linking with `cc` failed -undefined reference to `some_symbol` -``` - -### Local Reproduction - -```bash -# Check what linker is being used -cargo build -vv 2>&1 | grep "Running" - -# Ensure linker is installed -which aarch64-linux-gnu-gcc -``` - -### Common Causes - -- Wrong linker for target -- Missing system libraries -- `.cargo/config.toml` linker settings not matching CI - -### Fixing - -```toml -# .cargo/config.toml -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" -``` - ---- - -## Timeout Failures - -### Symptoms - -``` -Error: The operation was canceled. -##[error]The job running on runner ... has exceeded the maximum execution time -``` - -### Local Reproduction - -```bash -# Run with timeout to simulate CI -timeout 600 cargo kani --harness proof_name - -# Profile build time -cargo build --timings -``` - -### Common Causes - -1. **Missing `#[kani::unwind(N)]`** causing Kani to hang -2. **Excessive test count** in integration tests -3. **Network timeouts** in tests that hit external services - -### Debugging Tips - -```bash -# For Kani hangs, add unwind bounds -#[kani::proof] -#[kani::unwind(11)] # Max iterations + 1 -fn proof_with_loop() { - let n: usize = kani::any(); - kani::assume(n <= 10); - for _ in 0..n { /* ... */ } -} -``` - ---- - -## Workflow Failures - -### Symptoms - -``` -Error: .github/workflows/ci.yml: unexpected key "on" for mapping -``` - -### Local Reproduction - -```bash -# Run actionlint on all workflows -actionlint - -# Check specific workflow -actionlint .github/workflows/ci-rust.yml -``` - -### Common Causes - -- YAML syntax errors -- Invalid GitHub Actions expressions -- Shellcheck warnings in run blocks - -### Debugging Tips - -```bash -# Test workflow locally with act -brew install act -act push -j build --dryrun - -# Validate expressions -echo '${{ github.event_name }}' | yq -``` - ---- - -## Documentation Failures - -### Symptoms - -``` -error: unresolved link to `SomeType` -warning: Passive voice -``` - -### Local Reproduction - -```bash -# Rustdoc link check (exact CI command) -RUSTDOCFLAGS="-D warnings" cargo doc --no-deps - -# Vale prose linting -vale sync -vale docs/ - -# Markdownlint -npx markdownlint '**/*.md' --config .markdownlint.json -``` - -### Common Causes - -- Broken intra-doc links -- Missing link reference definitions -- Vale style rule violations - ---- - -## Environment Differences Checklist - -When CI fails but local passes, check these differences: - -| Factor | Check Command | Common Issue | -|--------|---------------|--------------| -| Rust version | `rustc --version` | CI uses toolchain from `rust-toolchain.toml` | -| Toolchain | `rustup show` | Missing components (clippy, rustfmt) | -| Environment variables | `env \| grep CARGO` | CI sets `CARGO_TERM_COLOR`, etc. | -| Working directory | `pwd` | Tests assume specific cwd | -| File permissions | `ls -la` | Scripts need execute permission | -| Git state | `git status` | Uncommitted changes affect builds | -| Cache | N/A | CI has clean cache, local has stale | - -### Simulate Clean CI Environment - -```bash -# Clean all build artifacts -cargo clean - -# Clean cargo registry cache (careful - slow to rebuild) -rm -rf ~/.cargo/registry/cache - -# Run with no local config -CARGO_HOME=$(mktemp -d) cargo build -``` - ---- - -## CI Log Interpretation - -### GitHub Actions Log Navigation - -1. Click on failing job name in CI summary -2. Expand the failing step -3. Click "Search logs" (magnifying glass) to find specific errors -4. Look for `##[error]` prefixed lines - -### Common Log Patterns - -| Pattern | Meaning | -|---------|---------| -| `##[error]` | Explicit error from action | -| `error[E...]` | Rust compiler error code | -| `FAILED` | Test or verification failure | -| `panicked at` | Rust panic occurred | -| `timeout` | Step exceeded time limit | -| `exit code 1` | Command failed | - -### Getting More Debug Output - -Add to workflow for verbose logging: - -```yaml -env: - CARGO_TERM_VERBOSE: true - RUST_BACKTRACE: 1 -``` - -Or re-run with debug logging enabled in GitHub UI. - ---- - -## Quick Reference - -### Most Common CI Fixes - -| CI Failure | Quick Fix | -|------------|-----------| -| Formatting | `cargo fmt` | -| Clippy | `cargo clippy --fix --allow-dirty` | -| Doc links | Add link reference definition | -| Kani timeout | Add `#[kani::unwind(N)]` | -| Kani assertion | Verify assertion matches implementation | -| Cross-compile | Check Cross.toml for unstable tags | -| actionlint | Run `actionlint` and fix syntax | - -### Commands to Run Before Every Commit - -```bash -# Format + lint + test (project aliases) -cargo c && cargo t - -# For documentation changes -cargo doc --no-deps - -# For workflow changes -actionlint - -# For markdown changes -npx markdownlint '**/*.md' --config .markdownlint.json --fix -``` - ---- - -*Systematic debugging saves hours of frustration.* diff --git a/.llm/skills/ci-debugging.md b/.llm/skills/ci-debugging.md new file mode 100644 index 00000000..e34e3310 --- /dev/null +++ b/.llm/skills/ci-debugging.md @@ -0,0 +1,133 @@ + + +# CI/CD Debugging Guide + +## Failure Category Quick Reference + +| Error Pattern | Category | Quick Fix | +|---------------|----------|-----------| +| `cargo fmt --check` fails | Formatting | `cargo fmt` | +| Clippy warnings | Linting | `cargo clippy --fix --allow-dirty` | +| Test assertion failures | Test logic | `RUST_BACKTRACE=1 cargo test name -- --nocapture` | +| `VERIFICATION RESULT: FAILURE` | Kani | Verify assertion matches impl; add `#[kani::unwind(N)]` | +| `linker cc not found` | Cross-compilation | Check Cross.toml, avoid unstable image tags | +| `invalid linker name in argument '-fuse-ld=lld'` | Missing linker | Install lld, or use `cargo_linker.get_cargo_env()` fallback | +| Timeout / cancelled | Performance | Add `#[kani::unwind(N)]`; increase `timeout-minutes` | +| `actionlint` errors | Workflow syntax | Run `actionlint` locally | +| `unresolved link` | Documentation | Add link reference definition | +| Markdownlint errors | Markdown | `npx markdownlint --fix '**/*.md'` | + +## Step 1: Read the Full Error + +1. Scroll UP past the obvious error to find root causes +2. Check for cascading failures that obscure the original problem +3. Note the exact command and environment variables +4. Look for timestamps to distinguish timeouts from immediate failures + +## Step 2: Reproduce Locally + +Run the EXACT same command as CI: + +```bash +# Check what CI ran +cat .github/workflows/ci-*.yml | grep "run:" + +# Common reproductions +cargo fmt --check +cargo clippy --all-targets -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc --no-deps +cargo nextest run test_name --no-capture +cargo kani --harness proof_function_name +actionlint +``` + +## Specific Failure Types + +### Kani Failures + +```bash +cargo kani --harness proof_name --verbose +./scripts/check-kani-coverage.sh # Check registration +``` + +Common causes: +1. Missing `#[kani::unwind(N)]` for loops (N = max_iterations + 1) +2. Proof assertions don't match implementation +3. Proof not registered in tier lists + +Always verify the assertion itself is correct -- check what the code actually returns. + +### Cross-Compilation Failures + +```bash +cargo install cross --git https://github.com/cross-rs/cross +CROSS_DEBUG=1 cross build --target aarch64-unknown-linux-gnu +``` + +Never use `:main` or `:edge` image tags. Use environment variable passthrough: + +```toml +# Cross.toml +[build.env] +passthrough = ["CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER"] +``` + +### Timeout Failures + +```bash +timeout 600 cargo kani --harness proof_name +cargo build --timings # Profile build time +``` + +### Test Failures + +```bash +RUST_BACKTRACE=1 cargo test test_name -- --nocapture +cargo test -- --test-threads=1 # Eliminate race conditions +``` + +### Documentation Failures + +```bash +RUSTDOCFLAGS="-D warnings" cargo doc --no-deps +vale sync && vale docs/ +npx markdownlint '**/*.md' --config .markdownlint.json +``` + +## Environment Differences Checklist + +| Factor | Check Command | Common Issue | +|--------|---------------|--------------| +| Rust version | `rustc --version` | Mismatch with `rust-toolchain.toml` | +| Toolchain | `rustup show` | Missing components | +| Env vars | `env \| grep CARGO` | CI sets `CARGO_TERM_COLOR` etc. | +| Cache | N/A | CI has clean cache, local has stale | +| Git state | `git status` | Uncommitted changes | + +### Simulate Clean CI + +```bash +cargo clean +CARGO_HOME=$(mktemp -d) cargo build +``` + +## CI Log Patterns + +| Pattern | Meaning | +|---------|---------| +| `##[error]` | Explicit error from action | +| `error[E...]` | Rust compiler error code | +| `panicked at` | Rust panic occurred | +| `timeout` | Step exceeded time limit | +| `exit code 1` | Command failed | + +Enable debug logging: set repo variable `ACTIONS_STEP_DEBUG=true`. + +## Commands Before Every Commit + +```bash +cargo c && cargo t # Format + lint + test +cargo doc --no-deps # Doc changes +actionlint # Workflow changes +npx markdownlint '**/*.md' --config .markdownlint.json --fix # Markdown changes +``` diff --git a/.llm/skills/clippy-configuration.md b/.llm/skills/clippy-configuration.md deleted file mode 100644 index cf3f304d..00000000 --- a/.llm/skills/clippy-configuration.md +++ /dev/null @@ -1,581 +0,0 @@ -# Clippy Configuration Guide — Lint-Driven Code Quality - -> **This document provides a comprehensive guide to configuring Clippy for maximum code quality.** -> Use these lints to catch bugs, enforce best practices, and improve performance automatically. - -## TL;DR — Recommended Baseline Configuration - -Add to `Cargo.toml`: - -```toml -[lints.clippy] -# Correctness (bugs) -correctness = { level = "deny", priority = -1 } - -# Suspicious patterns (likely bugs) -suspicious = { level = "warn", priority = -1 } - -# Zero-panic code (critical for production) -unwrap_used = "deny" -expect_used = "deny" -panic = "deny" -indexing_slicing = "deny" - -# Performance -perf = { level = "warn", priority = -1 } - -# Code quality -pedantic = { level = "warn", priority = -1 } -missing_errors_doc = "allow" # Relax if too noisy -``` - -Run: `cargo clippy --all-targets` - ---- - -## Understanding Clippy Lint Groups - -### The Nine Lint Groups - -| Group | Default | Description | When to Enable | -|-------|---------|-------------|----------------| -| **correctness** | deny | Actual bugs, always wrong | Always | -| **suspicious** | warn | Likely bugs, rarely correct | Always | -| **style** | warn | Idiomatic style violations | Always | -| **complexity** | warn | Overly complex code | Always | -| **perf** | warn | Performance issues | Always | -| **pedantic** | allow | Very strict, opinionated | Libraries | -| **restriction** | allow | Very restrictive, use selectively | Security-critical | -| **nursery** | allow | Unstable, may have false positives | Experimental | -| **cargo** | allow | Cargo.toml issues | CI/CD | - -### Group Priority - -Use priority to control lint group ordering: - -```toml -[lints.clippy] -# Enable pedantic first (priority -1 = lower = applied first) -pedantic = { level = "warn", priority = -1 } -# Then allow specific lints (higher priority = applied later) -missing_errors_doc = "allow" -``` - ---- - -## Critical Correctness Lints (Always Enable) - -These catch actual bugs — never disable them: - -```toml -[lints.clippy] -# These are deny by default, but be explicit -correctness = { level = "deny", priority = -1 } - -# Specific high-impact correctness lints -absurd_extreme_comparisons = "deny" # x > MAX or x < MIN is always false -approx_constant = "deny" # Using 3.14 instead of std::f64::consts::PI -bad_bit_mask = "deny" # if (x & 0b111) == 0b1000 { } // never true -deprecated = "deny" # Using deprecated items -derive_ord_xor_partial_ord = "deny" # Ord without PartialOrd is wrong -drop_copy = "deny" # Dropping Copy types does nothing -eq_op = "deny" # x == x is always true -erasing_op = "deny" # x * 0 = 0, x & 0 = 0 -infinite_loop = "deny" # Loop that never exits -invalid_regex = "deny" # Regex that won't compile -iter_skip_next = "deny" # .skip(1).next() instead of .nth(1) -modulo_one = "deny" # x % 1 is always 0 -never_loop = "deny" # Loop that always breaks on first iteration -nonsensical_open_options = "deny" # File::create().read() is silly -not_unsafe_ptr_arg_deref = "deny" # Deref raw ptr in safe fn without unsafe block -out_of_bounds_indexing = "deny" # array[10] when array.len() < 10 -suspicious_map = "deny" # .map().unwrap() instead of .and_then() -uninit_assumed_init = "deny" # MaybeUninit::assume_init without initialization -unit_cmp = "deny" # Comparing () values -``` - ---- - -## Zero-Panic Production Code Lints - -For production code that must never panic: - -```toml -[lints.clippy] -# Direct panic sources -panic = "deny" -panic_in_result_fn = "deny" -todo = "deny" -unimplemented = "deny" -unreachable = "deny" - -# Implicit panics -unwrap_used = "deny" -expect_used = "deny" -indexing_slicing = "deny" - -# Arithmetic panics -integer_division = "deny" -modulo_arithmetic = "deny" - -# Infallible conversions that could panic -fallible_impl_from = "deny" -``` - -**Handling false positives:** - -```rust -// When you must use unwrap (rare cases with proof) -#[allow(clippy::unwrap_used, reason = "Vec always has at least one element after push")] -let first = vec.first().unwrap(); - -// Better: use expect with justification (if you allow expect_used) -let first = vec.first().expect("Vec non-empty after push"); - -// Best: restructure to avoid the need -let first = vec.first().ok_or(Error::EmptyVec)?; -``` - ---- - -## Performance Lints - -```toml -[lints.clippy] -perf = { level = "warn", priority = -1 } - -# Critical performance lints -box_collection = "warn" # Box> instead of Vec -box_default = "warn" # Box::new(Default::default()) -boxed_local = "warn" # Using Box for local variable -expect_fun_call = "warn" # .expect(format!(...)) allocates even on Ok -inefficient_to_string = "warn" # ToString impl that could use Display -iter_on_empty_collections = "warn" # Iterating over known-empty collection -iter_on_single_items = "warn" # Iterating over single item -large_const_arrays = "warn" # Large arrays in const context -large_enum_variant = "warn" # One variant much larger than others -large_types_passed_by_value = "warn" # Passing large types by value -mutex_atomic = "warn" # Mutex instead of AtomicBool -naive_bytecount = "warn" # Manual byte counting instead of bytecount -needless_collect = "warn" # collect() when direct iteration works -or_fun_call = "warn" # .or(expensive()) instead of .or_else() -single_char_pattern = "warn" # "x" instead of 'x' in string methods -slow_vector_initialization = "warn" # vec![0; n] vs vec.resize() -unnecessary_to_owned = "warn" # to_owned() when borrow suffices -useless_vec = "warn" # Vec when array works -vec_init_then_push = "warn" # Vec::new() then push vs vec![] -``` - -### Examples - -```rust -// ❌ box_collection: Box> is double-indirection -fn get_data() -> Box> { ... } -// ✅ Just return Vec (already heap-allocated) -fn get_data() -> Vec { ... } - -// ❌ large_enum_variant: Forces all variants to large size -enum Message { - Ping, - Data([u8; 1024]), // 1024 bytes for all Messages! -} -// ✅ Box the large variant -enum Message { - Ping, - Data(Box<[u8; 1024]>), -} - -// ❌ or_fun_call: compute_default() called even if Some -x.or(compute_default()) -// ✅ Use or_else for lazy evaluation -x.or_else(|| compute_default()) -``` - ---- - -## API Design Lints - -For library authors: - -```toml -[lints.clippy] -# Public API quality -missing_panics_doc = "warn" # Document when functions can panic -missing_safety_doc = "warn" # Document unsafe preconditions -missing_errors_doc = "warn" # Document when Result can be Err -must_use_candidate = "warn" # Functions that should be #[must_use] -return_self_not_must_use = "warn" # Builder methods should be must_use -undocumented_unsafe_blocks = "warn" # Unsafe blocks need // SAFETY: - -# Type design -enum_variant_names = "warn" # Variant names shouldn't repeat enum name -struct_excessive_bools = "warn" # Multiple bools, use enum instead -``` - ---- - -## Code Quality Lints (Pedantic) - -```toml -[lints.clippy] -pedantic = { level = "warn", priority = -1 } - -# Often useful to relax these -missing_errors_doc = "allow" # Can be noisy -module_name_repetitions = "allow" # project::project_error is fine -must_use_candidate = "allow" # Too noisy for internal code -similar_names = "allow" # item vs items is clear enough -too_many_lines = "allow" # Functions can be long if clear -``` - -### Most Valuable Pedantic Lints - -```toml -# Keep these even if you relax pedantic overall -bool_to_int_with_if = "warn" # if b { 1 } else { 0 } → u32::from(b) -case_sensitive_file_extension_comparisons = "warn" -cloned_instead_of_copied = "warn" # .cloned() on Copy types -default_trait_access = "warn" # Default::default() → T::default() -explicit_deref_methods = "warn" # (*x).foo() → x.foo() -explicit_iter_loop = "warn" # for x in v.iter() → for x in &v -filter_map_next = "warn" # .filter_map().next() → .find_map() -flat_map_option = "warn" # .flat_map(|x| x) on Option -from_iter_instead_of_collect = "warn" # FromIterator::from_iter → .collect() -if_not_else = "warn" # if !cond { a } else { b } → if cond { b } else { a } -implicit_clone = "warn" # .clone() on reference when deref would copy -inefficient_to_string = "warn" # Better ToString implementation available -items_after_statements = "warn" # Items should be at module top -iter_without_into_iter = "warn" # iter() without IntoIterator impl -manual_assert = "warn" # if !cond { panic!() } → assert!(cond) -manual_instant_elapsed = "warn" # Instant::now() - start → start.elapsed() -manual_is_ascii_check = "warn" # Manual ASCII range checks -manual_let_else = "warn" # let x = match expr { Some(x) => x, None => return } -manual_string_new = "warn" # String::from("") → String::new() -map_unwrap_or = "warn" # .map().unwrap_or() → .map_or() -match_bool = "warn" # match on bool → if/else -match_same_arms = "warn" # Duplicate match arms -match_wildcard_for_single_variants = "warn" -needless_continue = "warn" # Unnecessary continue at end of loop -needless_for_each = "warn" # .for_each() when for loop is clearer -no_effect_underscore_binding = "warn" # let _ = expr; when expr has no side effect -range_plus_one = "warn" # 0..n+1 → 0..=n -redundant_closure_for_method_calls = "warn" # |x| x.foo() → T::foo -redundant_else = "warn" # else after diverging if -semicolon_if_nothing_returned = "warn" -unnecessary_wraps = "warn" # Returning Result when function never errors -unnested_or_patterns = "warn" # Some(1) | Some(2) → Some(1 | 2) -unused_self = "warn" # Methods that don't use self -used_underscore_binding = "warn" # Using _x after declaring it unused -verbose_bit_mask = "warn" # x & (y - 1) when y is power of 2 -``` - ---- - -## Restriction Lints (Use Selectively) - -These are very strict — enable only what makes sense: - -```toml -[lints.clippy] -# Safe defaults for most projects -absolute_paths = "allow" # Too strict -as_conversions = "warn" # Prefer TryFrom -clone_on_ref_ptr = "warn" # Rc::clone(&x) is clearer -dbg_macro = "warn" # dbg!() shouldn't be in production -default_numeric_fallback = "warn" # 42 without type annotation -deref_by_slicing = "warn" # &vec[..] → &*vec -empty_drop = "warn" # Empty Drop impl -empty_structs_with_brackets = "warn" # struct Empty {} → struct Empty; -exit = "warn" # std::process::exit is usually wrong -filetype_is_file = "warn" # .is_file() misses symlinks -float_arithmetic = "allow" # Too strict unless embedded -get_unwrap = "warn" # .get().unwrap() → indexing or ? -if_then_some_else_none = "warn" # if c { Some(x) } else { None } → c.then(|| x) -impl_trait_in_params = "warn" # fn f(x: impl Trait) → fn f(x: T) -indexing_slicing = "warn" # Already in zero-panic section -inline_asm_x86_att_syntax = "allow" # Platform specific -inline_asm_x86_intel_syntax = "allow" # Platform specific -integer_division_remainder_used = "allow" # Too strict -let_underscore_must_use = "warn" # let _ = must_use_fn() -let_underscore_untyped = "warn" # let _ = expr; might hide type error -lossy_float_literal = "warn" # Float literals that lose precision -map_err_ignore = "warn" # .map_err(|_| ...) loses error info -mem_forget = "warn" # mem::forget usually wrong -min_ident_chars = "allow" # Too strict (i, x, n are fine) -missing_asserts_for_indexing = "warn" # Index without prior bounds check -mixed_read_write_in_expression = "warn" -multiple_unsafe_ops_per_block = "warn" -mutex_integer = "warn" # Mutex → AtomicI32 -needless_raw_strings = "warn" # r"no special chars" → "no special chars" -panic_in_result_fn = "warn" # Don't panic in fn returning Result -partial_pub_fields = "allow" # Sometimes legitimate -print_stderr = "warn" # Use logging instead -print_stdout = "warn" # Use logging instead -pub_without_shorthand = "warn" # pub(in self) → private -rc_buffer = "warn" # Rc → Rc -rc_mutex = "warn" # Rc> is usually wrong -redundant_type_annotations = "warn" # let x: i32 = 5i32; -ref_patterns = "allow" # &x in patterns is sometimes clearer -rest_pat_in_fully_bound_structs = "warn" -same_name_method = "warn" # Method shadowing trait method -self_named_module_files = "allow" # mod.rs vs folder/name.rs -semicolon_inside_block = "allow" # Style preference -semicolon_outside_block = "allow" # Style preference -shadow_reuse = "allow" # Shadowing is fine in Rust -shadow_same = "allow" # let x = x; is fine -shadow_unrelated = "warn" # Shadowing with unrelated type -single_call_fn = "allow" # Single-use functions are fine -std_instead_of_alloc = "allow" # Only for no_std -std_instead_of_core = "allow" # Only for no_std -str_to_string = "warn" # "".to_string() → String::new() -string_add = "warn" # s + "x" → s.push_str("x") -string_lit_as_bytes = "warn" # "abc".as_bytes() → b"abc" -string_slice = "warn" # UTF-8 slicing can panic -string_to_string = "warn" # String.to_string() → .clone() -suspicious_xor_used_as_pow = "warn" # 2^10 (XOR) likely meant 2**10 -try_err = "warn" # Err(x)? → return Err(x) -undocumented_unsafe_blocks = "deny" # Unsafe needs // SAFETY: -unneeded_field_pattern = "warn" # Foo { x: _, .. } → Foo { .. } -unnecessary_safety_comment = "warn" # Safety comment on safe code -unnecessary_safety_doc = "warn" # Safety doc on safe fn -unseparated_literal_suffix = "warn" # 1usize → 1_usize -unwrap_in_result = "warn" # .unwrap() in fn returning Result -unwrap_used = "deny" # Already in zero-panic -use_debug = "warn" # {:?} in user-facing output -verbose_file_reads = "warn" # fs::read_to_string is simpler -wildcard_enum_match_arm = "warn" # _ => ... misses new variants -``` - ---- - -## Cargo.toml Configuration Reference - -### Full Production Configuration - -```toml -[lints.rust] -unsafe_code = "forbid" - -[lints.clippy] -# Lint groups -correctness = { level = "deny", priority = -1 } -suspicious = { level = "warn", priority = -1 } -style = { level = "warn", priority = -1 } -complexity = { level = "warn", priority = -1 } -perf = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } - -# Zero-panic essentials -panic = "deny" -unwrap_used = "deny" -expect_used = "deny" -indexing_slicing = "deny" -todo = "deny" -unimplemented = "deny" - -# Allow some pedantic lints -missing_errors_doc = "allow" -module_name_repetitions = "allow" -must_use_candidate = "allow" - -# Additional strict lints -undocumented_unsafe_blocks = "deny" -``` - -### clippy.toml Configuration - -```toml -# Project root: clippy.toml - -# Type complexity threshold (default: 250) -type-complexity-threshold = 300 - -# Max lines per function (default: 100) -too-many-lines-threshold = 150 - -# Max arguments per function (default: 7) -too-many-arguments-threshold = 8 - -# Large type threshold for pass-by-value (default: 256) -trivial-copy-size-limit = 16 -pass-by-value-size-limit = 256 - -# Enum variant size threshold (default: 200) -enum-variant-size-threshold = 400 - -# Struct field count threshold -struct-field-count-threshold = 10 - -# Cognitive complexity threshold (default: 25) -cognitive-complexity-threshold = 30 - -# Disallowed types (enforce consistent hashing) -disallowed-types = [ - { path = "std::collections::HashMap", reason = "Use FxHashMap for non-crypto hashing" }, - { path = "std::collections::HashSet", reason = "Use FxHashSet for non-crypto hashing" }, -] - -# Disallowed methods -disallowed-methods = [ - { path = "std::env::var", reason = "Use config module instead" }, -] - -# Allowed wildcard imports (usually none) -allowed-wildcard-imports = [] - -# Doc comment code block ignore (for incomplete examples) -doc-valid-idents = ["OpenGL", "WebGL", "SIMD"] -``` - ---- - -## Running Clippy - -### Basic Commands - -```bash -# Run on all targets (lib, tests, examples, benches) -cargo clippy --all-targets - -# Include feature-gated code -cargo clippy --all-targets --all-features - -# Fix automatically where possible -cargo clippy --fix --allow-dirty - -# Treat warnings as errors (CI) -cargo clippy --all-targets -- -D warnings - -# Check workspace -cargo clippy --workspace --all-targets -``` - -### CI Configuration - -```yaml -# .github/workflows/lint.yml -name: Lint - -on: [push, pull_request] - -jobs: - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - run: cargo clippy --all-targets --all-features -- -D warnings -``` - ---- - -## Handling Lint Violations - -### Project-Wide Allowances - -In `lib.rs` or `main.rs`: - -```rust -// Allow throughout the crate -#![allow(clippy::module_name_repetitions)] - -// Deny throughout the crate (stronger than Cargo.toml for sub-crates) -#![deny(clippy::unwrap_used)] -``` - -### Module-Level Allowances - -```rust -// Allow for entire module -#![allow(clippy::too_many_arguments)] - -mod internal_module { - // ... -} -``` - -### Function/Item-Level Allowances - -```rust -// With reason (Rust 1.81+) -#[allow(clippy::unwrap_used, reason = "Guaranteed Some by invariant")] -fn guaranteed_some() -> i32 { - STATIC_OPTION.unwrap() -} - -// Without reason (older Rust) -#[allow(clippy::unwrap_used)] // OK: Guaranteed by invariant -fn guaranteed_some() -> i32 { - STATIC_OPTION.unwrap() -} -``` - -### Inline Allowances - -```rust -fn process() { - #[allow(clippy::indexing_slicing)] // Length checked on previous line - let first = &slice[0]; -} -``` - ---- - -## Common Anti-Patterns in Configuration - -### ❌ Don't: Blanket Allow to Silence Warnings - -```toml -# BAD: Hides real issues -[lints.clippy] -pedantic = "allow" -perf = "allow" -``` - -### ❌ Don't: Allow Without Reason - -```rust -// BAD: Why is this allowed? -#[allow(clippy::unwrap_used)] -fn mystery() { ... } -``` - -### ❌ Don't: Global Deny on Noisy Lints - -```toml -# BAD: Will frustrate developers -[lints.clippy] -single_char_lifetime_names = "deny" # 'a is idiomatic! -``` - -### ✅ Do: Be Selective and Document - -```rust -// GOOD: Explains the allowance -#[allow( - clippy::unwrap_used, - reason = "JSON schema guarantees this field exists" -)] -fn extract_required_field(json: &Value) -> &str { - json["required_field"].as_str().unwrap() -} -``` - ---- - -## Quick Reference — Lint Categories - -| Category | Lints | Use Case | -|----------|-------|----------| -| **Zero-Panic** | `unwrap_used`, `expect_used`, `panic`, `indexing_slicing` | Production code | -| **Performance** | `perf` group, `large_enum_variant`, `needless_collect` | Hot paths | -| **Safety** | `undocumented_unsafe_blocks`, `missing_safety_doc` | Unsafe code | -| **API Quality** | `missing_panics_doc`, `must_use_candidate` | Public APIs | -| **Style** | `pedantic` group minus noisy ones | Code review | -| **Security** | `restriction` subset | Security-critical | - ---- - -*Configure Clippy to match your project's requirements. Start strict, relax as needed with documented reasons.* diff --git a/.llm/skills/clippy-lints-guide.md b/.llm/skills/clippy-lints-guide.md deleted file mode 100644 index 1ab665cc..00000000 --- a/.llm/skills/clippy-lints-guide.md +++ /dev/null @@ -1,748 +0,0 @@ -# Clippy Lints and Rust Code Quality Guide - -> A comprehensive guide to Clippy lints, configuration, and best practices for production-quality Rust code. - -## Quick Reference - -| Lint Group | Default Level | Count | Purpose | -|------------|--------------|-------|---------| -| `correctness` | **deny** | ~60 | Code that is outright wrong | -| `suspicious` | warn | ~90 | Code that looks wrong but might be intentional | -| `style` | warn | ~150 | Idiomatic Rust code conventions | -| `complexity` | warn | ~100 | Simplifiable code patterns | -| `perf` | warn | ~50 | Performance improvements | -| `pedantic` | allow | ~100 | Stricter, opinionated lints | -| `restriction` | allow | ~100 | Highly restrictive lints | -| `nursery` | allow | ~50 | Experimental/incomplete lints | -| `cargo` | allow | ~5 | Cargo.toml quality checks | - ---- - -## Part 1: Lint Groups Explained - -### Correctness (deny-by-default) - -**Never suppress these without extreme justification.** These lints catch actual bugs. - -```rust -// Example: approx_constant - catches incorrect mathematical constants -// ❌ Triggers correctness lint -let pi = 3.14159; // clippy::approx_constant -// ✅ Use the constant -let pi = std::f64::consts::PI; - -// Example: infinite_iter - catches infinite iterators consumed completely -// ❌ This will hang forever -let sum: i32 = (0..).sum(); // clippy::infinite_iter -// ✅ Limit the iterator -let sum: i32 = (0..100).sum(); -``` - -### Suspicious (warn-by-default) - -Code that looks wrong but might be intentional. Review carefully. - -```rust -// Example: suspicious_arithmetic_impl - catches odd operator implementations -impl Add for MyType { - type Output = Self; - fn add(self, other: Self) -> Self { - // ❌ Suspicious: subtraction in Add impl - Self(self.0 - other.0) // clippy::suspicious_arithmetic_impl - } -} - -// Example: await_holding_lock - catches deadlock potential -async fn bad() { - let guard = mutex.lock().unwrap(); - // ❌ Holding lock across await point - some_async_op().await; // clippy::await_holding_lock -} -``` - -### Complexity (warn-by-default) - -Code that can be simplified without changing behavior. - -```rust -// Example: needless_bool - unnecessary boolean operations -// ❌ Overly complex -if condition { true } else { false } // clippy::needless_bool -// ✅ Simplified -condition - -// Example: redundant_closure - unnecessary closure wrapper -// ❌ Unnecessary closure -items.iter().map(|x| f(x)) // clippy::redundant_closure -// ✅ Direct function reference -items.iter().map(f) -``` - -### Performance (warn-by-default) - -Code that could be more efficient. - -```rust -// Example: box_collection - boxing already-heap-allocated types -// ❌ Double indirection -struct Bad { - data: Box>, // clippy::box_collection -} -// ✅ Single allocation -struct Good { - data: Vec, -} - -// Example: manual_memcpy - using loop instead of copy_from_slice -// ❌ Manual loop -for i in 0..len { - dst[i] = src[i]; // clippy::manual_memcpy -} -// ✅ Use built-in -dst[..len].copy_from_slice(&src[..len]); -``` - -### Style (warn-by-default) - -Idiomatic Rust conventions. Subjective but generally good advice. - -```rust -// Example: len_zero - using .len() == 0 instead of .is_empty() -// ❌ Less idiomatic -if vec.len() == 0 { } // clippy::len_zero -// ✅ More idiomatic -if vec.is_empty() { } - -// Example: redundant_field_names - verbose struct initialization -// ❌ Redundant -let s = MyStruct { name: name, value: value }; // clippy::redundant_field_names -// ✅ Shorthand -let s = MyStruct { name, value }; -``` - -### Pedantic (allow-by-default) - -Stricter lints that may have false positives. Cherry-pick useful ones. - -```rust -// Example: cast_lossless - using `as` instead of `into()` for safe casts -// ❌ Could be clearer about losslessness -let big: u64 = small_u32 as u64; // clippy::cast_lossless -// ✅ Explicit safe conversion -let big: u64 = small_u32.into(); - -// Example: must_use_candidate - functions that should have #[must_use] -// ❌ Missing must_use -pub fn calculate(&self) -> Result { } // clippy::must_use_candidate -// ✅ With must_use -#[must_use] -pub fn calculate(&self) -> Result { } -``` - -### Restriction (allow-by-default) - -Highly restrictive lints. **Never enable the entire group.** Cherry-pick only. - -```rust -// Example: unwrap_used - any use of .unwrap() -// ❌ Can panic -let value = option.unwrap(); // clippy::unwrap_used -// ✅ Handle the error -let value = option.ok_or(Error::Missing)?; - -// Example: panic - any use of panic!() -// ❌ Crashes the program -panic!("something went wrong"); // clippy::panic -// ✅ Return error -return Err(Error::SomethingWrong); -``` - -### Nursery (allow-by-default) - -Experimental lints that may have bugs. Cherry-pick stable ones. - -```rust -// Example: significant_drop_in_scrutinee - drops in match scrutinees -// ❌ Drop timing may be surprising -match mutex.lock().unwrap().data.clone() { // clippy::significant_drop_in_scrutinee - Some(x) => use(x), - None => {}, -} -// ✅ Explicit drop timing -let data = mutex.lock().unwrap().data.clone(); -drop(mutex); -match data { ... } -``` - ---- - -## Part 2: Top 40 Most Important Lints - -### Critical Correctness Lints (Always Enable) - -| Lint | Category | Why It Matters | -|------|----------|----------------| -| `invalid_regex` | correctness | Invalid regex causes runtime panic | -| `approx_constant` | correctness | Using imprecise mathematical constants | -| `infinite_iter` | correctness | Infinite iterator consumed completely | -| `iter_next_loop` | correctness | Calling `.next()` in a for loop | -| `out_of_bounds_indexing` | correctness | Compile-time detected OOB | -| `panicking_unwrap` | correctness | Unwrap on known-None value | -| `uninit_assumed_init` | correctness | Using uninitialized memory | -| `derive_ord_xor_partial_ord` | correctness | Inconsistent ordering impls | -| `if_let_mutex` | correctness | Deadlock from mutex in if let | - -### Essential Safety Lints (For Production Code) - -| Lint | Category | Why It Matters | -|------|----------|----------------| -| `unwrap_used` | restriction | Prevents panic from None/Err | -| `expect_used` | restriction | Prevents panic from None/Err | -| `panic` | restriction | Prevents explicit panics | -| `todo` | restriction | Catches incomplete code | -| `unimplemented` | restriction | Catches stub implementations | -| `indexing_slicing` | restriction | Prevents OOB panic | -| `arithmetic_side_effects` | restriction | Prevents overflow panic | - -### Performance Lints (Enable for Hot Paths) - -| Lint | Category | Why It Matters | -|------|----------|----------------| -| `box_collection` | perf | Unnecessary double indirection | -| `large_enum_variant` | perf | Enum size bloated by one variant | -| `manual_memcpy` | perf | Loop slower than memcpy | -| `useless_vec` | perf | Vec where array/slice suffices | -| `unnecessary_to_owned` | perf | Cloning when borrowing works | -| `redundant_clone` | nursery | Clone of already-owned value | -| `manual_str_repeat` | perf | Manual loop vs `.repeat()` | -| `map_entry` | perf | Inefficient HashMap access | -| `slow_vector_initialization` | perf | Pre-size vectors when possible | -| `cmp_owned` | perf | Unnecessary allocation for comparison | - -### API Design Lints (For Public APIs) - -| Lint | Category | Why It Matters | -|------|----------|----------------| -| `must_use_candidate` | pedantic | Functions should indicate useful return | -| `missing_errors_doc` | pedantic | Document error conditions | -| `missing_panics_doc` | pedantic | Document panic conditions | -| `missing_safety_doc` | style | Unsafe functions need documentation | -| `new_without_default` | style | Types with `new()` should impl Default | -| `len_without_is_empty` | style | Types with `.len()` should have `.is_empty()` | -| `result_unit_err` | style | `Result` loses error info | - -### Code Quality Lints (Improve Maintainability) - -| Lint | Category | Why It Matters | -|------|----------|----------------| -| `cognitive_complexity` | restriction | Functions too complex to understand | -| `too_many_arguments` | complexity | Functions with too many params | -| `needless_pass_by_value` | pedantic | Take references when ownership not needed | -| `implicit_clone` | pedantic | Make clones explicit | -| `clone_on_ref_ptr` | restriction | Clone Arc/Rc explicitly | -| `wildcard_imports` | pedantic | `use foo::*` hides dependencies | -| `enum_glob_use` | pedantic | `use Enum::*` obscures variants | -| `fallible_impl_from` | nursery | From should be infallible; use TryFrom | - ---- - -## Part 3: Cargo.toml Configuration - -### Basic Setup - -```toml -[lints.rust] -# Enable warnings for unsafe code -unsafe_code = "warn" -# Or forbid entirely -# unsafe_code = "forbid" - -[lints.clippy] -# Start with pedantic as base, then allow noisy ones -pedantic = { level = "warn", priority = -1 } -# Allow commonly noisy pedantic lints -module_name_repetitions = "allow" -similar_names = "allow" -too_many_lines = "allow" -``` - -### Production-Grade Configuration - -```toml -[lints.rust] -# Require docs on public items -missing_docs = "warn" -# Warn about unused items -unused = "warn" -# Handle custom cfg flags -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(test)', 'cfg(feature, values(...))'] } - -[lints.clippy] -# === Base: Enable pedantic === -pedantic = { level = "warn", priority = -1 } - -# === Defensive programming (deny these) === -todo = "deny" -unimplemented = "deny" -fallible_impl_from = "deny" - -# === No-panic enforcement (warn for gradual adoption, deny when clean) === -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" - -# === Debug/print prevention === -dbg_macro = "warn" -print_stdout = "warn" -print_stderr = "warn" - -# === Allow noisy pedantic lints === -module_name_repetitions = "allow" -similar_names = "allow" -too_many_lines = "allow" -missing_errors_doc = "allow" -missing_panics_doc = "allow" -cast_precision_loss = "allow" -cast_sign_loss = "allow" -cast_possible_truncation = "allow" -``` - -### Strict Library Configuration - -```toml -[lints.clippy] -# Enable all major groups -pedantic = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } -cargo = { level = "warn", priority = -1 } - -# Deny dangerous patterns -unwrap_used = "deny" -expect_used = "deny" -panic = "deny" -todo = "deny" -unimplemented = "deny" -unreachable = "deny" - -# Allow specific noisy lints -multiple_crate_versions = "allow" -significant_drop_tightening = "allow" -redundant_pub_crate = "allow" -future_not_send = "allow" -cognitive_complexity = "allow" -missing_const_for_fn = "allow" -``` - -### Application Configuration (More Relaxed) - -```toml -[lints.clippy] -# Enable pedantic but allow more flexibility -pedantic = { level = "warn", priority = -1 } - -# Applications can panic in unrecoverable situations -unwrap_used = "warn" # Not deny -panic = "allow" # Applications may legitimately panic - -# Still prevent incomplete code -todo = "deny" -unimplemented = "deny" - -# Allow CLI output -print_stdout = "allow" -print_stderr = "allow" -``` - ---- - -## Part 4: clippy.toml Configuration - -Create `clippy.toml` in project root for lint-specific settings: - -```toml -# Minimum Supported Rust Version - affects which lints apply -msrv = "1.80" - -# Cognitive complexity threshold (default: 25) -cognitive-complexity-threshold = 30 - -# Type complexity threshold (default: 250) -type-complexity-threshold = 350 - -# Trivially copy pass by ref threshold (bytes) -trivial-copy-size-limit = 16 - -# Pass by value threshold (bytes) -pass-by-value-size-limit = 256 - -# Allow short identifier names -allowed-idents-below-min-chars = ["x", "y", "z", "i", "j", "k", "n", "id"] - -# Require docs on crate items -missing-docs-in-crate-items = true - -# Disallow specific macros (prevents debug output in production) -disallowed-macros = [ - { path = "std::dbg", reason = "Use tracing macros instead" }, - { path = "std::print", reason = "Use tracing macros instead" }, - { path = "std::println", reason = "Use tracing macros instead" }, -] - -# Allow unwrap/expect in tests -allow-unwrap-in-tests = true -allow-expect-in-tests = true -allow-indexing-slicing-in-tests = true -``` - ---- - -## Part 5: Handling False Positives - -### Per-Item Allowances - -```rust -// Allow on a single item with justification -#[allow(clippy::too_many_arguments)] -fn complex_but_necessary( - a: u32, b: u32, c: u32, d: u32, e: u32, f: u32, g: u32 -) -> Result<(), Error> { - // Justification: This matches the wire protocol format - ... -} -``` - -### Scoped Allowances - -```rust -fn process() { - // Allow only for this specific line - #[allow(clippy::unwrap_used)] - let value = guaranteed_some.unwrap(); // SAFETY: validated above -} -``` - -### Module-Level Allowances - -```rust -// In a module that intentionally uses unsafe patterns -#![allow(clippy::indexing_slicing)] -// Justification: This module handles bounds checking manually for performance -``` - -### Test-Specific Configuration - -```rust -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] // OK in tests - #![allow(clippy::panic)] // OK in tests - - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} -``` - ---- - -## Part 6: Lints by Use Case - -### For Zero-Panic Code - -```toml -[lints.clippy] -unwrap_used = "deny" -expect_used = "deny" -panic = "deny" -todo = "deny" -unimplemented = "deny" -unreachable = "deny" -indexing_slicing = "deny" # Use .get() instead -arithmetic_side_effects = "deny" # Use checked_* methods -``` - -### For Performance-Critical Code - -```toml -[lints.clippy] -# Performance warnings -box_collection = "warn" -large_enum_variant = "warn" -redundant_clone = "warn" -unnecessary_to_owned = "warn" -useless_vec = "warn" -manual_memcpy = "warn" -slow_vector_initialization = "warn" -# More aggressive -large_stack_arrays = "warn" -large_futures = "warn" -``` - -### For Public Library APIs - -```toml -[lints.clippy] -# API quality -must_use_candidate = "warn" -missing_errors_doc = "warn" -missing_panics_doc = "warn" -missing_safety_doc = "warn" -# Prevent breaking changes -exhaustive_enums = "warn" -exhaustive_structs = "warn" -``` - -### For Async Code - -```toml -[lints.clippy] -# Async-specific -await_holding_lock = "deny" -await_holding_refcell_ref = "deny" -future_not_send = "warn" # May need to allow -large_futures = "warn" -``` - ---- - -## Part 7: Before/After Examples - -### Unwrap to Error Handling - -```rust -// ❌ Before: Can panic -fn get_config(path: &str) -> Config { - let content = std::fs::read_to_string(path).unwrap(); - toml::from_str(&content).unwrap() -} - -// ✅ After: Returns Result -fn get_config(path: &str) -> Result { - let content = std::fs::read_to_string(path) - .map_err(|e| ConfigError::Read { path: path.into(), source: e })?; - toml::from_str(&content) - .map_err(|e| ConfigError::Parse { path: path.into(), source: e }) -} -``` - -### Indexing to Safe Access - -```rust -// ❌ Before: Can panic on out of bounds -fn get_element(data: &[u8], index: usize) -> u8 { - data[index] // clippy::indexing_slicing -} - -// ✅ After: Returns Option -fn get_element(data: &[u8], index: usize) -> Option { - data.get(index).copied() -} - -// Or with Result -fn get_element(data: &[u8], index: usize) -> Result { - data.get(index).copied().ok_or(Error::IndexOutOfBounds { index, len: data.len() }) -} -``` - -### Clone to Borrow - -```rust -// ❌ Before: Unnecessary allocation -fn process(data: String) { - use_data(&data); // Only borrowing -} -// Called with: process(my_string.clone()) - -// ✅ After: Accept reference -fn process(data: &str) { - use_data(data); -} -// Called with: process(&my_string) -``` - -### Manual Loop to Iterator - -```rust -// ❌ Before: Manual, error-prone -let mut results = Vec::new(); -for item in items { - if item.is_valid() { - results.push(item.process()); - } -} - -// ✅ After: Functional, clear intent -let results: Vec<_> = items - .into_iter() - .filter(|item| item.is_valid()) - .map(|item| item.process()) - .collect(); -``` - -### Box Collection to Direct Collection - -```rust -// ❌ Before: Double indirection -struct Cache { - data: Box>, // clippy::box_collection -} - -// ✅ After: Single allocation -struct Cache { - data: HashMap, -} -``` - -### Large Enum Variant - -```rust -// ❌ Before: All variants sized to largest -enum Message { - Small(u8), - Large([u8; 1024]), // clippy::large_enum_variant -} - -// ✅ After: Box the large variant -enum Message { - Small(u8), - Large(Box<[u8; 1024]>), -} -``` - ---- - -## Part 8: Running Clippy - -### Basic Usage - -```bash -# Run with default settings -cargo clippy - -# Run on all targets (tests, benches, examples) -cargo clippy --all-targets - -# Run with all features enabled -cargo clippy --all-features - -# Fix automatically where possible -cargo clippy --fix - -# Deny all warnings (for CI) -cargo clippy -- -D warnings -``` - -### With Feature Flags - -```bash -# Check specific features -cargo clippy --features "feature1,feature2" - -# Check without default features -cargo clippy --no-default-features - -# Check all feature combinations -cargo clippy --all-features -``` - -### CI Configuration - -```yaml -# GitHub Actions example -- name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings -``` - ---- - -## Part 9: Common Anti-Patterns to Avoid - -### Don't Enable All Restriction Lints - -```toml -# ❌ NEVER do this -[lints.clippy] -restriction = "warn" # Will cause hundreds of false positives - -# ✅ Cherry-pick specific restriction lints -[lints.clippy] -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -``` - -### Don't Suppress Without Reason - -```rust -// ❌ Bad: No explanation -#[allow(clippy::unwrap_used)] -fn foo() { ... } - -// ✅ Good: Documented justification -#[allow(clippy::unwrap_used)] -// SAFETY: `value` is validated to be Some by `validate()` called above -fn foo() { ... } -``` - -### Don't Mix Allow and Deny Inconsistently - -```toml -# ❌ Confusing: Group denied but member allowed -[lints.clippy] -pedantic = "deny" -similar_names = "allow" # Still denied due to group - -# ✅ Clear: Use priority -[lints.clippy] -pedantic = { level = "deny", priority = -1 } # Lower priority -similar_names = "allow" # Higher priority, overrides -``` - ---- - -## Part 10: Quick Troubleshooting - -| Issue | Solution | -|-------|----------| -| "unknown lint" | Update Rust/Clippy version | -| Lint triggers in macro | Add `#[allow]` to macro invocation | -| Lint triggers in generated code | Use `#[cfg_attr(not(clippy), ...)]` | -| Too many warnings | Enable lints gradually | -| False positive | File issue, use `#[allow]` with comment | -| Lint not triggering | Check if lint is in enabled group | - ---- - -## Summary: Recommended Baseline - -For most production Rust projects: - -```toml -[lints.clippy] -# Enable pedantic as baseline -pedantic = { level = "warn", priority = -1 } - -# Zero-panic enforcement -unwrap_used = "warn" -expect_used = "warn" -panic = "warn" -todo = "deny" -unimplemented = "deny" - -# Common noisy lints to allow -module_name_repetitions = "allow" -similar_names = "allow" -too_many_lines = "allow" -missing_errors_doc = "allow" -``` - -Combined with `clippy.toml`: - -```toml -msrv = "1.80" -allow-unwrap-in-tests = true -allow-expect-in-tests = true -``` - -This provides excellent coverage without being overwhelming. diff --git a/.llm/skills/clippy.md b/.llm/skills/clippy.md new file mode 100644 index 00000000..d5e02348 --- /dev/null +++ b/.llm/skills/clippy.md @@ -0,0 +1,249 @@ + + + +# Clippy Configuration + +--- + +## Lint Groups + +| Group | Default | Purpose | Enable | +|-------|---------|---------|--------| +| `correctness` | deny | Actual bugs | Always | +| `suspicious` | warn | Likely bugs | Always | +| `style` | warn | Idiomatic style | Always | +| `complexity` | warn | Overly complex code | Always | +| `perf` | warn | Performance issues | Always | +| `pedantic` | allow | Strict, opinionated | Libraries | +| `restriction` | allow | Very restrictive | Cherry-pick only | +| `nursery` | allow | Unstable | Cherry-pick only | +| `cargo` | allow | Cargo.toml issues | CI | + +--- + +## Recommended Baseline (Cargo.toml) + +```toml +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +# Lint groups (lower priority = applied first) +correctness = { level = "deny", priority = -1 } +suspicious = { level = "warn", priority = -1 } +style = { level = "warn", priority = -1 } +complexity = { level = "warn", priority = -1 } +perf = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } + +# Zero-panic enforcement +panic = "deny" +unwrap_used = "deny" +expect_used = "deny" +indexing_slicing = "deny" +todo = "deny" +unimplemented = "deny" + +# Additional strict lints +undocumented_unsafe_blocks = "deny" + +# Relax noisy pedantic lints +missing_errors_doc = "allow" +module_name_repetitions = "allow" +must_use_candidate = "allow" +similar_names = "allow" +too_many_lines = "allow" +``` + +--- + +## Zero-Panic Lints + +```toml +[lints.clippy] +# Direct panic sources +panic = "deny" +panic_in_result_fn = "deny" +todo = "deny" +unimplemented = "deny" +unreachable = "deny" + +# Implicit panics +unwrap_used = "deny" +expect_used = "deny" +indexing_slicing = "deny" + +# Arithmetic panics +integer_division = "deny" +modulo_arithmetic = "deny" +fallible_impl_from = "deny" +``` + +Handling false positives: + +```rust +// Best: restructure to avoid the need +let first = vec.first().ok_or(Error::EmptyVec)?; + +// When unavoidable, allow with reason +#[allow(clippy::unwrap_used, reason = "Vec non-empty after push")] +let first = vec.first().unwrap(); +``` + +--- + +## Key Performance Lints + +| Lint | Catches | +|------|---------| +| `box_collection` | `Box>` double indirection | +| `large_enum_variant` | One variant bloating enum size | +| `needless_collect` | `collect()` when direct iteration works | +| `or_fun_call` | `.or(expensive())` instead of `.or_else()` | +| `unnecessary_to_owned` | `.to_owned()` when borrow suffices | +| `useless_vec` | Vec when array works | +| `expect_fun_call` | `.expect(format!())` allocates even on Ok | +| `large_types_passed_by_value` | Pass large types by reference | +| `mutex_atomic` | `Mutex` instead of `AtomicBool` | + +--- + +## API Design Lints + +```toml +[lints.clippy] +missing_panics_doc = "warn" +missing_safety_doc = "warn" +missing_errors_doc = "warn" +must_use_candidate = "warn" +return_self_not_must_use = "warn" +undocumented_unsafe_blocks = "warn" +``` + +--- + +## Most Valuable Pedantic Lints + +Keep these even when relaxing pedantic overall: + +```toml +cloned_instead_of_copied = "warn" # .cloned() on Copy types +explicit_iter_loop = "warn" # for x in v.iter() -> for x in &v +manual_let_else = "warn" # match-to-return -> let-else +manual_string_new = "warn" # String::from("") -> String::new() +redundant_closure_for_method_calls = "warn" # |x| x.foo() -> T::foo +unnecessary_wraps = "warn" # Result when never errors +``` + +--- + +## clippy.toml Configuration + +```toml +# clippy.toml in project root +type-complexity-threshold = 300 +too-many-lines-threshold = 150 +too-many-arguments-threshold = 8 +pass-by-value-size-limit = 256 +enum-variant-size-threshold = 400 +cognitive-complexity-threshold = 30 + +disallowed-types = [ + { path = "std::collections::HashMap", reason = "Use BTreeMap for determinism" }, + { path = "std::collections::HashSet", reason = "Use BTreeSet for determinism" }, +] + +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-indexing-slicing-in-tests = true +``` + +--- + +## Running Clippy + +```bash +cargo clippy --all-targets # All targets +cargo clippy --all-targets --all-features # With all features +cargo clippy --fix --allow-dirty # Auto-fix +cargo clippy --all-targets -- -D warnings # CI (deny warnings) +cargo clippy --workspace --all-targets # Workspace +``` + +--- + +## Handling Violations + +### Scope Levels + +```rust +// Crate-wide (lib.rs / main.rs) +#![allow(clippy::module_name_repetitions)] + +// Item-level with reason (Rust 1.81+) +#[allow(clippy::unwrap_used, reason = "Guaranteed Some by invariant")] +fn guaranteed_some() -> i32 { STATIC_OPTION.unwrap() } + +// Inline +fn process() { + #[allow(clippy::indexing_slicing)] // Length checked above + let first = &slice[0]; +} + +// Tests +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::panic)] +} +``` + +--- + +## Restriction Lints (Cherry-Pick) + +Never enable the entire `restriction` group. Useful individual lints: + +```toml +dbg_macro = "warn" # dbg!() in production +print_stdout = "warn" # Use logging instead +print_stderr = "warn" +shadow_unrelated = "warn" # Shadowing with unrelated type +string_add = "warn" # s + "x" -> s.push_str("x") +wildcard_enum_match_arm = "warn" # _ => misses new variants +as_conversions = "warn" # Prefer TryFrom +clone_on_ref_ptr = "warn" # Rc::clone(&x) is clearer +mem_forget = "warn" # mem::forget usually wrong +exit = "warn" # std::process::exit is usually wrong +``` + +--- + +## Anti-Patterns + +| Anti-Pattern | Why | +|--------------|-----| +| `restriction = "warn"` | Hundreds of false positives | +| `#[allow]` without reason | Hides real issues | +| `pedantic = "deny"` | Will frustrate developers | +| `perf = "allow"` | Hides performance issues | + +--- + +## CI Integration + +```yaml +- name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings +``` + +--- + +## Lint Category Quick Reference + +| Category | Key Lints | Use Case | +|----------|-----------|----------| +| Zero-Panic | `unwrap_used`, `expect_used`, `panic`, `indexing_slicing` | Production | +| Performance | `perf` group, `large_enum_variant`, `needless_collect` | Hot paths | +| Safety | `undocumented_unsafe_blocks`, `missing_safety_doc` | Unsafe code | +| API Quality | `missing_panics_doc`, `must_use_candidate` | Public APIs | diff --git a/.llm/skills/code-review-lessons.md b/.llm/skills/code-review-lessons.md deleted file mode 100644 index 223c9931..00000000 --- a/.llm/skills/code-review-lessons.md +++ /dev/null @@ -1,759 +0,0 @@ -# Code Review Lessons Learned - -> **This document captures patterns and anti-patterns discovered through code review feedback.** -> Use these guidelines to prevent similar issues in future development. - ---- - -## Eager vs Lazy Error Construction (`ok_or` vs `ok_or_else`) - -### The Pattern - -When using `Option::ok_or()`, the error value is constructed eagerly (every time), even when -the Option contains a value. This can be wasteful in hot paths. - -```rust -// Eager construction — error built even when index is valid -value.ok_or(ExpensiveError { context: compute_context() })? - -// Lazy construction — error only built on error path -value.ok_or_else(|| ExpensiveError { context: compute_context() })? -``` - -### When to Use `ok_or_else` - -Use `ok_or_else(|| ...)` when: - -- The error type allocates (contains `String`, `Vec`, `Box`, etc.) -- Computing error field values is expensive -- The code is in a hot path (inner loops, frequently called functions) -- The error construction has side effects - -### When `ok_or` Is Fine - -Use simple `ok_or(...)` when: - -- The error type is `Copy` (no allocation, trivial construction) -- All field values are already computed or are `Copy` -- Clippy's `unnecessary_lazy_evaluations` lint would trigger - -**Important:** Clippy will warn if you use `ok_or_else` with a `Copy` type. Trust the lint — -for `Copy` types, eager construction is actually more efficient than closure overhead. - -```rust -// ❌ Clippy warning: unnecessary_lazy_evaluations -.ok_or_else(|| CopyError::IndexOutOfBounds { index, len }) - -// ✅ Correct for Copy types -.ok_or(CopyError::IndexOutOfBounds { index, len }) -``` - ---- - -## Result Type Alias Semver Hazard - -### The Problem - -Exporting a `Result` type alias at the crate root can shadow `std::result::Result` for -downstream users who use glob imports: - -```rust -// In your library -pub type Result = std::result::Result; - -// Downstream user -use my_library::*; // Now `Result` shadows std::result::Result! -``` - -### The Solution - -Use a distinctive name that cannot shadow standard library types: - -```rust -// ✅ Safe — cannot shadow std::result::Result -pub type FortressResult = std::result::Result; -``` - -### Best Practices - -1. **Use distinctive names** — `FortressResult`, `MyLibResult`, etc. -2. **Export from prelude only** — Don't export at crate root, only in a `prelude` module -3. **Document the pattern** — Show users how to alias locally if they prefer short names: - -```rust -// Users can create a local alias -use fortress_rollback::FortressResult as Result; -``` - ---- - -## Test-Production Code Alignment - -### The Problem - -Tests that simulate internal implementation details can drift from production code: - -```rust -// Production code (evolved) -fn decode(data: &[u8]) -> Result, MyError> { - inner_decode(data).map_err(|e| match e { - InnerError::Foo => MyError::Foo, - InnerError::Bar => MyError::Bar, - })? -} - -// Tests (stuck on old implementation) -fn test_error_mapping() { - let error: Box = Box::new(SomeError); - let result = error.downcast_ref::(); // Production doesn't do this! - // ... -} -``` - -### The Solution - -**Extract testable helpers.** When production code has error mapping logic, extract it: - -```rust -// Extracted helper — testable in isolation -fn map_inner_error(e: InnerError) -> MyError { - match e { - InnerError::Foo => MyError::Foo, - InnerError::Bar => MyError::Bar, - } -} - -// Production code uses the helper -fn decode(data: &[u8]) -> Result, MyError> { - inner_decode(data).map_err(map_inner_error)? -} - -// Tests test the helper directly -#[test] -fn test_map_inner_error_foo() { - assert_eq!(map_inner_error(InnerError::Foo), MyError::Foo); -} -``` - -### Best Practices - -1. **Test the actual code path** — Don't simulate patterns not in production -2. **Extract helpers for complex mappings** — Makes them unit-testable -3. **Add integration tests** — Verify end-to-end behavior with real inputs -4. **Review tests when refactoring** — Ensure tests still test the right thing - ---- - -## Kani Proof Naming and Verification - -### The Problem - -Proof names and documentation can claim properties that the proof doesn't actually verify: - -```rust -/// Proof: Clone creates independent copy. -/// Verifies that modifying one doesn't affect other. // <-- claim -#[kani::proof] -fn proof_clone_is_independent() { - let a = MyStruct::new(); - let b = a.clone(); - - // Only checks equality, never modifies! - kani::assert(a.field == b.field, "fields match"); -} -``` - -### The Solution - -**Proofs must verify what they claim.** If the name says "independent", actually test -modification independence: - -```rust -#[kani::proof] -fn proof_clone_is_independent() { - let a = MyStruct::new(); - let mut b = a.clone(); - - let original_value = a.field; - - // Actually modify the clone - b.field = different_value(); - - // Verify original is unchanged (independence) - kani::assert(a.field == original_value, "Original unchanged after modifying clone"); - - // Verify clone has modification - kani::assert(b.field != original_value, "Clone has new value"); -} -``` - -### Best Practices - -1. **Name proofs accurately** — `proof_clone_preserves_fields` vs `proof_clone_is_independent` -2. **Verify all claimed properties** — Read doc comments, ensure assertions match -3. **Consider renaming over extending** — If a proof tests X but claims Y, maybe rename to X -4. **Document proof scope clearly** — What exactly does this proof verify? - ---- - -## Doc Comments and Implementation Details - -### The Problem - -Doc comments that describe *how* code works (implementation details) become stale when the -implementation changes: - -```rust -/// Creates a violation with a unique ID. -/// Uses static string slices for zero-allocation performance. // <-- LIE! -fn make_violation(id: u32) -> Violation { - Violation::new( - format!("test violation {}", id), // Actually allocates! - ) -} -``` - -The doc comment claims "static string slices" but the code uses `format!()` which allocates. -This mismatch misleads readers and erodes trust in documentation. - -### The Root Cause - -Doc comments describing implementation details are inherently fragile because: - -1. **Code changes, comments don't** — Refactoring updates code but forgets comments -2. **Performance claims age poorly** — Optimizations may be added or removed -3. **Allocation patterns shift** — Moving from `&'static str` to `String` is common -4. **Reviewers focus on code** — Comments are often skimmed, not verified against code - -### The Solution - -**Doc comments should describe WHAT, not HOW** — unless the HOW is part of the API contract. - -```rust -// ❌ Describes implementation (fragile) -/// Creates a violation with a unique ID. -/// Uses static string slices for zero-allocation performance. - -// ✅ Describes behavior (stable) -/// Creates a violation with a unique ID. - -// ✅ OK if allocation IS the contract -/// Creates a violation with a unique ID. -/// -/// # Performance -/// -/// This function is allocation-free and suitable for hot paths. -/// (Note: This creates an API contract — changing it is breaking!) -``` - -### When Implementation Details ARE Appropriate - -Include HOW only when it's part of the API contract: - -- **Performance guarantees** — "O(1) lookup", "allocation-free" -- **Thread safety** — "Lock-free", "Uses interior mutability" -- **Determinism** — "Uses seeded RNG for reproducibility" -- **Resource management** — "Caches results", "Lazily initialized" - -But remember: documenting these creates an implicit contract. Changing them becomes a -breaking change, even if the function signature doesn't change. - -### Best Practices - -1. **Focus on WHAT, not HOW** — Describe behavior and purpose -2. **Omit performance claims** — Unless they're API guarantees -3. **Review comments when refactoring** — Update or remove stale implementation details -4. **Use `# Performance` sections** — Makes performance contracts explicit and findable -5. **Avoid redundant phrases** — "for testing purposes" in test code is obvious - ---- - -## GitHub Actions Permissions - -### The Problem - -Writing to system directories like `/usr/local/bin` requires elevated permissions on -GitHub-hosted runners: - -```yaml -# ❌ Can fail with "permission denied" -run: | - curl -sfL "$URL" | tar xz -C /usr/local/bin my_tool -``` - -### Solutions - -**Option 1: Use sudo** - -```yaml -# ✅ Works on GitHub-hosted runners -run: | - curl -sfL "$URL" | sudo tar xz -C /usr/local/bin my_tool -``` - -**Option 2: Install to user directory** - -```yaml -# ✅ No sudo needed -run: | - mkdir -p "$HOME/.local/bin" - curl -sfL "$URL" | tar xz -C "$HOME/.local/bin" my_tool - echo "$HOME/.local/bin" >> "$GITHUB_PATH" -``` - -### Best Practices - -1. **Be consistent** — Use the same pattern across all workflows -2. **Prefer sudo for /usr/local/bin** — It's simpler and widely understood -3. **Use GITHUB_PATH for custom directories** — Ensures tools are available to later steps -4. **Test on fresh runners** — Local dev containers may have different permissions - ---- - -## Pattern Matching in Error Mappers - -### The Problem - -Using `if let` with a fallthrough path causes use-after-move bugs: - -```rust -// ❌ BUG: `e` moved in if-let, unusable in fallback -fn map_error(e: MyError) -> OtherError { - if let MyError::Specific { data } = e { - return OtherError::Mapped { data }; - } - log::warn!("unexpected: {:?}", e); // ERROR: use of moved value! - OtherError::Unknown -} -``` - -### The Solution - -**Always use `match` for error mapping functions** that need to: - -1. Handle a specific variant -2. Log/warn about unexpected variants - -```rust -// ✅ CORRECT: Single match expression -fn map_error(e: MyError) -> OtherError { - match e { - MyError::Specific { data } => OtherError::Mapped { data }, - other => { - log::warn!("unexpected: {:?}", other); - OtherError::Unknown - } - } -} -``` - -### Why This Pattern Exists - -Error mapping functions commonly need to: - -- Extract fields from a specific error variant for the happy path -- Handle unexpected variants gracefully with logging/metrics -- Include the original error in the fallback for debugging - -The `if let` + fallthrough pattern seems natural but moves ownership before the fallback. - -### Best Practices - -1. **Prefer `match` over `if let` + fallthrough** when you need the value in both paths -2. **Use `other` binding in catch-all arm** — gives access to the unmatched value -3. **Consider borrowing** — `if let Pattern = &value` if you don't need ownership -4. **Add `Unknown` variants to reason enums** — provides a fallback for error mapping - -See also: [rust-pitfalls.md](rust-pitfalls.md#use-after-move-in-if-let-fallthrough) - ---- - -## Shell Script Regex Flag Consistency - -### The Problem - -Using different regex modes in the same script leads to inconsistent behavior — one command -finds matches while another silently fails: - -```bash -# ❌ BUG: Mixed regex modes -# Count uses extended regex (works with +, |, ?) -count=$(grep -Ec 'pattern1|pattern2+' file.txt) -echo "Found: $count matches" - -# Display uses basic regex (+ and | are literal characters!) -grep -n 'pattern1|pattern2+' file.txt -# Output: nothing (even though count was > 0) -``` - -In basic regex (BRE), characters like `+`, `?`, `|`, and `()` are treated as literals. -Extended regex (ERE) treats them as metacharacters. Mixing modes causes correct counts -but empty output — a confusing failure mode. - -### The Solution - -**Use consistent regex flags throughout a script:** - -```bash -# ✅ CORRECT: Consistent extended regex everywhere -count=$(grep -Ec 'pattern1|pattern2+' file.txt) -grep -En 'pattern1|pattern2+' file.txt - -# ✅ ALTERNATIVE: Escape metacharacters in basic regex -count=$(grep -c 'pattern1\|pattern2\+' file.txt) -grep -n 'pattern1\|pattern2\+' file.txt -``` - -### Extended vs Basic Regex Quick Reference - -| Character | Basic Regex (BRE) | Extended Regex (ERE) | -|-----------|-------------------|----------------------| -| `+` | Literal `+` | One or more | -| `?` | Literal `?` | Zero or one | -| `\|` | Alternation | Literal `\|` | -| `()` | Literal `()` | Grouping | -| `\(\)` | Grouping | Literal `()` | - -### Best Practices - -1. **Use `-E` (extended regex) consistently** — It's more intuitive for complex patterns -2. **Define regex mode at script top** — Use a variable: `GREP_FLAGS="-E"` -3. **Test both count and display commands** — Verify they produce matching results -4. **Use shellcheck** — It catches some regex inconsistencies -5. **Prefer `grep -E` over `egrep`** — `egrep` is deprecated - ---- - -## Shell Script Grep Count Error Handling - -### The Problem - -When using `grep -c` (count matches) with `|| true` or `|| echo "0"` under `set -euo pipefail`, -the variable can become empty or contain unexpected values, causing numeric comparisons to fail. - -**Understanding grep exit codes:** - -| Exit Code | Meaning | `grep -c` Output | -|-----------|---------|------------------| -| 0 | Matches found | Count (e.g., "5") | -| 1 | No matches found | "0" | -| 2 | Error (file not found, permission denied) | Nothing | - -**The broken pattern:** - -```bash -#!/bin/bash -set -euo pipefail - -# ❌ BROKEN: Produces "0\n0" when no matches found -COUNT=$(grep -Ec "pattern" "$file" 2>/dev/null || echo "0") -if [[ "$COUNT" -gt 0 ]]; then # Fails: "0\n0" is not a valid integer - echo "Found $COUNT matches" -fi -``` - -When `grep -c` finds **no matches** in an existing file: - -1. It outputs "0" to stdout -2. It exits with code 1 (failure) -3. The `|| echo "0"` fires because of exit code 1 -4. Result: `COUNT` contains `"0\n0"` — **two zeros separated by a newline** - -This causes `[[ "$COUNT" -gt 0 ]]` to fail with: - -``` -bash: [[: 0 -0: syntax error in expression (error token is "0") -``` - -**Another broken pattern:** - -```bash -# ❌ BROKEN: COUNT may be empty if grep exits with code 2 (file error) -COUNT=$(grep -Ec "pattern" "$file" 2>/dev/null || true) -if [[ "$COUNT" -gt 0 ]]; then # Fails if COUNT is empty - echo "Found matches" -fi -``` - -### The Solution - -**Use `|| true` to suppress the exit code, then default to 0 if empty:** - -```bash -# ✅ CORRECT: Handles all cases safely -COUNT=$(grep -Ec "pattern" "$file" 2>/dev/null || true) -COUNT=${COUNT:-0} # Default to 0 if empty - -if [[ "$COUNT" -gt 0 ]]; then - echo "Found $COUNT matches" -fi -``` - -**Why this works:** - -| Scenario | `grep -c` Output | Exit Code | After `\|\| true` | After `${COUNT:-0}` | -|----------|------------------|-----------|-------------------|---------------------| -| Matches found | "5" | 0 | "5" | "5" ✓ | -| No matches | "0" | 1 | "0" | "0" ✓ | -| File error | "" (empty) | 2 | "" | "0" ✓ | - -### Best Practices - -1. **Always use `|| true` + `${VAR:-0}`** — Never use `|| echo "0"` for grep counts -2. **Add a comment explaining the pattern** — Future maintainers may not know the pitfall -3. **Validate before arithmetic** — Use `[[ "$VAR" =~ ^[0-9]+$ ]]` if paranoid -4. **Prefer `wc -l` for line counting** — `wc -l` exits 0 even for zero lines - -```bash -# Alternative: wc -l always exits 0 -COUNT=$(grep -E "pattern" "$file" 2>/dev/null | wc -l) -# No fallback needed — wc -l outputs "0" and exits 0 for empty input -``` - -### Related Scripts in This Repository - -The correct pattern is used in: - -- `scripts/check-code-fence-syntax.sh` -- `scripts/check-kani-coverage.sh` -- `scripts/sync-version.sh` -- `scripts/verify-markdown-code.sh` -- `.github/workflows/ci-quality.yml` - ---- - -## Cross-Platform Documentation Accuracy - -### The Problem - -Documentation claims cross-platform compatibility that isn't actually true: - -```markdown - -# Pre-commit Hooks - -Works on all platforms — no Git Bash or WSL required! - - -#!/bin/bash -# This requires bash, which isn't default on Windows! -``` - -When new scripts or hooks are added, existing cross-platform claims may become false. - -### The Solution - -**Validate cross-platform claims whenever adding platform-dependent components:** - -1. **Audit existing claims** — Search docs for "cross-platform", "all platforms", "Windows" -2. **Update or qualify claims** — Be specific about what works where -3. **Document requirements** — List what's needed on each platform - -```markdown - -# Pre-commit Hooks - -Requires bash. On Windows, use Git Bash, WSL, or install bash via MSYS2. - - -The pre-commit hook uses: -- **Unix/macOS**: Native bash script -- **Windows**: PowerShell version (see `hooks/pre-commit.ps1`) -``` - -### Cross-Platform Audit Checklist - -When adding new scripts or hooks, verify: - -- [ ] Shell scripts: Do they require bash, sh, or work with cmd/PowerShell? -- [ ] Path separators: `/` vs `\` — use `$()` or portable path handling -- [ ] Line endings: Will CRLF break the script? -- [ ] Available tools: Are `grep`, `sed`, `awk` available on Windows? -- [ ] File permissions: Does the script need `chmod +x`? - -### Best Practices - -1. **Be specific, not absolute** — "Works on Linux/macOS" is better than "cross-platform" -2. **List platform requirements** — "Requires bash 4.0+" or "Tested on: Ubuntu, macOS, Git Bash" -3. **Audit docs when adding scripts** — Search for platform claims and validate them -4. **Consider CI validation** — Run scripts on multiple OS runners to verify claims -5. **Provide alternatives** — PowerShell equivalents for Windows users - ---- - -## Documentation Code Example Accuracy - -### The Problem - -Code examples in documentation don't match the actual API: - -```markdown - -return Err(FortressError::InvalidRequest { info: "bad handle" }); - - -return Err(FortressError::InvalidRequest { - kind: InvalidRequestKind::InvalidSpectatorHandle, -}); -// Plus: InvalidRequestKind has a From impl for conversion -``` - -Readers try the documented pattern, get compiler errors, and lose trust in the docs. - -### The Solution - -**Option 1: Use exact API patterns (preferred)** - -```rust -// ✅ Exact API usage — compiles and works -return Err(FortressError::InvalidRequest { - kind: InvalidRequestKind::InvalidSpectatorHandle, -}); -``` - -**Option 2: Mark as pseudo-code explicitly** - -````markdown -```text -// Pseudo-code — see API docs for exact syntax -return Err(FortressError::InvalidRequest { info: "..." }); -``` -```` - -Or use inline annotation: - -```rust -// Simplified for illustration — actual API differs -return Err(Error::SomeVariant { ... }); -``` - -**Option 3: Use doc tests to validate examples** - -```rust -/// # Example -/// -/// ``` -/// # use my_crate::*; -/// let result = fallible_operation(); -/// assert!(result.is_err()); -/// ``` -``` - -Doc tests won't compile if the API changes, catching drift automatically. - -### Best Practices - -1. **Prefer compilable examples** — Use real API patterns that work -2. **Run doc tests in CI** — Catches documentation drift automatically -3. **Mark pseudo-code clearly** — Use `text` fence or explicit comments -4. **Keep examples minimal** — Less code = less to keep in sync -5. **Link to real examples** — "See `examples/error_handling.rs` for complete usage" -6. **Review examples when changing APIs** — Search docs for affected patterns - ---- - -## Metrics Consistency Across Documentation - -### The Problem - -Project metrics appear in multiple places with different values: - -```markdown - -✅ 1100+ tests with comprehensive coverage - - -The test suite includes ~1500 tests... - - -We maintain over 1000 tests... -``` - -Which is correct? Readers notice inconsistencies and question accuracy. - -### The Solution - -**Option 1: Centralize metrics (recommended for frequently-updated values)** - -Create a single source of truth: - -```markdown - -- **Test count**: ~1500 (as of 2025-01) -- **Coverage**: >90% -- **Supported platforms**: 5 -``` - -Reference via includes or variables: - -```markdown - -{% include "includes/metrics.md" %} -``` - -**Option 2: Use relative/stable descriptions** - -Avoid specific numbers that will become stale: - -```markdown - -Comprehensive test suite with >90% coverage - - -1,523 tests covering all modules -``` - -**Option 3: Automate metric extraction** - -Generate metrics from CI: - -```yaml -- name: Update metrics - run: | - TEST_COUNT=$(cargo test --no-run 2>&1 | grep -oP '\d+ tests') - # Use sed -i '' on macOS, sed -i on GNU/Linux - # This pattern works on both by creating a backup then removing it - sed -i.bak "s/TEST_COUNT_PLACEHOLDER/$TEST_COUNT/" docs/metrics.md - rm -f docs/metrics.md.bak -``` - -### Metrics That Should Be Centralized - -| Metric | Why | Recommended Approach | -|--------|-----|---------------------| -| Test count | Changes frequently | Use "comprehensive" or automate | -| Coverage % | Changes per commit | Centralize or use badge | -| Supported platforms | Changes rarely | OK to duplicate, audit quarterly | -| Performance benchmarks | Must be reproducible | Single source + methodology | - -### Best Practices - -1. **Audit metrics quarterly** — Search for numbers and percentages across docs -2. **Use relative terms for volatile metrics** — "comprehensive", "high coverage" -3. **Centralize stable metrics** — Platform count, version requirements -4. **Automate where possible** — Generate from CI, use badges -5. **Date your metrics** — "~1500 tests (as of 2025-01)" helps readers calibrate -6. **Search before adding** — Check if the metric exists elsewhere before duplicating - ---- - -## Summary Checklist - -Before submitting code: - -- [ ] `ok_or` vs `ok_or_else` — Used correctly based on error type (Copy vs allocating) -- [ ] Type aliases — Use distinctive names that can't shadow stdlib types -- [ ] Tests match production — No simulating patterns not in real code -- [ ] Kani proofs — Actually verify what their names/docs claim -- [ ] Doc comments — Describe WHAT, not HOW (unless HOW is API contract) -- [ ] CI permissions — Use sudo for system directories -- [ ] Pattern matching — Use `match` not `if let` when fallback needs the value -- [ ] Shell scripts — Use consistent regex flags (`-E` everywhere or escape metacharacters) -- [ ] Cross-platform claims — Validate after adding scripts/hooks -- [ ] Code examples — Use real API patterns or mark as pseudo-code -- [ ] Metrics — Check for consistency across all documentation - ---- - -*This document should be updated as new patterns are discovered through code review.* diff --git a/.llm/skills/collection-determinism.md b/.llm/skills/collection-determinism.md deleted file mode 100644 index dbef09f4..00000000 --- a/.llm/skills/collection-determinism.md +++ /dev/null @@ -1,836 +0,0 @@ -# Collection and Data Structure Determinism in Rust - -This guide covers achieving deterministic behavior with Rust collections, essential for rollback netcode, replay systems, and any application requiring reproducible execution across machines and runs. - -## Table of Contents - -- [Why Collection Determinism Matters](#why-collection-determinism-matters) -- [HashMap Non-Determinism](#hashmap-non-determinism) -- [Deterministic Alternatives](#deterministic-alternatives) -- [Detecting Non-Determinism](#detecting-non-determinism) -- [Practical Patterns](#practical-patterns) -- [WASM and no_std Concerns](#wasm-and-no_std-concerns) -- [Migration Patterns](#migration-patterns) -- [Performance Tradeoffs](#performance-tradeoffs) - ---- - -## Why Collection Determinism Matters - -Collection iteration order can cause **silent desyncs** in networked applications. If two peers iterate over the same data but in different orders, their state diverges even though they processed the same inputs. - -```rust -// ⚠️ DANGEROUS: Two peers may see different iteration orders -let map: HashMap = /* ... */; -for (id, player) in &map { - update_player(player); // Order affects game state! -} -``` - -This is particularly insidious because: - -- The bug is **non-deterministic** — it may work in testing but fail in production -- The bug is **platform-dependent** — it may work on your machine but fail on others -- The bug is **run-dependent** — it may work 99 times and fail on the 100th - ---- - -## HashMap Non-Determinism - -### Why std::collections::HashMap Has Randomized Iteration Order - -Rust's `HashMap` uses `RandomState` as its default hasher, which: - -1. **Obtains entropy at runtime** via the `getrandom` syscall -2. **Creates a unique hash seed** for each `RandomState` instance -3. **Produces different hash values** for the same keys across runs - -```rust -use std::collections::HashMap; - -// Each time you run this program, iteration order may differ! -let mut map = HashMap::new(); -map.insert("a", 1); -map.insert("b", 2); -map.insert("c", 3); - -for (k, v) in &map { - println!("{k}: {v}"); // Order is unpredictable! -} -``` - -### The RandomState Hasher and Security Implications - -`RandomState` exists to prevent **HashDoS attacks**, where an attacker crafts inputs that all hash to the same bucket, degrading O(1) lookups to O(n). - -```rust -use std::collections::hash_map::RandomState; - -// RandomState generates random keys at construction -let s1 = RandomState::new(); -let s2 = RandomState::new(); - -// These will produce DIFFERENT hashers -let h1 = s1.build_hasher(); -let h2 = s2.build_hasher(); -``` - -**Security trade-off:** Randomized hashing prevents DoS attacks but breaks determinism. For game state that isn't exposed to adversarial input, deterministic hashing is often acceptable. - -### ahash and const-random: Compile-Time Randomness - -The `ahash` crate (used by many popular crates including `hashbrown`) can introduce non-determinism in two ways: - -#### 1. Runtime Randomness (Default) - -```rust -use ahash::RandomState; - -// Default: obtains entropy from OS at runtime -let state = RandomState::default(); -``` - -#### 2. Compile-Time Randomness (`compile-time-rng` feature) - -```toml -# Cargo.toml - This makes your binary non-reproducible! -[dependencies] -ahash = { version = "0.8", features = ["compile-time-rng"] } -``` - -With `compile-time-rng`, ahash generates random constants **at compile time**, meaning: - -- Different builds produce different binaries -- Even `cargo clean && cargo build` creates a different binary -- CI builds may differ from local builds - -**Warning:** The `const-random` crate used by ahash reads from `/dev/urandom` at compile time! - -### How HashMap Randomization Leaks Through Dependencies - -Non-determinism can **propagate silently** through your dependency tree: - -```bash -# Check if any dependency uses ahash or getrandom -cargo tree | grep -E "ahash|getrandom|const-random" - -# Example output showing potential issues: -# ├── bevy_utils v0.15.0 -# │ └── ahash v0.8.11 -# │ ├── getrandom v0.2.15 -# │ └── const-random v0.1.18 -``` - -Common culprits: - -- `hashbrown` (powers `std::collections::HashMap`) -- `bevy_utils` (Bevy's default hasher) -- `indexmap` with default features -- `serde_json` (for deserializing into HashMap) - ---- - -## Deterministic Alternatives - -### BTreeMap/BTreeSet: Ordered Iteration - -`BTreeMap` iterates in **key-sorted order**, making it fully deterministic. - -```rust -use std::collections::BTreeMap; - -let mut map = BTreeMap::new(); -map.insert(3, "three"); -map.insert(1, "one"); -map.insert(2, "two"); - -// Always iterates in sorted order: 1, 2, 3 -for (k, v) in &map { - println!("{k}: {v}"); -} -``` - -**Requirements:** - -- Keys must implement `Ord` (not just `Hash + Eq`) -- Iteration is O(n) regardless of access pattern - -```rust -// ✅ Works: PlayerId implements Ord -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct PlayerId(u32); - -let players: BTreeMap = BTreeMap::new(); -``` - -### IndexMap/IndexSet: Insertion-Order with O(1) Lookups - -`IndexMap` maintains **insertion order** while providing hash-map performance. - -```toml -# Cargo.toml -[dependencies] -indexmap = "2" -``` - -```rust -use indexmap::IndexMap; - -let mut map = IndexMap::new(); -map.insert("first", 1); -map.insert("second", 2); -map.insert("third", 3); - -// Always iterates in insertion order: first, second, third -for (k, v) in &map { - println!("{k}: {v}"); -} - -// O(1) lookups still work -assert_eq!(map.get("second"), Some(&2)); -``` - -**Determinism caveat:** IndexMap is deterministic **only if insertion order is deterministic**. If you insert based on HashMap iteration, you inherit that non-determinism. - -```rust -// ⚠️ WRONG: Insertion order depends on HashMap iteration -let hash_map: HashMap = /* ... */; -let index_map: IndexMap = hash_map.into_iter().collect(); - -// ✅ CORRECT: Sort before converting -let mut pairs: Vec<_> = hash_map.into_iter().collect(); -pairs.sort_by_key(|(k, _)| k.clone()); -let index_map: IndexMap = pairs.into_iter().collect(); -``` - -### Using HashMap::with_hasher() with Deterministic Hashers - -You can use `HashMap` with a deterministic hasher for O(1) lookups without iteration-order guarantees: - -```rust -use std::collections::HashMap; -use std::hash::BuildHasherDefault; - -// FNV-1a: Simple, fast, deterministic -use fnv::FnvHasher; -type FnvHashMap = HashMap>; - -let mut map: FnvHashMap = FnvHashMap::default(); -map.insert(1, "one"); - -// Or use ahash with fixed seed -use ahash::RandomState; - -let fixed_state = RandomState::with_seed(42); -let mut map = HashMap::with_hasher(fixed_state); -map.insert(1, "one"); -``` - -**Important:** This makes hashing deterministic but **iteration order is still not guaranteed**! Use this only when you need deterministic hashes (e.g., for checksums) but don't iterate. - -### Custom Deterministic Hasher Example - -```rust -use std::hash::{Hash, Hasher}; - -/// FNV-1a hasher - deterministic across all platforms -pub struct DeterministicHasher { - state: u64, -} - -const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325; -const FNV_PRIME: u64 = 0x0100_0000_01b3; - -impl DeterministicHasher { - pub const fn new() -> Self { - Self { state: FNV_OFFSET_BASIS } - } -} - -impl Default for DeterministicHasher { - fn default() -> Self { - Self::new() - } -} - -impl Hasher for DeterministicHasher { - fn finish(&self) -> u64 { - self.state - } - - fn write(&mut self, bytes: &[u8]) { - for &byte in bytes { - self.state ^= u64::from(byte); - self.state = self.state.wrapping_mul(FNV_PRIME); - } - } -} - -// BuildHasher implementation for use with HashMap -#[derive(Clone, Copy, Default)] -pub struct DeterministicBuildHasher; - -impl std::hash::BuildHasher for DeterministicBuildHasher { - type Hasher = DeterministicHasher; - - fn build_hasher(&self) -> Self::Hasher { - DeterministicHasher::new() - } -} - -// Usage -type DetHashMap = std::collections::HashMap; -``` - -### stable-hash Crate for Cross-Platform Stable Hashing - -For hashes that must be stable across platforms, Rust versions, and time: - -```toml -# Cargo.toml -[dependencies] -stable-hash = "0.4" -``` - -```rust -use stable_hash::StableHasher; -use std::hash::Hash; - -fn stable_hash(value: &T) -> u64 { - let mut hasher = StableHasher::new(); - value.hash(&mut hasher); - hasher.finish() -} -``` - ---- - -## Detecting Non-Determinism - -### Using strace to Detect getrandom Syscalls - -On Linux, use `strace` to detect if your program calls `getrandom`: - -```bash -# During compilation (detects compile-time randomness) -strace -e getrandom cargo build 2>&1 | grep getrandom - -# During test execution -strace -e getrandom cargo test 2>&1 | grep getrandom - -# Example output showing randomness being used: -# getrandom("\x0b\xf9\x61\xc3\x41\x34\x99\x52", 8, GRND_NONBLOCK) = 8 -``` - -If you see `getrandom` calls during compilation, you have compile-time randomness. If you see them during runtime, you have runtime randomness. - -### Auditing Dependencies for HashMap Usage - -```bash -# Find all HashMap usages in your crate -rg "HashMap" --type rust - -# Find HashMap in dependencies (requires source) -rg "HashMap" -g '*.rs' ~/.cargo/registry/src/ - -# Check dependency features -cargo tree -f '{p} {f}' | grep -E "ahash|random" - -# Find getrandom in dependency tree -cargo tree -i getrandom -``` - -### CI Tests for Reproducibility - -```rust -#[test] -fn test_deterministic_iteration() { - use std::collections::BTreeMap; - - // Run multiple times with same data in different insertion orders - for _ in 0..10 { - let mut map1 = BTreeMap::new(); - map1.insert(2, "two"); - map1.insert(1, "one"); - map1.insert(3, "three"); - - let mut map2 = BTreeMap::new(); - map2.insert(3, "three"); - map2.insert(1, "one"); - map2.insert(2, "two"); - - let vec1: Vec<_> = map1.iter().collect(); - let vec2: Vec<_> = map2.iter().collect(); - - assert_eq!(vec1, vec2, "Iteration order must be deterministic"); - } -} - -#[test] -fn test_game_state_reproducibility() { - let seed = 12345u64; - let inputs = generate_test_inputs(); - - // Run game twice with same inputs - let state1 = run_game(seed, &inputs); - let state2 = run_game(seed, &inputs); - - // Must produce identical checksums - assert_eq!( - state1.checksum, state2.checksum, - "Game state must be reproducible" - ); -} -``` - -**CI workflow for cross-platform verification:** - -```yaml -# .github/workflows/determinism.yml -name: Determinism Check - -on: [push, pull_request] - -jobs: - test: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - name: Generate checksum - run: cargo run --release --example generate_checksum > checksum.txt - - uses: actions/upload-artifact@v4 - with: - name: checksum-${{ matrix.os }} - path: checksum.txt - - verify: - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@v4 - - name: Compare checksums - run: | - # All platforms must produce identical checksums - cat checksum-*/checksum.txt | sort -u | wc -l | grep -q '^1$' \ - || (echo "CHECKSUMS DIFFER!" && exit 1) -``` - ---- - -## Practical Patterns - -### Pattern 1: Sort Before Iterating Over HashMap - -When you must use HashMap for performance but need deterministic iteration: - -```rust -use std::collections::HashMap; - -let map: HashMap = /* ... */; - -// ✅ Sort keys before iterating -let mut keys: Vec<_> = map.keys().collect(); -keys.sort(); - -for key in keys { - let entity = &map[key]; - process_entity(entity); -} - -// Or collect and sort key-value pairs -let mut pairs: Vec<_> = map.iter().collect(); -pairs.sort_by_key(|(k, _)| *k); - -for (key, value) in pairs { - process(key, value); -} -``` - -### Pattern 2: BTreeMap for Iteration-Order-Sensitive Code - -```rust -use std::collections::BTreeMap; - -/// Player registry with deterministic iteration -struct PlayerRegistry { - // BTreeMap ensures iteration is always by PlayerHandle order - players: BTreeMap, -} - -impl PlayerRegistry { - fn update_all(&mut self, delta: f32) { - // Iteration order is deterministic (sorted by PlayerHandle) - for (handle, player) in &mut self.players { - player.update(delta); - } - } - - fn compute_checksum(&self) -> u64 { - let mut hasher = DeterministicHasher::new(); - // Order is guaranteed, so checksum is deterministic - for (handle, player) in &self.players { - handle.hash(&mut hasher); - player.hash(&mut hasher); - } - hasher.finish() - } -} -``` - -### Pattern 3: Feature Flags for Hasher Control - -```toml -# Cargo.toml -[features] -default = ["std"] -std = [] -deterministic = [] # Use fixed hasher for testing/debugging - -[dependencies] -ahash = { version = "0.8", optional = true } -``` - -```rust -// src/hash.rs -#[cfg(feature = "deterministic")] -pub type GameHashMap = std::collections::HashMap; - -#[cfg(not(feature = "deterministic"))] -pub type GameHashMap = std::collections::HashMap; - -// Usage -let mut map: GameHashMap = GameHashMap::default(); -``` - -### Pattern 4: IndexMap for Hash Performance with Determinism - -```rust -use indexmap::IndexMap; - -/// Component storage with deterministic iteration and fast lookups -struct ComponentStore { - // IndexMap provides O(1) lookups and insertion-order iteration - components: IndexMap, -} - -impl ComponentStore { - fn new() -> Self { - Self { components: IndexMap::new() } - } - - fn insert(&mut self, id: EntityId, component: T) { - // Insertion order is preserved - self.components.insert(id, component); - } - - fn get(&self, id: &EntityId) -> Option<&T> { - // O(1) lookup - self.components.get(id) - } - - fn iter(&self) -> impl Iterator { - // Iterates in insertion order (deterministic if insertions are) - self.components.iter() - } - - /// Sort by key for fully deterministic iteration - fn iter_sorted(&self) -> impl Iterator - where - EntityId: Ord, - { - let mut pairs: Vec<_> = self.components.iter().collect(); - pairs.sort_by_key(|(k, _)| *k); - pairs.into_iter() - } -} -``` - -### Pattern 5: Type Aliases for Clarity - -```rust -// src/collections.rs - -/// Deterministic map - iteration order guaranteed by key ordering -pub type DetMap = std::collections::BTreeMap; - -/// Deterministic set - iteration order guaranteed by value ordering -pub type DetSet = std::collections::BTreeSet; - -/// Insertion-order map - deterministic if insertion order is deterministic -pub type InsertOrderMap = indexmap::IndexMap; - -/// Fast lookup map - NOT deterministic for iteration -/// Only use when you never iterate, or always sort before iterating -pub type FastMap = std::collections::HashMap; -``` - ---- - -## WASM and no_std Concerns - -### hashbrown Behavior in no_std - -`hashbrown` (which powers `std::collections::HashMap`) requires a hasher in no_std: - -```rust -// no_std environment -#![no_std] -extern crate alloc; - -use hashbrown::HashMap; -use core::hash::BuildHasherDefault; - -// Must provide a hasher - no default RandomState! -type NoStdHashMap = HashMap>; - -let mut map: NoStdHashMap = HashMap::default(); -``` - -### Feature Leaking Can Introduce Non-Determinism - -Dependencies can enable features that add randomness: - -```toml -# Your Cargo.toml -[dependencies] -some-crate = "1.0" - -# some-crate's Cargo.toml might have: -[dependencies] -ahash = { version = "0.8", features = ["compile-time-rng"] } -# This affects YOUR binary too! -``` - -**Mitigation strategies:** - -1. **Audit dependencies:** - - ```bash - cargo tree -f '{p} {f}' | grep ahash - ``` - -2. **Use `default-features = false`:** - - ```toml - [dependencies] - some-crate = { version = "1.0", default-features = false } - ``` - -3. **Pin dependency versions** and audit changes: - - ```toml - [dependencies] - ahash = "=0.8.11" # Exact version - ``` - -### WASM-Specific Randomness - -In WASM, `getrandom` behavior depends on the target: - -- **wasm32-unknown-unknown:** No default entropy source -- **wasm32-wasi:** Uses WASI's `random_get` -- **wasm32-unknown-emscripten:** Uses Emscripten's entropy - -```toml -# For deterministic WASM builds -[target.wasm32-unknown-unknown.dependencies] -getrandom = { version = "0.2", features = ["custom"] } - -# Or disable getrandom entirely and use fixed hasher -``` - ---- - -## Migration Patterns - -### Migrating HashMap to BTreeMap - -```rust -// Before: Non-deterministic -use std::collections::HashMap; - -struct GameState { - entities: HashMap, -} - -// After: Deterministic -use std::collections::BTreeMap; - -struct GameState { - entities: BTreeMap, -} - -// Key type must implement Ord -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -struct EntityId(u32); -``` - -**API differences to handle:** - -| HashMap Method | BTreeMap Equivalent | Notes | -|---------------|---------------------|-------| -| `new()` | `new()` | Same | -| `with_capacity(n)` | `new()` | BTreeMap doesn't pre-allocate | -| `reserve(n)` | N/A | BTreeMap doesn't support | -| `shrink_to_fit()` | N/A | Not applicable | -| `entry(k)` | `entry(k)` | Same API | - -### Migrating HashMap to IndexMap - -```rust -// Before -use std::collections::HashMap; - -let mut map: HashMap = HashMap::new(); - -// After -use indexmap::IndexMap; - -let mut map: IndexMap = IndexMap::new(); -``` - -IndexMap is largely API-compatible with HashMap, plus additional methods: - -```rust -use indexmap::IndexMap; - -let mut map = IndexMap::new(); -map.insert("a", 1); -map.insert("b", 2); - -// IndexMap-specific methods -map.get_index(0); // Get by insertion index -map.swap_remove("a"); // Remove and swap with last (O(1)) -map.shift_remove("a"); // Remove and shift (preserves order, O(n)) -map.sort_keys(); // Sort in-place by keys -map.reverse(); // Reverse order -``` - -### Gradual Migration with Type Aliases - -```rust -// Phase 1: Create aliases pointing to old types -pub type PlayerMap = std::collections::HashMap; - -// Phase 2: Update code to use aliases (no behavior change) -let players: PlayerMap = PlayerMap::new(); - -// Phase 3: Change alias to deterministic type -pub type PlayerMap = std::collections::BTreeMap; - -// Phase 4: Fix any compilation errors (missing Ord, etc.) -``` - ---- - -## Performance Tradeoffs - -| Collection | Lookup | Insert | Iterate | Deterministic | Memory | -|-----------|--------|--------|---------|---------------|--------| -| `HashMap` | O(1) | O(1) | O(n) | ❌ No | Lower | -| `BTreeMap` | O(log n) | O(log n) | O(n) | ✅ Yes (sorted) | Higher | -| `IndexMap` | O(1) | O(1) | O(n) | ⚠️ If insertions are | Higher | -| `HashMap` + sort | O(1) | O(1) | O(n log n) | ✅ Yes | Lower | - -### When to Use Each - -**Use `BTreeMap` when:** - -- You iterate frequently -- Data set is small to medium (<10,000 entries) -- You need range queries (`range()`, `range_mut()`) -- Keys naturally have an ordering - -**Use `IndexMap` when:** - -- You need O(1) lookups AND deterministic iteration -- Insertion order has meaning -- Data set is large -- You're migrating from HashMap with minimal changes - -**Use `HashMap` + sort when:** - -- Lookups vastly outnumber iterations -- You only occasionally need to iterate -- Memory is a concern - -**Use `HashMap` with fixed hasher when:** - -- You NEVER iterate over entries -- You only need deterministic hashes (for checksums) -- Maximum lookup performance is required - -### Benchmarking Example - -```rust -use criterion::{criterion_group, criterion_main, Criterion}; -use std::collections::{BTreeMap, HashMap}; -use indexmap::IndexMap; - -fn benchmark_iteration(c: &mut Criterion) { - let n = 10_000; - - let hash_map: HashMap = (0..n).map(|i| (i, i)).collect(); - let btree_map: BTreeMap = (0..n).map(|i| (i, i)).collect(); - let index_map: IndexMap = (0..n).map(|i| (i, i)).collect(); - - c.bench_function("HashMap iterate", |b| { - b.iter(|| { - let mut keys: Vec<_> = hash_map.keys().collect(); - keys.sort(); - keys.iter().map(|k| hash_map[k]).sum::() - }) - }); - - c.bench_function("BTreeMap iterate", |b| { - b.iter(|| hash_map.values().sum::()) - }); - - c.bench_function("IndexMap iterate", |b| { - b.iter(|| index_map.values().sum::()) - }); -} - -criterion_group!(benches, benchmark_iteration); -criterion_main!(benches); -``` - ---- - -## Quick Reference Checklist - -### For Game State (Must Be Deterministic) - -- [ ] Use `BTreeMap`/`BTreeSet` instead of `HashMap`/`HashSet` -- [ ] Or use `IndexMap`/`IndexSet` with deterministic insertion order -- [ ] Ensure all keys implement `Ord` (for BTreeMap) or maintain insertion order -- [ ] Never iterate over `HashMap` without sorting first -- [ ] Audit dependencies for `ahash`, `getrandom`, `const-random` - -### For Checksums/Hashing - -- [ ] Use a deterministic hasher (FNV-1a, SipHash with fixed key) -- [ ] Hash in deterministic order (sorted keys or stable index) -- [ ] Don't use `DefaultHasher` (it's randomized) - -### For Testing - -- [ ] Run same scenario multiple times, verify identical results -- [ ] Test on multiple platforms via CI -- [ ] Use `strace -e getrandom` to detect runtime randomness -- [ ] Compare checksums across runs and platforms - -### For Dependencies - -- [ ] Audit `cargo tree` for hash-related crates -- [ ] Use `default-features = false` where possible -- [ ] Consider pinning versions of hash crates -- [ ] Test with `--features deterministic` in CI - ---- - -*Collection determinism is subtle but critical. When in doubt, use `BTreeMap` — the performance cost is usually acceptable, and the bugs you avoid are worth it.* diff --git a/.llm/skills/concurrency-patterns.md b/.llm/skills/concurrency-patterns.md deleted file mode 100644 index fa09198a..00000000 --- a/.llm/skills/concurrency-patterns.md +++ /dev/null @@ -1,563 +0,0 @@ -# Concurrent Rust Patterns — Thread Safety Without Tears - -> **This document provides guidance for writing correct concurrent Rust code.** -> These patterns complement loom testing and help prevent common concurrency bugs. - -## Core Philosophy - -**Concurrent code should be correct by construction, not by luck.** Achieve this through: - -1. **Minimize shared mutable state** — Less sharing = fewer bugs -2. **Use appropriate synchronization** — Choose the right primitive for the job -3. **Prefer message passing** — Channels over shared memory when practical -4. **Test deterministically** — Use loom for critical concurrent code -5. **Document synchronization intent** — Make the contract explicit - ---- - -## Choosing Synchronization Primitives - -### Decision Tree - -``` -Is the data accessed from multiple threads? -├── No → Use regular types, no synchronization needed -└── Yes → Is it read-only after initialization? - ├── Yes → Use Arc (no interior mutability needed) - └── No → What's the access pattern? - ├── Mostly reads, rare writes → RwLock - ├── Frequent writes, short critical sections → Mutex - ├── Single atomic value → Atomic* types - └── Producer-consumer pattern → Channels (mpsc, crossbeam) -``` - -### Primitive Comparison - -| Primitive | Use When | Avoid When | -|-----------|----------|------------| -| `Mutex` | Short critical sections, frequent writes | Long-held locks, read-heavy workloads | -| `RwLock` | Read-heavy, occasional writes | Write-heavy (readers starve writers) | -| `Atomic*` | Single values, lock-free algorithms | Complex multi-field updates | -| `Arc` | Shared ownership, immutable data | Mutable data (use `Arc>`) | -| `mpsc::channel` | Producer-consumer, message passing | Low-latency requirements | -| `crossbeam::channel` | High-performance channels | Simple use cases (more dependencies) | - ---- - -## Common Patterns - -### Pattern 1: Shared State with Mutex - -```rust -use std::sync::{Arc, Mutex}; -use std::thread; - -// ✅ CORRECT: Minimal time holding lock -fn update_counter(counter: Arc>, increment: u64) { - let mut guard = counter.lock().unwrap(); - *guard += increment; - // Lock released here when guard goes out of scope -} - -// ❌ DANGEROUS: Holding lock during I/O or long operations -fn bad_update(counter: Arc>) { - let guard = counter.lock().unwrap(); - expensive_network_call(); // Other threads blocked! - drop(guard); -} - -// ✅ BETTER: Clone data, release lock, then do expensive work -fn good_update(counter: Arc>) -> u64 { - let value = { - let guard = counter.lock().unwrap(); - *guard // Copy the value - }; // Lock released here - - expensive_network_call(); // Other threads can proceed - value -} -``` - -### Pattern 2: RwLock for Read-Heavy Workloads - -```rust -use std::sync::{Arc, RwLock}; - -struct Cache { - data: Arc>>, -} - -impl Cache { - // Multiple threads can read simultaneously - fn get(&self, key: &str) -> Option { - let guard = self.data.read().unwrap(); - guard.get(key).cloned() - } - - // ⚠️ CRITICAL: Drop read lock before acquiring write lock! - fn get_or_insert(&self, key: String, default: String) -> String { - // First, try to read - { - let guard = self.data.read().unwrap(); - if let Some(value) = guard.get(&key) { - return value.clone(); - } - } // Read lock MUST be dropped here - - // Now acquire write lock - let mut guard = self.data.write().unwrap(); - // Double-check (another thread might have inserted) - guard.entry(key).or_insert(default).clone() - } -} -``` - -### Pattern 3: Atomic State Machines - -```rust -use std::sync::atomic::{AtomicU8, Ordering}; - -#[repr(u8)] -enum ConnectionState { - Disconnected = 0, - Connecting = 1, - Connected = 2, - Disconnecting = 3, -} - -struct Connection { - state: AtomicU8, -} - -impl Connection { - // ✅ Atomic state transitions - fn try_connect(&self) -> bool { - // Only transition Disconnected -> Connecting - self.state.compare_exchange( - ConnectionState::Disconnected as u8, - ConnectionState::Connecting as u8, - Ordering::AcqRel, - Ordering::Acquire, - ).is_ok() - } - - fn complete_connection(&self) { - // Connecting -> Connected - let _ = self.state.compare_exchange( - ConnectionState::Connecting as u8, - ConnectionState::Connected as u8, - Ordering::AcqRel, - Ordering::Acquire, - ); - } -} -``` - -### Pattern 4: Message Passing with Channels - -```rust -use std::sync::mpsc; -use std::thread; - -enum Command { - Process(Data), - Shutdown, -} - -fn worker_thread(rx: mpsc::Receiver) { - loop { - match rx.recv() { - Ok(Command::Process(data)) => { - process(data); - } - Ok(Command::Shutdown) | Err(_) => { - break; - } - } - } -} - -// ✅ Clean producer-consumer separation -fn run_workers(data: Vec) { - let (tx, rx) = mpsc::channel(); - - let handle = thread::spawn(move || worker_thread(rx)); - - for item in data { - tx.send(Command::Process(item)).unwrap(); - } - tx.send(Command::Shutdown).unwrap(); - - handle.join().unwrap(); -} -``` - ---- - -## Memory Ordering Guide - -### Quick Reference - -| Ordering | Use Case | Guarantees | -|----------|----------|------------| -| `Relaxed` | Counters where order doesn't matter | No synchronization, just atomicity | -| `Acquire` | Load that must see prior Release stores | Prevents reordering of later reads/writes before this | -| `Release` | Store that must be visible to Acquire loads | Prevents reordering of earlier reads/writes after this | -| `AcqRel` | Read-modify-write (fetch_add, compare_exchange) | Both Acquire and Release | -| `SeqCst` | When in doubt, or need total ordering | Strongest guarantees, highest cost | - -### Common Patterns - -```rust -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - -// Pattern: Flag to signal completion -static DONE: AtomicBool = AtomicBool::new(false); -static DATA: AtomicUsize = AtomicUsize::new(0); - -fn producer() { - DATA.store(42, Ordering::Relaxed); // Data write - DONE.store(true, Ordering::Release); // Signal: all prior writes visible -} - -fn consumer() { - while !DONE.load(Ordering::Acquire) {} // Wait for signal - let data = DATA.load(Ordering::Relaxed); // Safe: Acquire synced with Release - assert_eq!(data, 42); -} -``` - -### When to Use SeqCst - -```rust -// ✅ Use SeqCst when multiple atomics must be globally ordered -// Example: Implementing a spinlock with fairness - -// ❌ DON'T use SeqCst just because you're unsure -// It's the slowest ordering and often unnecessary - -// ✅ DO use SeqCst for: -// - Lock-free algorithms requiring total order -// - When debugging and correctness > performance -// - Fences that need to order SeqCst operations -``` - ---- - -## Deadlock Prevention - -### Rule 1: Consistent Lock Ordering - -```rust -// ❌ DEADLOCK RISK: Different threads acquire in different order -fn thread_a(lock1: &Mutex<()>, lock2: &Mutex<()>) { - let _g1 = lock1.lock(); - let _g2 = lock2.lock(); // Waits for thread_b -} - -fn thread_b(lock1: &Mutex<()>, lock2: &Mutex<()>) { - let _g2 = lock2.lock(); - let _g1 = lock1.lock(); // Waits for thread_a → DEADLOCK -} - -// ✅ SAFE: Always acquire in same order (by address, ID, etc.) -fn safe_acquire(lock1: &Mutex<()>, lock2: &Mutex<()>) { - let (first, second) = if (lock1 as *const _) < (lock2 as *const _) { - (lock1, lock2) - } else { - (lock2, lock1) - }; - let _g1 = first.lock(); - let _g2 = second.lock(); -} -``` - -### Rule 2: Don't Hold Locks Across Await Points - -```rust -// ❌ DANGEROUS in async code: Lock held across await -async fn bad_async(data: Arc>) { - let guard = data.lock().unwrap(); - some_async_operation().await; // Lock held during await! - println!("{}", *guard); -} - -// ✅ SAFE: Clone data, drop lock, then await -async fn good_async(data: Arc>) { - let value = { - let guard = data.lock().unwrap(); - guard.clone() - }; - some_async_operation().await; - println!("{}", value); -} -``` - -### Rule 3: Use try_lock for Timeout-Based Deadlock Prevention - -```rust -use std::time::Duration; -use parking_lot::Mutex; - -fn try_acquire_with_timeout(lock: &Mutex<()>) -> Option> { - lock.try_lock_for(Duration::from_millis(100)) -} -``` - ---- - -## Testing Concurrent Code - -### Strategy 1: Unit Test with Loom - -```rust -#[test] -#[cfg(loom)] -fn test_concurrent_counter() { - loom::model(|| { - use loom::sync::Arc; - use loom::sync::atomic::{AtomicUsize, Ordering}; - use loom::thread; - - let counter = Arc::new(AtomicUsize::new(0)); - let c1 = counter.clone(); - let c2 = counter.clone(); - - let t1 = thread::spawn(move || c1.fetch_add(1, Ordering::SeqCst)); - let t2 = thread::spawn(move || c2.fetch_add(1, Ordering::SeqCst)); - - t1.join().unwrap(); - t2.join().unwrap(); - - assert_eq!(counter.load(Ordering::SeqCst), 2); - }); -} -``` - -### Strategy 2: Stress Testing (When Loom Doesn't Scale) - -```rust -#[test] -fn stress_test_concurrent_queue() { - use std::sync::Arc; - use std::thread; - - for _ in 0..100 { // Run many times to catch races - let queue = Arc::new(ConcurrentQueue::new()); - let mut handles = vec![]; - - for i in 0..8 { - let q = queue.clone(); - handles.push(thread::spawn(move || { - for j in 0..1000 { - q.push(i * 1000 + j); - } - })); - } - - for h in handles { - h.join().unwrap(); - } - - assert_eq!(queue.len(), 8 * 1000); - } -} -``` - -### Strategy 3: ThreadSanitizer - -```bash -# Compile with ThreadSanitizer (requires nightly) -RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test - -# Or use Miri for memory safety -cargo +nightly miri test -``` - ---- - -## Common Pitfalls - -### Pitfall 1: Forgetting Arc for Shared Ownership - -```rust -// ❌ COMPILE ERROR: Cannot move into multiple closures -fn broken(data: Mutex>) { - thread::spawn(move || { data.lock(); }); - thread::spawn(move || { data.lock(); }); // Error: data already moved -} - -// ✅ CORRECT: Use Arc for shared ownership -fn working(data: Arc>>) { - let d1 = data.clone(); - let d2 = data.clone(); - thread::spawn(move || { d1.lock(); }); - thread::spawn(move || { d2.lock(); }); -} -``` - -### Pitfall 2: Poisoned Mutexes - -```rust -use std::sync::Mutex; - -// std::sync::Mutex becomes "poisoned" if a thread panics while holding it -fn handle_poisoned_mutex(mutex: &Mutex) -> i32 { - match mutex.lock() { - Ok(guard) => *guard, - Err(poisoned) => { - // Recover: get the data anyway (may be inconsistent!) - *poisoned.into_inner() - } - } -} - -// ✅ BETTER: Use parking_lot::Mutex which doesn't poison -use parking_lot::Mutex as ParkingLotMutex; - -fn no_poison_worry(mutex: &ParkingLotMutex) -> i32 { - *mutex.lock() // Never returns Err -} -``` - -### Pitfall 3: Send + Sync Confusion - -```rust -// Send: Safe to transfer ownership to another thread -// Sync: Safe to share references between threads - -// Rc is neither Send nor Sync -use std::rc::Rc; -// let rc = Rc::new(42); -// thread::spawn(move || { rc; }); // ERROR: Rc is not Send - -// Arc is Send + Sync (if T is Send + Sync) -use std::sync::Arc; -let arc = Arc::new(42); -thread::spawn(move || { arc; }); // OK - -// RefCell is Send but NOT Sync -use std::cell::RefCell; -// Cannot share &RefCell between threads - -// Mutex makes T Sync (if T is Send) -use std::sync::Mutex; -let mutex = Arc::new(Mutex::new(RefCell::new(42))); -// Now can share between threads via Arc>> -``` - -### Pitfall 4: Busy-Waiting Without Yield - -```rust -use std::sync::atomic::{AtomicBool, Ordering}; -use std::hint; - -// ❌ BAD: Burns CPU, unfair to other threads -fn bad_spin_wait(flag: &AtomicBool) { - while !flag.load(Ordering::Acquire) {} -} - -// ✅ BETTER: Hint to CPU, slightly less wasteful -fn better_spin_wait(flag: &AtomicBool) { - while !flag.load(Ordering::Acquire) { - hint::spin_loop(); - } -} - -// ✅ BEST: Use parking/condition variable for real waiting -use parking_lot::{Mutex, Condvar}; - -struct WaitableFlag { - flag: Mutex, - condvar: Condvar, -} - -impl WaitableFlag { - fn wait(&self) { - let mut guard = self.flag.lock(); - while !*guard { - self.condvar.wait(&mut guard); - } - } - - fn signal(&self) { - *self.flag.lock() = true; - self.condvar.notify_all(); - } -} -``` - ---- - -## Performance Tips - -### Tip 1: Reduce Lock Contention - -```rust -// ❌ Single lock for entire data structure -struct Slow { - data: Mutex>>, -} - -// ✅ Sharded locks reduce contention -struct Fast { - shards: [Mutex>>; 16], -} - -impl Fast { - fn get_shard(&self, key: &str) -> &Mutex>> { - let hash = calculate_hash(key); - &self.shards[hash % 16] - } -} -``` - -### Tip 2: Use parking_lot Instead of std - -```toml -[dependencies] -parking_lot = "0.12" -``` - -`parking_lot` is faster than `std::sync` for most use cases: - -- Smaller `Mutex` (1 byte vs 40 bytes on Linux) -- No poisoning overhead -- Better algorithms for fairness/throughput tradeoff - -### Tip 3: Consider Lock-Free Data Structures - -```toml -[dependencies] -crossbeam = "0.8" -``` - -```rust -use crossbeam::queue::SegQueue; - -// Lock-free MPMC queue -let queue = SegQueue::new(); -queue.push(42); -let item = queue.pop(); -``` - ---- - -## Summary Checklist - -When writing concurrent code: - -- [ ] Minimize shared mutable state -- [ ] Use `Arc>` or `Arc>` for shared state -- [ ] Keep critical sections short — don't hold locks during I/O -- [ ] Drop read locks before acquiring write locks (`RwLock`) -- [ ] Use consistent lock ordering to prevent deadlocks -- [ ] Choose appropriate memory ordering (when in doubt, use `SeqCst`) -- [ ] Test with loom for critical concurrent code -- [ ] Consider `parking_lot` over `std::sync` for performance -- [ ] Document synchronization intent and invariants -- [ ] Use channels for producer-consumer patterns - ---- - -*See also: [loom-testing.md](loom-testing.md) for deterministic concurrency testing, [rust-pitfalls.md](rust-pitfalls.md) for common bugs.* diff --git a/.llm/skills/concurrency.md b/.llm/skills/concurrency.md new file mode 100644 index 00000000..46cee7a5 --- /dev/null +++ b/.llm/skills/concurrency.md @@ -0,0 +1,125 @@ + + +# Concurrency Patterns + +## Decision Tree + +``` +Is data accessed from multiple threads? + No -> No synchronization needed + Yes -> Read-only after init? + Yes -> Arc + No -> What access pattern? + Mostly reads, rare writes -> RwLock + Frequent writes, short sections -> Mutex + Single atomic value -> Atomic* types + Producer-consumer -> Channels +``` + +## Primitive Comparison + +| Primitive | Use When | Avoid When | +|-----------|----------|------------| +| `Mutex` | Short critical sections, frequent writes | Long-held locks, read-heavy | +| `RwLock` | Read-heavy, occasional writes | Write-heavy (reader starvation) | +| `Atomic*` | Single values, lock-free algorithms | Complex multi-field updates | +| `Arc` | Shared ownership, immutable data | Mutable data (use `Arc>`) | +| `mpsc::channel` | Producer-consumer, message passing | Low-latency requirements | +| `crossbeam::channel` | High-performance channels | Simple use cases | + +## Key Patterns + +### Mutex -- Minimize Hold Time +```rust +// Use parking_lot::Mutex -- no poisoning, so .lock() returns the guard directly +let value = { + let guard = counter.lock(); + *guard // copy value +}; // lock released +expensive_operation(); // other threads can proceed +``` + +### RwLock -- Drop Read Before Write +```rust +// Use parking_lot::RwLock -- no poisoning, no .unwrap() needed +let needs_write = { + let guard = self.data.read(); + needs_update(&guard) +}; // read lock MUST drop here +if needs_write { + let mut guard = self.data.write(); + if needs_update(&guard) { update(&mut guard); } // double-check +} +``` + +### Atomic State Machine +```rust +fn try_connect(&self) -> bool { + self.state.compare_exchange( + Disconnected as u8, Connecting as u8, + Ordering::AcqRel, Ordering::Acquire, + ).is_ok() +} +``` + +### Message Passing +```rust +enum Command { Process(Data), Shutdown } +fn worker(rx: mpsc::Receiver) { + loop { + match rx.recv() { + Ok(Command::Process(d)) => process(d), + Ok(Command::Shutdown) | Err(_) => break, + } + } +} +``` + +## Memory Ordering + +| Ordering | Use Case | +|----------|----------| +| `Relaxed` | Counters where order doesn't matter | +| `Acquire` | Load that must see prior Release stores | +| `Release` | Store that must be visible to Acquire loads | +| `AcqRel` | Read-modify-write (fetch_add, compare_exchange) | +| `SeqCst` | Total ordering needed, or when in doubt | + +## Deadlock Prevention + +1. **Consistent lock ordering** -- always acquire in same order (by address or ID) +2. **Don't hold locks across await** -- clone data, release, then await +3. **Use `try_lock`** for timeout-based prevention + +## Testing + +- **Loom:** Deterministic exploration of all interleavings for critical code +- **Stress testing:** Run many iterations to catch races +- **ThreadSanitizer:** `RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test` + +## Performance Tips + +- **Sharded locks** reduce contention (e.g., 16 Mutex shards) +- **`parking_lot`** is faster than `std::sync` (smaller Mutex, no poisoning) +- **Lock-free structures** via `crossbeam` (SegQueue, etc.) + +## Common Pitfalls + +| Pitfall | Fix | +|---------|-----| +| No `Arc` for shared ownership | `Arc::clone()` before `thread::spawn` | +| Poisoned mutexes | Use `parking_lot::Mutex` (no poisoning) | +| `Rc` across threads | `Rc` is not `Send`; use `Arc` | +| Busy-wait without yield | `std::hint::spin_loop()` or condvar | +| `std::sync::Mutex` held across `.await` | Use `tokio::sync::Mutex` | + +## Checklist + +- [ ] Minimize shared mutable state +- [ ] Keep critical sections short -- no I/O under lock +- [ ] Drop read locks before acquiring write locks +- [ ] Consistent lock ordering +- [ ] Appropriate memory ordering (SeqCst if unsure) +- [ ] Test with loom for critical concurrent code +- [ ] Consider `parking_lot` over `std::sync` +- [ ] Document synchronization intent diff --git a/.llm/skills/crate-publishing-guide.md b/.llm/skills/crate-publishing-guide.md deleted file mode 100644 index d472d8a8..00000000 --- a/.llm/skills/crate-publishing-guide.md +++ /dev/null @@ -1,998 +0,0 @@ -# Crate Publishing Guide — Publishing Rust Libraries to crates.io - -> **This document provides comprehensive guidance for publishing high-quality Rust crates.** -> Follow these practices to create maintainable, well-documented, and user-friendly libraries. - -## TL;DR — Quick Reference - -```bash -# Pre-publish validation -cargo fmt --check -cargo clippy --all-targets -- -D warnings -cargo test --all-features -cargo doc --no-deps --all-features -cargo package --list # Review what will be published -cargo publish --dry-run - -# Semantic versioning checks -cargo install cargo-semver-checks -cargo semver-checks check-release - -# Security audit -cargo install cargo-audit -cargo audit - -# Outdated dependencies -cargo install cargo-outdated -cargo outdated -``` - -**Key Principles:** - -1. **Minimal API surface** — Expose only what users need -2. **Complete metadata** — Fill out all Cargo.toml fields -3. **Document everything** — Use `#![warn(missing_docs)]` -4. **Follow semver strictly** — Use cargo-semver-checks -5. **Limit dependencies** — Each one is a liability - ---- - -## Cargo.toml Metadata — Complete Package Configuration - -### Required Fields - -Every published crate MUST have these fields: - -```toml -[package] -name = "my-crate" -version = "0.1.0" -edition = "2021" -rust-version = "1.70" # MSRV - Minimum Supported Rust Version -authors = ["Your Name "] -description = "A clear, concise description of what the crate does." -license = "MIT OR Apache-2.0" -repository = "https://github.com/username/my-crate" -readme = "README.md" -keywords = ["keyword1", "keyword2", "keyword3"] # Max 5 -categories = ["category1", "category2"] # See crates.io/category_slugs -``` - -### Recommended Optional Fields - -```toml -[package] -# Documentation link (defaults to docs.rs) -documentation = "https://docs.rs/my-crate" - -# Homepage (if different from repository) -homepage = "https://my-crate.dev" - -# Exclude unnecessary files from package -exclude = [ - ".github/", - ".devcontainer/", - "scripts/", - "tests/", # Large test suites - "benches/", # Benchmarks - "*.md", # Keep README.md via readme field - "!README.md", - "!CHANGELOG.md", - "!LICENSE*", -] - -# Or use include for explicit allowlist -include = [ - "src/**/*", - "Cargo.toml", - "README.md", - "LICENSE-MIT", - "LICENSE-APACHE", - "CHANGELOG.md", # Include changelog in package -] -``` - -> **Note:** Cargo.toml does not have a `changelog` field. To reference your changelog: -> -> - Include `CHANGELOG.md` in your package (shown above in the `include` list) -> - Link to it from your README.md -> - Add a link in your crate-level documentation: `//! [Changelog](https://github.com/username/my-crate/blob/main/CHANGELOG.md)` -> - docs.rs will automatically render and link to `CHANGELOG.md` if it's included in the package - -### Badge Configuration - -```toml -[badges] -maintenance = { status = "actively-developed" } -# Other options: experimental, passively-maintained, as-is, deprecated, none -``` - -### Dependency Best Practices - -```toml -[dependencies] -# ✅ GOOD - Use stable 1.0+ crates when available -serde = "1.0" - -# ✅ GOOD - Disable unnecessary default features -tokio = { version = "1.0", default-features = false, features = ["rt", "net"] } - -# ✅ GOOD - Re-export dependencies exposed in public API -# This prevents version conflicts for users -bytes = "1.0" # Re-exported as `pub use bytes;` - -# ⚠️ CAUTION - Pre-1.0 crates may have breaking changes -rand = "0.8" # Acceptable if no stable alternative - -# ❌ AVOID - Pinning exact versions (blocks updates) -some-crate = "=1.2.3" # Don't do this - -# ❌ AVOID - Git dependencies in published crates -# git-dep = { git = "https://github.com/..." } # Use crates.io version -``` - -### Feature Flags - -```toml -[features] -default = [] # Prefer minimal defaults - -# Document each feature in README and lib.rs -std = [] # Enable std library support -alloc = [] # Enable alloc without full std -serde = ["dep:serde"] # Optional serde support -async = ["dep:tokio"] # Async runtime support - -# Use dep: syntax to avoid implicit features (Rust 1.60+) -[dependencies] -serde = { version = "1.0", optional = true } -tokio = { version = "1.0", optional = true } -``` - ---- - -## Project Structure — Organizing Crate Code - -### Library Crate Structure - -``` -my-crate/ -├── Cargo.toml -├── README.md -├── LICENSE-MIT -├── LICENSE-APACHE -├── CHANGELOG.md -├── src/ -│ ├── lib.rs # Library entry point -│ ├── error.rs # Error types -│ ├── config.rs # Configuration -│ └── module/ -│ ├── mod.rs # Module with submodules -│ ├── types.rs -│ └── impl.rs -├── examples/ -│ ├── basic.rs -│ └── advanced.rs -├── benches/ -│ └── benchmark.rs -└── tests/ - └── integration/ - └── main.rs # Single integration test crate -``` - -### Module Organization - -```rust -// src/lib.rs - -//! # My Crate -//! -//! A brief description of what this crate does. -//! -//! ## Features -//! -//! - `std` - Enable std library support (default) -//! - `serde` - Enable serde serialization -//! -//! ## Example -//! -//! ```rust -//! use my_crate::Widget; -//! -//! let widget = Widget::new(); -//! widget.do_something(); -//! ``` - -#![warn(missing_docs)] -#![warn(missing_debug_implementations)] -#![warn(rust_2018_idioms)] -#![warn(unreachable_pub)] -#![deny(unsafe_code)] // Or #![forbid(unsafe_code)] if no unsafe needed - -// Re-export main types at crate root for convenience -pub use self::config::Config; -pub use self::error::{Error, Result}; -pub use self::widget::Widget; - -mod config; -mod error; -mod widget; - -// Internal modules use pub(crate) -pub(crate) mod internal; -``` - -### Visibility Guidelines - -```rust -// ✅ GOOD - Minimal public API -pub struct Widget { - // Private field - implementation detail - inner: InnerState, -} - -impl Widget { - /// Creates a new widget with default settings. - pub fn new() -> Self { /* ... */ } - - /// Internal helper - not part of public API - pub(crate) fn internal_method(&self) { /* ... */ } - - // Private method - fn helper(&self) { /* ... */ } -} - -// ✅ GOOD - Re-export for convenience -pub mod prelude { - pub use crate::{Widget, Config, Error, Result}; -} - -// ✅ GOOD - Use #[non_exhaustive] for extensible enums -#[non_exhaustive] -pub enum ErrorKind { - InvalidInput, - Timeout, - // Future variants won't break downstream -} - -// ✅ GOOD - Use newtype pattern to hide implementation -pub struct UserId(u64); // Users can't depend on it being u64 - -impl UserId { - pub fn new(id: u64) -> Self { Self(id) } - pub fn as_u64(&self) -> u64 { self.0 } -} -``` - ---- - -## Documentation — Writing Excellent Docs - -### Enable Documentation Lints - -```rust -// src/lib.rs -#![warn(missing_docs)] -#![warn(rustdoc::missing_crate_level_docs)] -#![warn(rustdoc::broken_intra_doc_links)] -#![warn(rustdoc::private_intra_doc_links)] -``` - -### Documentation Structure - -```rust -/// A widget for processing data. -/// -/// `Widget` provides high-performance data processing with -/// configurable options for various use cases. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```rust -/// use my_crate::Widget; -/// -/// let widget = Widget::new(); -/// let result = widget.process("input data")?; -/// # Ok::<(), my_crate::Error>(()) -/// ``` -/// -/// With custom configuration: -/// -/// ```rust -/// use my_crate::{Widget, Config}; -/// -/// let config = Config::builder() -/// .timeout(Duration::from_secs(30)) -/// .build(); -/// let widget = Widget::with_config(config); -/// ``` -/// -/// # Errors -/// -/// Returns [`Error::InvalidInput`] if the input is empty. -/// Returns [`Error::Timeout`] if processing exceeds the configured timeout. -/// -/// # Panics -/// -/// This function does not panic. (Or document when it does) -/// -/// # Safety -/// -/// (Only for unsafe functions - document all invariants) -pub struct Widget { /* ... */ } -``` - -### README.md Template - -````markdown -# my-crate - -[![Crates.io](https://img.shields.io/crates/v/my-crate.svg)](https://crates.io/crates/my-crate) -[![Documentation](https://docs.rs/my-crate/badge.svg)](https://docs.rs/my-crate) -[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](#license) -[![CI](https://github.com/username/my-crate/workflows/CI/badge.svg)](https://github.com/username/my-crate/actions) - -A brief description of what this crate does. - -## Features - -- Feature 1: Description -- Feature 2: Description -- Feature 3: Description - -## Installation - -Add to your `Cargo.toml`: - -```toml -[dependencies] -my-crate = "0.1" -``` - -## Quick Start - -```rust -use my_crate::Widget; - -fn main() -> Result<(), my_crate::Error> { - let widget = Widget::new(); - widget.do_something()?; - Ok(()) -} -``` - -## Cargo Features - -| Feature | Default | Description | -|---------|---------|-------------| -| `std` | Yes | Enable std library support | -| `serde` | No | Enable serde serialization | -| `async` | No | Enable async runtime support | - -## Minimum Supported Rust Version (MSRV) - -This crate requires Rust 1.70 or later. - -## License - -Licensed under either of: - -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) - -at your option. - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -```` - ---- - -## API Design — Building User-Friendly Interfaces - -### Minimize Public API Surface - -```rust -// ❌ AVOID - Exposing implementation details -pub struct Parser { - pub buffer: Vec, // Users shouldn't access this - pub state: ParserState, // Implementation detail - pub position: usize, // Internal bookkeeping -} - -// ✅ GOOD - Hide implementation, expose behavior -pub struct Parser { - buffer: Vec, - state: ParserState, - position: usize, -} - -impl Parser { - pub fn new() -> Self { /* ... */ } - pub fn parse(&mut self, input: &[u8]) -> Result { /* ... */ } - pub fn is_complete(&self) -> bool { /* ... */ } -} -``` - -### Use Builder Pattern for Complex Configuration - -```rust -/// Configuration for the widget. -#[derive(Debug, Clone)] -pub struct Config { - timeout: Duration, - max_retries: u32, - buffer_size: usize, -} - -impl Config { - /// Creates a new configuration builder. - pub fn builder() -> ConfigBuilder { - ConfigBuilder::default() - } -} - -/// Builder for [`Config`]. -#[derive(Debug, Default)] -pub struct ConfigBuilder { - timeout: Option, - max_retries: Option, - buffer_size: Option, -} - -impl ConfigBuilder { - /// Sets the operation timeout. - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = Some(timeout); - self - } - - /// Sets the maximum retry count. - pub fn max_retries(mut self, retries: u32) -> Self { - self.max_retries = Some(retries); - self - } - - /// Builds the configuration. - pub fn build(self) -> Config { - Config { - timeout: self.timeout.unwrap_or(Duration::from_secs(30)), - max_retries: self.max_retries.unwrap_or(3), - buffer_size: self.buffer_size.unwrap_or(4096), - } - } -} -``` - -### Re-export Dependencies in Public API - -```rust -// If your public API exposes types from dependencies, -// re-export them so users don't need to add the dependency - -// src/lib.rs -pub use bytes::Bytes; // Users can use my_crate::Bytes - -// This prevents version conflicts: -// - User adds bytes = "1.5" -// - Your crate uses bytes = "1.4" -// - Without re-export: conflict! -// - With re-export: user uses your version -``` - -### Design for Extension with `#[non_exhaustive]` - -```rust -/// Error type for widget operations. -#[derive(Debug)] -#[non_exhaustive] // Allows adding variants without breaking change -pub enum Error { - /// Invalid input was provided. - InvalidInput { message: String }, - - /// Operation timed out. - Timeout { elapsed: Duration }, - - /// Network error occurred. - Network(std::io::Error), -} - -/// Configuration options. -#[derive(Debug, Clone)] -#[non_exhaustive] // Allows adding fields without breaking change -pub struct Options { - pub timeout: Duration, - pub retries: u32, - // Future fields won't break users -} - -impl Default for Options { - fn default() -> Self { - Self { - timeout: Duration::from_secs(30), - retries: 3, - } - } -} -``` - ---- - -## Versioning — Semantic Versioning for Rust - -### Semver Rules - -| Change Type | Version Bump | Examples | -|-------------|--------------|----------| -| Breaking API change | Major (1.0.0 → 2.0.0) | Remove public item, change function signature | -| New feature, backward compatible | Minor (1.0.0 → 1.1.0) | Add new public function, add optional feature | -| Bug fix, no API change | Patch (1.0.0 → 1.0.1) | Fix bug, improve performance | - -### Pre-1.0 Versioning - -For crates before 1.0.0: - -- **0.x.y** — Minor version bumps (0.1.0 → 0.2.0) can be breaking -- **0.x.y** — Patch version bumps (0.1.0 → 0.1.1) should be non-breaking - -### Use cargo-semver-checks - -```bash -# Install -cargo install cargo-semver-checks - -# Check for breaking changes before release -cargo semver-checks check-release - -# Compare against specific version -cargo semver-checks check-release --baseline-version 1.0.0 -``` - -### CHANGELOG Best Practices - -```markdown -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/), -and this project adheres to [Semantic Versioning](https://semver.org/). - -## [Unreleased] - -### Added -- New `Widget::with_config` constructor - -### Changed -- Improved error messages for `ParseError` - -### Deprecated -- `Widget::new_with_options` - use `Widget::with_config` instead - -### Removed -- (Nothing) - -### Fixed -- Fixed panic when input is empty - -### Security -- Updated `vulnerable-dep` to fix CVE-XXXX-YYYY - -## [1.0.0] - 2024-01-15 - -### Added -- Initial stable release -``` - ---- - -## Testing — Comprehensive Test Coverage - -### Doctest Best Practices - -```rust -/// Parses the input string. -/// -/// # Examples -/// -/// ```rust -/// use my_crate::parse; -/// -/// let result = parse("hello")?; -/// assert_eq!(result.len(), 5); -/// # Ok::<(), my_crate::Error>(()) -/// ``` -/// -/// Error handling: -/// -/// ```rust -/// use my_crate::parse; -/// -/// let result = parse(""); -/// assert!(result.is_err()); -/// ``` -/// -/// This example should compile but not run: -/// -/// ```rust,no_run -/// # use my_crate::connect; -/// let conn = connect("server:1234")?; -/// # Ok::<(), my_crate::Error>(()) -/// ``` -/// -/// This example should fail to compile: -/// -/// ```rust,compile_fail -/// use my_crate::PrivateType; // Error: not accessible -/// ``` -pub fn parse(input: &str) -> Result { /* ... */ } -``` - -### Feature-Gated Tests - -```rust -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn basic_test() { - // Always runs - } - - #[test] - #[cfg(feature = "serde")] - fn serde_roundtrip() { - // Only runs with --features serde - } - - #[test] - #[cfg(feature = "std")] - fn std_io_test() { - // Only runs with std feature - } -} -``` - ---- - -## CI/CD — Automated Quality Checks - -### GitHub Actions Workflow - -```yaml -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - - name: Check formatting - run: cargo fmt --check - - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - - name: Run tests - run: cargo test --all-features - - - name: Run doc tests - run: cargo test --doc --all-features - - - name: Build docs - run: cargo doc --no-deps --all-features - env: - RUSTDOCFLAGS: -D warnings - - msrv: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install MSRV Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.70" # Match rust-version in Cargo.toml - - - name: Check MSRV - run: cargo check --all-features - - semver: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Check semver - uses: obi1kenobi/cargo-semver-checks-action@v2 - - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Security audit - uses: rustsec/audit-check@v2 - with: - token: ${{ secrets.GITHUB_TOKEN }} -``` - -### Pre-publish Checklist Workflow - -```yaml -name: Publish - -on: - push: - tags: - - 'v*' - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Verify version matches tag - run: | - CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version') - TAG_VERSION=${GITHUB_REF#refs/tags/v} - if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then - echo "Version mismatch: Cargo.toml=$CARGO_VERSION, tag=$TAG_VERSION" - exit 1 - fi - - - name: Dry run - run: cargo publish --dry-run - - - name: Publish to crates.io - run: cargo publish - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} -``` - ---- - -## Dependencies — Managing External Code - -### Dependency Evaluation Checklist - -Before adding a dependency, evaluate: - -- [ ] **Necessity** — Can you implement this yourself in <100 lines? -- [ ] **Popularity** — Downloads, stars, dependents on crates.io -- [ ] **Maintenance** — Recent commits, responsive maintainers -- [ ] **License** — Compatible with your license (MIT/Apache-2.0) -- [ ] **Security** — Check RustSec advisories, run `cargo audit` -- [ ] **Stability** — Is it 1.0+? How often do they break semver? -- [ ] **Dependencies** — Does it pull in a huge transitive tree? -- [ ] **Features** — Can you disable features you don't need? - -### Dependency Maintenance - -```bash -# Check for outdated dependencies -cargo install cargo-outdated -cargo outdated - -# Check for security vulnerabilities -cargo install cargo-audit -cargo audit - -# Check for unmaintained/yanked crates -cargo install cargo-deny -cargo deny check - -# Update dependencies -cargo update - -# Update specific dependency -cargo update -p dependency-name -``` - -### Automated Dependency Updates - -Set up Dependabot or Renovate: - -```yaml -# .github/dependabot.yml -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - groups: - minor-and-patch: - update-types: - - "minor" - - "patch" -``` - ---- - -## Security — Safe Publishing Practices - -### Security Checklist - -- [ ] Run `cargo audit` before every release -- [ ] Enable Dependabot/Renovate for automatic updates -- [ ] Review all dependencies for security advisories -- [ ] Use `#![forbid(unsafe_code)]` if possible -- [ ] Document all `unsafe` code with `// SAFETY:` comments -- [ ] Run Miri in CI for unsafe code: `cargo +nightly miri test` -- [ ] Consider fuzzing with cargo-fuzz for parsing/input handling - -### Unsafe Code Documentation - -```rust -// If you must use unsafe, document thoroughly: - -/// # Safety -/// -/// The caller must ensure: -/// - `ptr` is valid for reads of `len` bytes -/// - `ptr` is properly aligned for `T` -/// - The memory is initialized -pub unsafe fn read_bytes(ptr: *const T, len: usize) -> &[u8] { - // SAFETY: Caller guarantees ptr validity, alignment, and initialization. - // We only read `len` bytes which caller has verified is within bounds. - std::slice::from_raw_parts(ptr as *const u8, len) -} -``` - ---- - -## Publishing Checklist - -### Before First Publish - -- [ ] Choose a unique, descriptive crate name -- [ ] Create crates.io account and get API token -- [ ] Fill out all Cargo.toml metadata -- [ ] Write comprehensive README.md -- [ ] Add LICENSE-MIT and LICENSE-APACHE files -- [ ] Set up CI/CD pipeline -- [ ] Run `cargo publish --dry-run` -- [ ] Review package contents with `cargo package --list` - -### Before Every Release - -```bash -# 1. Update version in Cargo.toml -# 2. Update CHANGELOG.md - -# 3. Run full validation -cargo fmt --check -cargo clippy --all-targets --all-features -- -D warnings -cargo test --all-features -cargo test --doc --all-features -cargo doc --no-deps --all-features - -# 4. Check for breaking changes -cargo semver-checks check-release - -# 5. Security audit -cargo audit - -# 6. Verify package contents -cargo package --list -cargo publish --dry-run - -# 7. Create git tag -git tag -a v1.0.0 -m "Release v1.0.0" -git push origin v1.0.0 - -# 8. Publish -cargo publish -``` - -### After Publishing - -- [ ] Verify crate appears on crates.io -- [ ] Verify docs render correctly on docs.rs -- [ ] Test installation: `cargo add your-crate` -- [ ] Announce release (GitHub release, social media, etc.) - ---- - -## Common Mistakes to Avoid - -### ❌ Exposing Too Much API - -```rust -// ❌ BAD - Exposes internal module structure -pub mod internal; -pub mod helpers; -pub mod utils; - -// ✅ GOOD - Flat, minimal API -pub use crate::widget::Widget; -pub use crate::error::Error; -``` - -### ❌ Forgetting `#[non_exhaustive]` - -```rust -// ❌ BAD - Adding variants is breaking change -pub enum Error { - Io(std::io::Error), - Parse(String), -} - -// ✅ GOOD - Future-proof -#[non_exhaustive] -pub enum Error { - Io(std::io::Error), - Parse(String), -} -``` - -### ❌ Not Re-exporting Dependency Types - -```rust -// ❌ BAD - Users must add compatible bytes version -pub fn get_data(&self) -> bytes::Bytes { /* ... */ } - -// ✅ GOOD - Re-export dependency -pub use bytes::Bytes; -pub fn get_data(&self) -> Bytes { /* ... */ } -``` - -### ❌ Pinning Exact Dependency Versions - -```toml -# ❌ BAD - Blocks users from updating -serde = "=1.0.150" - -# ✅ GOOD - Allows compatible updates -serde = "1.0" -``` - -### ❌ Publishing Without Dry Run - -```bash -# ❌ BAD - Might include unwanted files -cargo publish - -# ✅ GOOD - Always verify first -cargo package --list -cargo publish --dry-run -cargo publish -``` - ---- - -## Related Resources - -- [The Cargo Book — Publishing](https://doc.rust-lang.org/cargo/reference/publishing.html) -- [API Guidelines](https://rust-lang.github.io/api-guidelines/) -- [Semver Specification](https://semver.org/) -- [Keep a Changelog](https://keepachangelog.com/) -- [RustSec Advisory Database](https://rustsec.org/) -- [cargo-semver-checks](https://github.com/obi1kenobi/cargo-semver-checks) diff --git a/.llm/skills/cross-platform-ci-cd.md b/.llm/skills/cross-platform-ci-cd.md deleted file mode 100644 index 425e1962..00000000 --- a/.llm/skills/cross-platform-ci-cd.md +++ /dev/null @@ -1,835 +0,0 @@ -# Cross-Platform CI/CD for Rust Projects - -> **A guide to setting up continuous integration and deployment for Rust projects targeting multiple platforms.** - -## Overview - -Cross-platform CI/CD for Rust projects requires careful orchestration of builds across different operating systems, architectures, and target environments. This guide covers GitHub Actions patterns, but the concepts apply to other CI systems. - ---- - -## Core Principle: Test Everything Cross-Platform - -**All builds and tests should run on a cross-platform matrix whenever possible.** Platform-specific bugs (memory layout differences, threading behavior, endianness, OS-specific syscalls) can cause production failures that are invisible when testing on only one platform. - -### What Should Run Cross-Platform - -| Category | Cross-Platform Priority | Rationale | -|----------|------------------------|-----------| -| **Unit tests** | Required | Catch platform-specific logic bugs | -| **Integration tests** | Required | OS-specific behavior differences | -| **Loom concurrency tests** | Required | Threading/scheduler behavior varies | -| **Miri UB checks** | Required | Memory layout, alignment differ by platform | -| **Clippy/fmt** | One platform OK | Code is platform-agnostic | -| **Coverage** | One platform OK | Measures same code paths | -| **Security scanning** | One platform OK | Dependency analysis is platform-agnostic | -| **Formal verification (Kani)** | One platform OK | Proofs are platform-agnostic (Linux-only tool) | - -### Standard Cross-Platform Matrix - -```yaml -strategy: - fail-fast: false # Run all platforms even if one fails - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] -``` - -### Why `fail-fast: false`? - -Setting `fail-fast: false` ensures all platforms run to completion. This is critical because: - -1. A Linux-only failure might mask a different Windows-only failure -2. Developers can fix multiple platform issues in one PR cycle -3. Provides complete visibility into cross-platform health - ---- - -## Target Platform Matrix - -### Common Rust Targets - -| Target Triple | Platform | Tier | CI Runner | -|---------------|----------|------|-----------| -| `x86_64-unknown-linux-gnu` | Linux x64 (glibc) | 1 | `ubuntu-latest` | -| `x86_64-unknown-linux-musl` | Linux x64 (static) | 2 | `ubuntu-latest` + cross | -| `aarch64-unknown-linux-gnu` | Linux ARM64 | 2 | `ubuntu-latest` + cross | -| `x86_64-apple-darwin` | macOS Intel | 1 | `macos-latest` | -| `aarch64-apple-darwin` | macOS Apple Silicon | 1 | `macos-latest` | -| `x86_64-pc-windows-msvc` | Windows x64 | 1 | `windows-latest` | -| `x86_64-pc-windows-gnu` | Windows (MinGW) | 1 | `ubuntu-latest` + cross | -| `wasm32-unknown-unknown` | WebAssembly | 2 | `ubuntu-latest` | -| `aarch64-apple-ios` | iOS Device | 2 | `macos-latest` | -| `aarch64-linux-android` | Android ARM64 | 2 | `ubuntu-latest` + cross | - -### Platform Tiers - -- **Tier 1**: Guaranteed to work, full test suite runs -- **Tier 2**: Builds guaranteed, limited testing -- **Tier 3**: Best-effort support - ---- - -## Basic Multi-Platform Workflow - -```yaml -# .github/workflows/ci.yml -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - -jobs: - # Fast checks on Linux first - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - - name: Format check - run: cargo fmt --all -- --check - - - name: Clippy - run: cargo clippy --all-targets -- -D warnings - - - name: Test - run: cargo test - - # Multi-platform build matrix - build: - needs: check # Don't waste runner time if checks fail - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: macos-latest - target: x86_64-apple-darwin - - os: macos-latest - target: aarch64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Build - run: cargo build --release --target ${{ matrix.target }} - - - name: Test - run: cargo test --target ${{ matrix.target }} -``` - ---- - -## Cross-Compilation Patterns - -### Using cross-rs for Linux Targets - -```yaml -jobs: - cross-compile: - runs-on: ubuntu-latest - strategy: - matrix: - target: - - aarch64-unknown-linux-gnu - - armv7-unknown-linux-gnueabihf - - x86_64-unknown-linux-musl - - steps: - - uses: actions/checkout@v4 - - - name: Install cross - run: | - curl -L --proto '=https' --tlsv1.2 -sSf \ - https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash - cargo binstall cross --no-confirm - - - name: Build with cross - run: cross build --release --target ${{ matrix.target }} - - - name: Test with cross (QEMU) - run: cross test --target ${{ matrix.target }} -``` - -### CRITICAL: cross-rs Image Tag Stability - -**Never use unstable image tags like `:main` or `:edge` in CI.** These tags change without notice and can break builds unexpectedly. - -```yaml -# ❌ DANGEROUS: Unstable image tag — WILL break randomly -[target.aarch64-unknown-linux-gnu] -image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main" - -# ❌ DANGEROUS: Custom image with unstable base -[target.aarch64-unknown-linux-gnu.dockerfile] -file = "Dockerfile.cross" -# Dockerfile: FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:main # BAD! -``` - -**Recommended approach: Use environment variable passthrough instead of custom images.** - -The default cross-rs images are well-tested and stable. Instead of customizing images for different toolchain settings, use environment variable passthrough to control build behavior: - -```toml -# Cross.toml — Recommended pattern -[target.aarch64-unknown-linux-gnu] -# No custom image! Use environment variables to control behavior. - -[build.env] -passthrough = [ - "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER", - "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS", -] -``` - -Then in CI, set environment variables to override settings: - -```yaml -# .github/workflows/ci.yml -- name: Cross-compile for ARM64 - env: - # Override linker to use GCC instead of clang+lld (more stable in cross-rs) - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - run: cross build --release --target aarch64-unknown-linux-gnu -``` - -**Why this is better:** - -| Approach | Pros | Cons | -|----------|------|------| -| Custom images with `:main` | Full control | Breaks when upstream changes | -| Pinned image versions | Stable | Need to track updates manually | -| **Environment passthrough** | Stable + flexible | Slightly more verbose in CI | - -**When you MUST use custom images:** - -If you genuinely need packages not in the default image: - -1. **Pin to a specific version tag** (e.g., `:0.2.5`), not `:main` -2. **Document why** the custom image is needed -3. **Set up Dependabot** or similar to track upstream updates - -### Using cargo-zigbuild for glibc Targeting - -```yaml -jobs: - zigbuild: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-unknown-linux-gnu - - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - - - name: Install cargo-zigbuild - run: cargo install --locked cargo-zigbuild - - - name: Build targeting glibc 2.17 - run: cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17 -``` - ---- - -## WASM Build and Test - -```yaml -jobs: - wasm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Install wasm-pack - run: cargo install wasm-pack - - - name: Build WASM - run: cargo build --target wasm32-unknown-unknown --release - - - name: Build with wasm-pack (for web) - run: wasm-pack build --target web --release - - - name: Test in headless browser - run: wasm-pack test --headless --chrome -``` - -### WASM with wasm-bindgen-test - -```yaml -jobs: - wasm-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Install wasm-pack - run: cargo install wasm-pack - - - name: Setup Chrome - uses: browser-actions/setup-chrome@latest - - - name: Run WASM tests - run: | - RUSTFLAGS='--cfg getrandom_backend="wasm_js"' \ - wasm-pack test --headless --chrome -``` - ---- - -## Mobile Build Workflows - -### iOS Build (macOS Runner Required) - -```yaml -jobs: - ios: - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-apple-ios, aarch64-apple-ios-sim - - - name: Build for iOS device - run: cargo build --target aarch64-apple-ios --release - - - name: Build for iOS simulator - run: cargo build --target aarch64-apple-ios-sim --release -``` - -### Android Build with cargo-ndk - -```yaml -jobs: - android: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: aarch64-linux-android, armv7-linux-androideabi - - - name: Setup Android NDK - uses: nttld/setup-ndk@v1 - with: - ndk-version: r25c - - - name: Install cargo-ndk - run: cargo install cargo-ndk - - - name: Build for Android - run: | - cargo ndk -t arm64-v8a -t armeabi-v7a \ - -o ./jniLibs build --release - - - name: Upload Android libraries - uses: actions/upload-artifact@v4 - with: - name: android-libs - path: jniLibs/ -``` - ---- - -## Release Workflow - -### Automated Release on Tag - -```yaml -# .github/workflows/release.yml -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - build-release: - strategy: - matrix: - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - artifact: my-app-linux-x64 - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - artifact: my-app-linux-x64-static - use_cross: true - - os: macos-latest - target: x86_64-apple-darwin - artifact: my-app-macos-intel - - os: macos-latest - target: aarch64-apple-darwin - artifact: my-app-macos-arm64 - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact: my-app-windows-x64 - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross - if: matrix.use_cross - run: cargo install cross --git https://github.com/cross-rs/cross - - - name: Build (native) - if: ${{ !matrix.use_cross }} - run: cargo build --release --target ${{ matrix.target }} - - - name: Build (cross) - if: matrix.use_cross - run: cross build --release --target ${{ matrix.target }} - - - name: Package (Unix) - if: matrix.os != 'windows-latest' - run: | - cd target/${{ matrix.target }}/release - tar czf ../../../${{ matrix.artifact }}.tar.gz my-app - - - name: Package (Windows) - if: matrix.os == 'windows-latest' - run: | - cd target/${{ matrix.target }}/release - 7z a ../../../${{ matrix.artifact }}.zip my-app.exe - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact }} - path: ${{ matrix.artifact }}.* - - create-release: - needs: build-release - runs-on: ubuntu-latest - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create release - uses: softprops/action-gh-release@v1 - with: - files: artifacts/**/* - generate_release_notes: true -``` - ---- - -## Caching Strategies - -### Basic Cargo Caching - -```yaml -steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- -``` - -### Separate Caches for Better Hit Rates - -```yaml -steps: - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry - key: cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: ~/.cargo/git - key: cargo-git-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache target directory - uses: actions/cache@v4 - with: - path: target - key: target-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - target-${{ runner.os }}-${{ matrix.target }}- -``` - -### Using sccache for Distributed Caching - -```yaml -env: - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" - -steps: - - uses: actions/checkout@v4 - - - name: Setup sccache - uses: mozilla-actions/sccache-action@v0.0.4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Build - run: cargo build --release -``` - ---- - -## Cost Optimization - -### Strategy: Cross-compile on Cheap Runners - -macOS and Windows runners cost 10x and 2x more than Linux. Minimize their usage: - -```yaml -jobs: - # All cross-compilation on cheap Linux runners - linux-builds: - runs-on: ubuntu-latest - strategy: - matrix: - target: - - x86_64-unknown-linux-gnu - - x86_64-unknown-linux-musl - - aarch64-unknown-linux-gnu - - x86_64-pc-windows-gnu # MinGW cross-compile - steps: - - uses: actions/checkout@v4 - - run: cargo install cross --git https://github.com/cross-rs/cross - - run: cross build --release --target ${{ matrix.target }} - - # Native macOS only for final verification - macos-verify: - runs-on: macos-latest - needs: linux-builds - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - steps: - - uses: actions/checkout@v4 - - run: cargo test - - # Native Windows only for MSVC builds - windows-msvc: - runs-on: windows-latest - needs: linux-builds - steps: - - uses: actions/checkout@v4 - - run: cargo build --release -``` - -### Run Expensive Jobs Only on Main/Tags - -```yaml -jobs: - full-matrix: - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - run: cargo test - - quick-check: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: cargo fmt --check && cargo clippy && cargo test -``` - ---- - -## Dependency and Security Scanning - -### cargo-deny for License and Security - -```yaml -jobs: - deny: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 -``` - -### cargo-audit for Vulnerabilities - -```yaml -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: rustsec/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} -``` - ---- - -## Platform-Specific Dependencies - -### Linux Build Dependencies - -```yaml -steps: - - name: Install Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libasound2-dev \ - libudev-dev \ - libwayland-dev \ - libxkbcommon-dev \ - pkg-config -``` - -### macOS Code Signing - -```yaml -steps: - - name: Import signing certificate - if: matrix.os == 'macos-latest' - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - run: | - echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 - security create-keychain -p temp build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p temp build.keychain - security import certificate.p12 -k build.keychain \ - -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple: \ - -s -k temp build.keychain - - - name: Sign binary - if: matrix.os == 'macos-latest' - run: codesign --force --sign "$SIGNING_IDENTITY" target/release/my-app -``` - ---- - -## Documentation and Coverage - -### Generate and Deploy Docs - -```yaml -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly - - - name: Build docs - run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps - - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - if: github.ref == 'refs/heads/main' - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc -``` - -### Code Coverage with cargo-llvm-cov - -```yaml -jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: llvm-tools-preview - - - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov - - - name: Generate coverage - run: cargo llvm-cov --all-features --lcov --output-path lcov.info - - - name: Upload to Codecov - uses: codecov/codecov-action@v3 - with: - files: lcov.info -``` - ---- - -## Complete Example Workflow - -```yaml -# .github/workflows/complete-ci.yml -name: Complete CI - -on: - push: - branches: [main] - tags: ['v*'] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - -jobs: - # Stage 1: Quick checks (always run) - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - run: cargo fmt --check - - run: cargo clippy --all-targets -- -D warnings - - run: cargo test - - # Stage 2: Security checks - security: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v1 - - # Stage 3: Multi-platform builds (after checks pass) - build: - needs: [check, security] - strategy: - fail-fast: false - matrix: - include: - - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } - - { os: ubuntu-latest, target: wasm32-unknown-unknown } - - { os: macos-latest, target: aarch64-apple-darwin } - - { os: windows-latest, target: x86_64-pc-windows-msvc } - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - run: cargo build --release --target ${{ matrix.target }} - - uses: actions/upload-artifact@v4 - with: - name: build-${{ matrix.target }} - path: target/${{ matrix.target }}/release/ - - # Stage 4: Cross-compiled builds (Linux runner) - cross-build: - needs: check - runs-on: ubuntu-latest - strategy: - matrix: - target: [aarch64-unknown-linux-gnu, armv7-unknown-linux-gnueabihf] - steps: - - uses: actions/checkout@v4 - - run: cargo install cross --git https://github.com/cross-rs/cross - - run: cross build --release --target ${{ matrix.target }} - - # Stage 5: Release (only on tags) - release: - if: startsWith(github.ref, 'refs/tags/') - needs: [build, cross-build] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - path: artifacts - - uses: softprops/action-gh-release@v1 - with: - files: artifacts/**/* - generate_release_notes: true -``` - ---- - -## Checklist - -### Basic CI - -- [ ] Format check (`cargo fmt --check`) -- [ ] Lint check (`cargo clippy -- -D warnings`) -- [ ] Unit tests (`cargo test`) -- [ ] Build verification for all targets - -### Security - -- [ ] cargo-deny for licenses -- [ ] cargo-audit for vulnerabilities -- [ ] Dependabot or Renovate for updates - -### Multi-Platform - -- [ ] Linux (glibc and musl) -- [ ] macOS (Intel and Apple Silicon) -- [ ] Windows (MSVC) -- [ ] WASM (if applicable) -- [ ] Mobile targets (if applicable) - -### Optimization - -- [ ] Cargo caching enabled -- [ ] Cross-compilation on Linux where possible -- [ ] Expensive jobs gated to main/tags -- [ ] Matrix fail-fast disabled for visibility - -### Release - -- [ ] Automated release on tag -- [ ] Artifacts for all platforms -- [ ] Release notes generation -- [ ] Code signing (if required) - ---- - -*Well-configured CI/CD is essential for maintaining cross-platform Rust projects.* diff --git a/.llm/skills/cross-platform-games.md b/.llm/skills/cross-platform-games.md deleted file mode 100644 index 317ca747..00000000 --- a/.llm/skills/cross-platform-games.md +++ /dev/null @@ -1,903 +0,0 @@ -# Cross-Platform Rust Game Development Guide - -> **A practical guide to building Rust games that deploy to desktop, web (WASM), and mobile (iOS/Android).** - -## Overview - -Cross-platform game development in Rust requires careful attention to platform differences in graphics, audio, input, and build systems. This guide covers practical patterns for games specifically. - ---- - -## Platform Support Matrix - -### Current State of Rust Game Engines (2024-2025) - -| Engine/Framework | Desktop | Web (WASM) | iOS | Android | Consoles | -|------------------|---------|------------|-----|---------|----------| -| **Bevy** | ✅ Excellent | ✅ Good (WebGL2/WebGPU) | ✅ Good | ✅ Improved | ❌ NDA | -| **Macroquad** | ✅ Excellent | ✅ Excellent (WebGL1) | ⚠️ Experimental | ⚠️ Experimental | ❌ | -| **miniquad** | ✅ Excellent | ✅ Excellent | ⚠️ Experimental | ⚠️ Experimental | ❌ | -| **ggez** | ✅ Good | ⚠️ Limited | ❌ | ❌ | ❌ | -| **godot-rust** | ✅ Excellent | ✅ Via Godot | ✅ Via Godot | ✅ Via Godot | ✅ Via Godot | -| **SDL2-rs** | ✅ Proven | ❌ | ✅ Proven | ✅ Proven | Varies | - -### Recommendation by Use Case - -| Scenario | Recommended | Reasoning | -|----------|-------------|-----------| -| **Desktop + Web** | Macroquad | Best WASM compatibility, WebGL1 works everywhere | -| **Feature-rich 2D/3D** | Bevy | ECS architecture, rich ecosystem, active development | -| **Maximum control** | miniquad + custom | Fastest compile, smallest binary | -| **Ship to consoles** | godot-rust | Godot handles console certification | -| **Proven mobile** | SDL2-rs | Battle-tested, "A Snake's Tale" shipped on all platforms | - ---- - -## WASM Game Development - -### Build Setup - -```bash -# Install targets -rustup target add wasm32-unknown-unknown - -# Dev tools -cargo install wasm-server-runner # Auto-serve for development -cargo install trunk # Production builds with asset bundling -``` - -### Cargo Configuration - -```toml -# .cargo/config.toml -[target.wasm32-unknown-unknown] -runner = "wasm-server-runner" - -# For getrandom compatibility (required by many game crates) -[target.wasm32-unknown-unknown.dependencies] -rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] -``` - -Or set via environment: - -```bash -RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo build --target wasm32-unknown-unknown -``` - -### WASM Size Optimization - -Binary size directly impacts load times. Apply these optimizations: - -```toml -# Cargo.toml -[profile.release] -opt-level = 'z' # Optimize for size ('s' is less aggressive) -lto = true # Link-time optimization -codegen-units = 1 # Better optimization, slower compile -panic = 'abort' # Remove panic unwinding code -strip = true # Strip symbols - -# Post-build optimization (after wasm-bindgen) -# wasm-opt -Oz -o output.wasm input.wasm -``` - -**Measure your binary:** - -```bash -cargo install twiggy -twiggy top target/wasm32-unknown-unknown/release/game.wasm -``` - -### WASM Limitations for Games - -| Limitation | Impact | Workaround | -|------------|--------|------------| -| **No threads** | Single-threaded execution | Use async/await, Web Workers for heavy tasks | -| **No filesystem** | Can't read/write files | Embed assets with `include_bytes!`, use IndexedDB | -| **WebGL2 default** | Max 256 lights, some features unavailable | Target WebGL1 for compatibility or WebGPU for features | -| **No dynamic linking** | Must statically link everything | Accepted limitation | -| **Audio restrictions** | Browser requires user interaction first | Start audio on first click/keypress | - -### Game Loop Pattern for WASM - -```rust -use std::cell::RefCell; -use std::rc::Rc; -use wasm_bindgen::prelude::*; -use web_sys::window; - -fn request_animation_frame(f: &Closure) { - window() - .expect("window should exist") - .request_animation_frame(f.as_ref().unchecked_ref()) - .expect("should register RAF"); -} - -#[wasm_bindgen(start)] -fn main() -> Result<(), JsValue> { - console_error_panic_hook::set_once(); - - let game_state = Rc::new(RefCell::new(GameState::new())); - - let f = Rc::new(RefCell::new(None)); - let g = f.clone(); - - *g.borrow_mut() = Some(Closure::new({ - let game_state = game_state.clone(); - move || { - let mut state = game_state.borrow_mut(); - state.update(); - state.render(); - request_animation_frame(f.borrow().as_ref().unwrap()); - } - })); - - request_animation_frame(g.borrow().as_ref().unwrap()); - Ok(()) -} -``` - -### Input Handling in WASM - -```rust -use wasm_bindgen::prelude::*; -use web_sys::{KeyboardEvent, MouseEvent, HtmlCanvasElement}; - -// Enable features in Cargo.toml: -// web-sys = { features = ["Document", "KeyboardEvent", "MouseEvent", "HtmlCanvasElement"] } - -fn setup_input(canvas: &HtmlCanvasElement, input_state: Rc>) { - // Keyboard - let input = input_state.clone(); - let keydown = Closure::::new(move |e: KeyboardEvent| { - let mut state = input.borrow_mut(); - match e.key().as_str() { - "ArrowUp" | "w" | "W" => state.up = true, - "ArrowDown" | "s" | "S" => state.down = true, - "ArrowLeft" | "a" | "A" => state.left = true, - "ArrowRight" | "d" | "D" => state.right = true, - " " => state.action = true, - _ => {} - } - e.prevent_default(); - }); - - web_sys::window() - .unwrap() - .add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref()) - .unwrap(); - keydown.forget(); // Prevent cleanup - - // Mouse (for canvas) - let input = input_state.clone(); - let mousedown = Closure::::new(move |e: MouseEvent| { - let mut state = input.borrow_mut(); - state.mouse_x = e.offset_x(); - state.mouse_y = e.offset_y(); - state.mouse_down = true; - }); - - canvas.add_event_listener_with_callback("mousedown", mousedown.as_ref().unchecked_ref()).unwrap(); - mousedown.forget(); -} -``` - ---- - -## Mobile Game Development - -### iOS Setup - -**Prerequisites:** - -- macOS with Xcode 12+ -- Install targets: - - ```bash - rustup target add aarch64-apple-ios # Device - rustup target add aarch64-apple-ios-sim # Simulator (Apple Silicon) - rustup target add x86_64-apple-ios # Simulator (Intel) - ``` - -**Build Commands:** - -```bash -# Device build -cargo build --release --target aarch64-apple-ios - -# Simulator (detect host architecture) -cargo build --release --target aarch64-apple-ios-sim # M1/M2/M3 -cargo build --release --target x86_64-apple-ios # Intel Mac -``` - -**Tools:** - -| Tool | Purpose | -|------|---------| -| `cargo-xcode` | Generate Xcode project from Cargo | -| `cargo-swift` | Generate Swift Package with UniFFI bindings | -| `cbindgen` | Generate C headers for FFI | - -### Android Setup - -**Prerequisites:** - -- Android NDK (r25+ recommended) -- Install targets: - - ```bash - rustup target add aarch64-linux-android # ARM64 (most devices) - rustup target add armv7-linux-androideabi # ARMv7 (legacy) - rustup target add x86_64-linux-android # Emulator x86_64 - rustup target add i686-linux-android # Emulator x86 - ``` - -**Using cargo-ndk:** - -```bash -cargo install cargo-ndk - -# Build for multiple ABIs -cargo ndk -t arm64-v8a -t armeabi-v7a -o ./app/src/main/jniLibs build --release - -# Common flags -cargo ndk --platform 24 -t arm64-v8a build --release -``` - -**Activity Types:** - -- `NativeActivity`: Simpler, no Java/Kotlin required initially -- `GameActivity`: Better input (keyboard, controllers), based on AppCompatActivity - -### Mobile-Specific Patterns - -**Touch Input Abstraction:** - -```rust -pub struct Touch { - pub id: u64, - pub x: f32, - pub y: f32, - pub phase: TouchPhase, -} - -pub enum TouchPhase { - Started, - Moved, - Ended, - Cancelled, -} - -pub trait InputHandler { - fn on_touch(&mut self, touch: Touch); - fn on_key(&mut self, key: KeyCode, pressed: bool); -} - -// Abstract over platform specifics -#[cfg(target_os = "ios")] -fn get_touches() -> Vec { /* iOS impl */ } - -#[cfg(target_os = "android")] -fn get_touches() -> Vec { /* Android impl */ } -``` - -**Lifecycle Handling:** - -```rust -pub trait GameLifecycle { - /// Called when app goes to background - fn on_pause(&mut self) { - self.save_state(); - self.pause_audio(); - } - - /// Called when app returns to foreground - fn on_resume(&mut self) { - self.restore_state(); - self.resume_audio(); - } - - /// Called when app is being terminated - fn on_destroy(&mut self) { - self.save_state(); - } -} -``` - ---- - -## Cross-Platform Architecture - -### Recommended Project Structure - -``` -game/ -├── Cargo.toml # Workspace root -├── crates/ -│ ├── game-core/ # Platform-agnostic game logic -│ │ ├── Cargo.toml -│ │ └── src/ -│ │ ├── lib.rs -│ │ ├── simulation.rs # Deterministic game state -│ │ ├── input.rs # Input abstraction -│ │ └── assets.rs # Asset loading traits -│ │ -│ └── game-render/ # Rendering abstraction -│ ├── Cargo.toml -│ └── src/lib.rs -│ -├── platforms/ -│ ├── desktop/ # Native desktop app -│ │ ├── Cargo.toml -│ │ └── src/main.rs -│ │ -│ ├── web/ # WASM build -│ │ ├── Cargo.toml -│ │ ├── src/lib.rs -│ │ └── index.html -│ │ -│ ├── ios/ # iOS app -│ │ ├── Cargo.toml -│ │ └── GameApp.xcodeproj/ -│ │ -│ └── android/ # Android app -│ ├── Cargo.toml -│ └── app/ -│ -└── assets/ # Shared game assets -``` - -### Feature Flags for Platforms - -```toml -# game-core/Cargo.toml -[features] -default = ["std"] -std = [] - -# Platform presets -desktop = ["std", "threading", "filesystem"] -web = ["wasm-bindgen", "web-sys", "js-sys"] -mobile = ["std", "touch-input"] - -# Capabilities -threading = ["std"] -filesystem = ["std"] -touch-input = [] -audio = [] -networking = ["std"] -``` - -### Platform Abstraction Traits - -```rust -// game-core/src/platform.rs - -/// Time source - different per platform -pub trait Clock { - fn now_millis(&self) -> u64; - fn elapsed_since(&self, previous: u64) -> u64 { - self.now_millis().saturating_sub(previous) - } -} - -/// Random number source - must be deterministic for replays -pub trait Random { - fn next_u32(&mut self) -> u32; - fn next_f32(&mut self) -> f32 { - (self.next_u32() as f32) / (u32::MAX as f32) - } -} - -/// Asset loading - different storage per platform -pub trait AssetLoader { - type Error; - fn load_bytes(&self, path: &str) -> Result, Self::Error>; - fn load_string(&self, path: &str) -> Result; -} - -/// Audio playback -pub trait AudioPlayer { - fn play_sound(&mut self, id: SoundId); - fn play_music(&mut self, id: MusicId); - fn stop_music(&mut self); - fn set_volume(&mut self, volume: f32); -} - -/// Full platform interface -pub trait Platform { - type Clock: Clock; - type Random: Random; - type Assets: AssetLoader; - type Audio: AudioPlayer; - - fn clock(&self) -> &Self::Clock; - fn random(&mut self) -> &mut Self::Random; - fn assets(&self) -> &Self::Assets; - fn audio(&mut self) -> &mut Self::Audio; -} -``` - -### Native Implementation - -```rust -// platforms/desktop/src/platform.rs - -#[cfg(not(target_arch = "wasm32"))] -pub struct NativePlatform { - clock: NativeClock, - random: Pcg32, - assets: NativeAssets, - audio: NativeAudio, -} - -pub struct NativeClock; - -impl Clock for NativeClock { - fn now_millis(&self) -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0) - } -} - -pub struct NativeAssets { - base_path: std::path::PathBuf, -} - -impl AssetLoader for NativeAssets { - type Error = std::io::Error; - - fn load_bytes(&self, path: &str) -> Result, Self::Error> { - std::fs::read(self.base_path.join(path)) - } - - fn load_string(&self, path: &str) -> Result { - std::fs::read_to_string(self.base_path.join(path)) - } -} -``` - -### WASM Implementation - -```rust -// platforms/web/src/platform.rs - -#[cfg(target_arch = "wasm32")] -pub struct WasmPlatform { - clock: WasmClock, - random: Pcg32, - assets: EmbeddedAssets, - audio: WebAudio, -} - -pub struct WasmClock; - -impl Clock for WasmClock { - fn now_millis(&self) -> u64 { - js_sys::Date::now() as u64 - } -} - -pub struct EmbeddedAssets { - // Assets compiled into binary - data: &'static [(&'static str, &'static [u8])], -} - -impl AssetLoader for EmbeddedAssets { - type Error = AssetError; - - fn load_bytes(&self, path: &str) -> Result, Self::Error> { - self.data - .iter() - .find(|(p, _)| *p == path) - .map(|(_, data)| data.to_vec()) - .ok_or(AssetError::NotFound(path.to_string())) - } - - fn load_string(&self, path: &str) -> Result { - let bytes = self.load_bytes(path)?; - String::from_utf8(bytes).map_err(|_| AssetError::InvalidUtf8) - } -} -``` - ---- - -## Asset Embedding Strategies - -### Compile-Time Embedding - -```rust -// For WASM and mobile - embed assets at compile time -mod assets { - pub const PLAYER_SPRITE: &[u8] = include_bytes!("../assets/player.png"); - pub const LEVEL_DATA: &str = include_str!("../assets/level1.json"); - pub const SOUND_JUMP: &[u8] = include_bytes!("../assets/jump.wav"); -} -``` - -### Build Script Asset Processing - -```rust -// build.rs -use std::env; -use std::fs; -use std::path::Path; - -fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("assets.rs"); - - let mut code = String::from("pub mod embedded_assets {\n"); - - // Process each asset file - for entry in fs::read_dir("assets").unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - let name = path.file_stem().unwrap().to_str().unwrap(); - let name_upper = name.to_uppercase().replace("-", "_"); - - code.push_str(&format!( - " pub const {}: &[u8] = include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/assets/{}\"));\n", - name_upper, - path.file_name().unwrap().to_str().unwrap() - )); - } - - code.push_str("}\n"); - fs::write(&dest_path, code).unwrap(); - - println!("cargo::rerun-if-changed=assets/"); -} -``` - ---- - -## Graphics Backend Selection - -### Bevy Feature Configuration - -```toml -# Cargo.toml for cross-platform Bevy game -[dependencies.bevy] -version = "0.15" -default-features = false -features = [ - "bevy_asset", - "bevy_audio", - "bevy_sprite", - "bevy_text", - "bevy_ui", - "bevy_winit", - "png", - "vorbis", -] - -# Platform-specific -[target.'cfg(not(target_arch = "wasm32"))'.dependencies.bevy] -version = "0.15" -features = ["bevy_render", "multi_threaded"] - -[target.'cfg(target_arch = "wasm32")'.dependencies.bevy] -version = "0.15" -features = ["webgl2"] # or "webgpu" for cutting-edge browsers -``` - -### Manual Graphics Backend Selection - -```rust -// Using wgpu for cross-platform rendering -pub fn create_graphics_backend() -> Backend { - #[cfg(target_arch = "wasm32")] - { - // WebGL2 is the safe default - Backend::WebGl2 - // Or for modern browsers: Backend::WebGpu - } - - #[cfg(target_os = "macos")] - { - Backend::Metal - } - - #[cfg(target_os = "windows")] - { - Backend::Dx12 // or Vulkan - } - - #[cfg(target_os = "linux")] - { - Backend::Vulkan - } - - #[cfg(any(target_os = "ios", target_os = "android"))] - { - Backend::OpenGLES - } -} -``` - ---- - -## Audio Handling - -### Cross-Platform Audio Libraries - -| Library | Desktop | WASM | Mobile | Notes | -|---------|---------|------|--------|-------| -| **kira** | ✅ | ⚠️ Limited | ⚠️ | Rich features, no file I/O in WASM | -| **rodio** | ✅ | ❌ | ⚠️ | Simple API, desktop-focused | -| **cpal** | ✅ | ⚠️ | ⚠️ | Low-level, build on top | -| **web-sys AudioContext** | ❌ | ✅ | ❌ | Native Web Audio API | - -### WASM Audio Pattern - -```rust -#[cfg(target_arch = "wasm32")] -mod web_audio { - use wasm_bindgen::prelude::*; - use web_sys::{AudioContext, OscillatorNode, GainNode}; - - pub struct WebAudioPlayer { - context: AudioContext, - // Store decoded audio buffers - sounds: std::collections::HashMap, - } - - impl WebAudioPlayer { - pub fn new() -> Result { - let context = AudioContext::new()?; - Ok(Self { - context, - sounds: std::collections::HashMap::new(), - }) - } - - pub fn play(&self, sound_id: SoundId) -> Result<(), JsValue> { - if let Some(buffer) = self.sounds.get(&sound_id) { - let source = self.context.create_buffer_source()?; - source.set_buffer(Some(buffer)); - source.connect_with_audio_node(&self.context.destination())?; - source.start()?; - } - Ok(()) - } - } -} -``` - ---- - -## Testing Across Platforms - -### Platform-Specific Test Modules - -```rust -// Core tests run everywhere -#[cfg(test)] -mod tests { - #[test] - fn test_game_logic() { - let state = GameState::new(); - assert!(state.is_valid()); - } -} - -// WASM-specific tests -#[cfg(all(test, target_arch = "wasm32"))] -mod wasm_tests { - use wasm_bindgen_test::*; - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - fn test_wasm_rendering() { - // Test that runs in actual browser - } -} - -// Tests requiring threading -#[cfg(all(test, not(target_arch = "wasm32")))] -mod threaded_tests { - #[test] - fn test_parallel_loading() { - use std::thread; - // Multi-threaded test - } -} -``` - -### CI Matrix for Games - -```yaml -# .github/workflows/game-ci.yml -name: Game CI - -on: [push, pull_request] - -jobs: - test: - strategy: - matrix: - include: - # Desktop - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: macos-latest - target: aarch64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc - # WASM - - os: ubuntu-latest - target: wasm32-unknown-unknown - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' && matrix.target != 'wasm32-unknown-unknown' - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev - - - name: Install wasm-pack - if: matrix.target == 'wasm32-unknown-unknown' - run: cargo install wasm-pack - - - name: Build - run: cargo build --target ${{ matrix.target }} - - - name: Test (native) - if: matrix.target != 'wasm32-unknown-unknown' - run: cargo test --target ${{ matrix.target }} - - - name: Test (WASM) - if: matrix.target == 'wasm32-unknown-unknown' - run: wasm-pack test --headless --chrome -``` - ---- - -## Common Pitfalls and Solutions - -### Pitfall: Floating-Point Determinism - -**Problem:** Different platforms may produce slightly different float results. - -**Solution:** Use fixed-point math or [`libm`](https://crates.io/crates/libm) for cross-platform consistency. - -```toml -[dependencies] -libm = "0.2" -``` - -```rust -use libm::{sinf, cosf, sqrtf}; - -fn distance(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { - let dx = x2 - x1; - let dy = y2 - y1; - sqrtf(dx * dx + dy * dy) // Cross-platform consistent -} -``` - -### Pitfall: HashMap Non-Determinism - -**Problem:** `HashMap` iteration order varies between runs/platforms. - -**Solution:** Use `BTreeMap` or sort before iteration. - -```rust -use std::collections::BTreeMap; - -// ✅ Deterministic iteration -let entities: BTreeMap = BTreeMap::new(); - -// ✅ Or sort HashMap keys -let mut keys: Vec<_> = hashmap.keys().collect(); -keys.sort(); -for key in keys { - process(hashmap.get(key).unwrap()); -} -``` - -### Pitfall: Audio User Interaction Requirement - -**Problem:** Browsers block audio until user interaction. - -**Solution:** Initialize audio context on first click/keypress. - -```rust -static AUDIO_INITIALIZED: AtomicBool = AtomicBool::new(false); - -fn handle_user_input() { - if !AUDIO_INITIALIZED.swap(true, Ordering::SeqCst) { - // First interaction - initialize audio - initialize_audio_context(); - } - // Normal input handling... -} -``` - -### Pitfall: WASM Binary Size Bloat - -**Problem:** Debug symbols and panic formatting inflate binary. - -**Solution:** Strip aggressively for release. - -```toml -[profile.release] -opt-level = 'z' -lto = true -codegen-units = 1 -panic = 'abort' -strip = true - -[profile.release.package."*"] -opt-level = 'z' -``` - -### Pitfall: Mobile Memory Pressure - -**Problem:** Mobile devices kill background apps aggressively. - -**Solution:** Save state frequently, handle lifecycle events. - -```rust -impl GameLifecycle for MyGame { - fn on_pause(&mut self) { - // Autosave on every pause - if let Err(e) = self.save_to_storage() { - log::error!("Failed to save: {}", e); - } - } -} -``` - ---- - -## Checklist for Cross-Platform Games - -### Project Setup - -- [ ] Workspace structure separating core from platform code -- [ ] Feature flags for platform capabilities -- [ ] Shared asset directory - -### Build Configuration - -- [ ] All target triplets in `rust-toolchain.toml` -- [ ] WASM runner configured in `.cargo/config.toml` -- [ ] Release profile optimized for size (WASM) or speed (native) - -### Code Quality - -- [ ] Platform traits for clock, random, assets, audio -- [ ] No `std::time::Instant` in core (use trait) -- [ ] Assets embedded for WASM, loaded from disk for native -- [ ] Deterministic collections (`BTreeMap` over `HashMap`) - -### Testing - -- [ ] Core logic tests run on all platforms -- [ ] WASM tests with `wasm_bindgen_test` -- [ ] CI builds for all target platforms - -### Mobile Specific - -- [ ] Lifecycle handling (pause/resume/destroy) -- [ ] Touch input abstraction -- [ ] Signed builds for device testing - ---- - -*Building cross-platform games in Rust is challenging but achievable with the right architecture.* diff --git a/.llm/skills/cross-platform-rust.md b/.llm/skills/cross-platform-rust.md deleted file mode 100644 index 5743d097..00000000 --- a/.llm/skills/cross-platform-rust.md +++ /dev/null @@ -1,1201 +0,0 @@ -# Cross-Platform Rust Development Guide - -> **A guide to writing Rust code that targets multiple platforms: native desktop, mobile (iOS/Android), and WebAssembly.** - -## Overview - -Rust's zero-cost abstractions and explicit platform handling make it excellent for cross-platform development. The key is a **shared core library** with platform-specific binding layers. - -**Related Skills:** - -- [cross-platform-games.md](cross-platform-games.md) — Game-specific cross-platform patterns -- [wasm-rust-guide.md](wasm-rust-guide.md) — WebAssembly deep dive -- [no-std-guide.md](no-std-guide.md) — `no_std` for embedded/WASM - ---- - -## Cross-Compilation Tooling (2024-2025) - -### Tool Comparison - -| Tool | Best For | Setup Complexity | Features | -|------|----------|------------------|----------| -| **cross-rs** | Full cross-compile + testing | Medium (Docker) | 60+ targets, QEMU testing | -| **cargo-zigbuild** | Simple Linux/macOS builds | Low | glibc version control | -| **Native rustup** | Single-platform CI | Lowest | Just needs linker | - -### cross-rs (Recommended for Complex Scenarios) - -Docker-based cross-compilation with zero setup for most targets: - -```bash -# Install -cargo install cross --git https://github.com/cross-rs/cross - -# Use exactly like cargo -cross build --target aarch64-unknown-linux-gnu -cross test --target aarch64-unknown-linux-gnu # Runs via QEMU! -``` - -**Configuration (Cross.toml):** - -```toml -[build] -default-target = "x86_64-unknown-linux-gnu" - -# Install target-specific dependencies -[build.pre-build] -commands = [ - "dpkg --add-architecture $CROSS_DEB_ARCH", - "apt-get update && apt-get -y install libssl-dev:$CROSS_DEB_ARCH" -] - -# Use zig as cross-linker for glibc version control -[target.aarch64-unknown-linux-gnu] -zig = "2.17" # Target glibc 2.17 - -# Custom Docker image -[target.x86_64-unknown-linux-musl.dockerfile] -file = "./Dockerfile.musl" -``` - -### cargo-zigbuild (Simple glibc Targeting) - -```bash -cargo install --locked cargo-zigbuild - -# Cross-compile with specific glibc version -cargo zigbuild --target aarch64-unknown-linux-gnu.2.17 - -# macOS universal binary (ARM64 + x86_64) -rustup target add x86_64-apple-darwin aarch64-apple-darwin -cargo zigbuild --target universal2-apple-darwin -``` - -### Target Installation with rustup - -```bash -# List available targets -rustup target list - -# Add targets -rustup target add aarch64-unknown-linux-gnu -rustup target add wasm32-unknown-unknown -rustup target add aarch64-apple-ios - -# Pin targets in rust-toolchain.toml -``` - -**rust-toolchain.toml:** - -```toml -[toolchain] -channel = "1.83.0" -components = ["clippy", "rustfmt", "rust-src"] -targets = [ - "x86_64-unknown-linux-gnu", - "x86_64-unknown-linux-musl", - "aarch64-unknown-linux-gnu", - "x86_64-apple-darwin", - "aarch64-apple-darwin", - "x86_64-pc-windows-msvc", - "wasm32-unknown-unknown", -] -``` - ---- - -## Project Architecture - -### Recommended Structure - -``` -project/ -├── crates/ -│ └── core/ # Platform-agnostic Rust code -│ ├── Cargo.toml -│ └── src/ -│ ├── lib.rs -│ └── game_logic.rs -├── bindings/ -│ ├── ffi/ # C FFI for iOS/Android -│ │ ├── Cargo.toml -│ │ └── src/lib.rs -│ ├── wasm/ # wasm-bindgen for Web -│ │ ├── Cargo.toml -│ │ └── src/lib.rs -│ └── uniffi/ # Mozilla's uniffi bindings -│ ├── Cargo.toml -│ ├── src/lib.rs -│ └── interface.udl -├── platforms/ -│ ├── android/ # Android app -│ ├── ios/ # iOS Xcode project -│ └── web/ # Web app -└── Cargo.toml # Workspace root -``` - -### Workspace Cargo.toml - -```toml -[workspace] -members = [ - "crates/core", - "bindings/ffi", - "bindings/wasm", -] -resolver = "2" - -[workspace.dependencies] -serde = { version = "1.0", features = ["derive"] } -``` - ---- - -## Platform Abstraction Techniques - -### Trait-Based Abstraction - -```rust -// crates/core/src/platform.rs - -/// Platform-agnostic clock interface -pub trait Clock { - fn now_millis(&self) -> u64; -} - -/// Platform-agnostic network interface -pub trait NetworkSocket { - type Error; - fn send(&mut self, data: &[u8]) -> Result; - fn recv(&mut self, buf: &mut [u8]) -> Result; -} - -// Core logic uses traits, not concrete implementations -pub fn game_loop( - clock: &C, - network: &mut N, - state: &mut GameState, -) -> Result<(), N::Error> { - let dt = clock.now_millis(); - state.update(dt); - network.send(&state.serialize())?; - Ok(()) -} -``` - -### Platform-Specific Implementations - -```rust -// Native implementation -#[cfg(not(target_arch = "wasm32"))] -pub struct NativeClock; - -#[cfg(not(target_arch = "wasm32"))] -impl Clock for NativeClock { - fn now_millis(&self) -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64 - } -} - -// WASM implementation -#[cfg(target_arch = "wasm32")] -pub struct WasmClock; - -#[cfg(target_arch = "wasm32")] -impl Clock for WasmClock { - fn now_millis(&self) -> u64 { - js_sys::Date::now() as u64 - } -} -``` - ---- - -## Build.rs Patterns for Cross-Platform - -### Key Environment Variables - -| Variable | Purpose | -|----------|---------| -| `TARGET` | Target triple being compiled for | -| `HOST` | Host triple (your machine) | -| `OUT_DIR` | Directory for build artifacts | -| `CARGO_CFG_TARGET_OS` | Target OS (linux, windows, macos, etc.) | -| `CARGO_CFG_TARGET_ARCH` | Target architecture (x86_64, aarch64, wasm32) | - -### Platform Detection in build.rs - -```rust -// build.rs -fn main() { - let target = std::env::var("TARGET").unwrap(); - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); - - // Register custom cfgs (required since Rust 1.80) - println!("cargo::rustc-check-cfg=cfg(is_mobile)"); - println!("cargo::rustc-check-cfg=cfg(has_threading)"); - - // Set custom cfg flags based on platform - match target_os.as_str() { - "ios" | "android" => println!("cargo::rustc-cfg=is_mobile"), - _ => {} - } - - // WASM doesn't have native threading - if target_arch != "wasm32" { - println!("cargo::rustc-cfg=has_threading"); - } - - // Rerun only when build.rs changes - println!("cargo::rerun-if-changed=build.rs"); -} -``` - -### Linking Native Libraries - -```rust -// build.rs using the `cc` crate (handles cross-compilation automatically) -fn main() { - cc::Build::new() - .file("src/native_helper.c") - .compile("native_helper"); - - // Link system libraries platform-specifically - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - match target_os.as_str() { - "linux" => println!("cargo::rustc-link-lib=dl"), - "macos" => println!("cargo::rustc-link-lib=framework=CoreFoundation"), - "windows" => println!("cargo::rustc-link-lib=user32"), - _ => {} - } - - println!("cargo::rerun-if-changed=src/native_helper.c"); -} -``` - -### Modern Build Script Syntax (Rust 1.77+) - -```rust -// Use cargo:: prefix (not cargo:) for modern syntax -println!("cargo::rerun-if-changed=src/"); -println!("cargo::rustc-link-lib=static=mylib"); -println!("cargo::rustc-link-search=native=/path/to/lib"); -println!("cargo::rustc-cfg=my_feature"); -println!("cargo::rustc-env=MY_VAR=value"); -println!("cargo::metadata=key=value"); // Pass info to dependent crates -``` - ---- - -## Conditional Compilation - -### Target-Based Compilation - -```rust -// OS-specific -#[cfg(target_os = "android")] -fn platform_init() { /* Android-specific */ } - -#[cfg(target_os = "ios")] -fn platform_init() { /* iOS-specific */ } - -#[cfg(target_os = "windows")] -fn platform_init() { /* Windows-specific */ } - -#[cfg(target_os = "linux")] -fn platform_init() { /* Linux-specific */ } - -#[cfg(target_os = "macos")] -fn platform_init() { /* macOS-specific */ } - -// Architecture-specific -#[cfg(target_arch = "wasm32")] -fn platform_init() { /* WASM-specific */ } - -#[cfg(target_arch = "x86_64")] -fn platform_init() { /* x86-64-specific */ } - -#[cfg(target_arch = "aarch64")] -fn platform_init() { /* ARM64-specific */ } -``` - -### Feature-Based Compilation - -```toml -# Cargo.toml -[features] -default = ["std"] -std = [] -alloc = [] - -# Platform features -mobile = [] -desktop = [] -web = ["wasm-bindgen", "js-sys", "web-sys"] - -# Optional capabilities -networking = ["std"] -multithreading = ["std"] -``` - -```rust -// Use features in code -#[cfg(feature = "std")] -use std::collections::HashMap; - -#[cfg(not(feature = "std"))] -use alloc::collections::BTreeMap as HashMap; - -#[cfg(feature = "networking")] -mod network; - -#[cfg(feature = "multithreading")] -use std::sync::Arc; - -#[cfg(not(feature = "multithreading"))] -use core::cell::RefCell; -``` - -### Combining Conditions - -```rust -// Multiple conditions with all/any -#[cfg(all(target_arch = "wasm32", feature = "web"))] -mod web_impl; - -#[cfg(any(target_os = "ios", target_os = "android"))] -mod mobile_impl; - -#[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))] -mod desktop_impl; -``` - ---- - -## Platform-Specific Dependencies - -### Cargo.toml Configuration - -```toml -[dependencies] -# Always included -serde = { version = "1.0", default-features = false, features = ["derive"] } - -# std-only dependencies -[dependencies.tokio] -version = "1.0" -optional = true - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" -js-sys = "0.3" -web-sys = { version = "0.3", features = ["console", "Window"] } -wasm-bindgen-futures = "0.4" - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { version = "1.0", features = ["rt-multi-thread", "net", "time"] } - -[target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.14" -jni = "0.21" - -[target.'cfg(target_os = "ios")'.dependencies] -objc = "0.2" -``` - ---- - -## FFI Bindings by Platform - -### C FFI (iOS/Android) - -```rust -// bindings/ffi/src/lib.rs -use core_lib::GameState; - -#[repr(C)] -pub struct FfiGameState { - ptr: *mut GameState, -} - -#[no_mangle] -pub extern "C" fn game_state_new() -> FfiGameState { - let state = Box::new(GameState::new()); - FfiGameState { - ptr: Box::into_raw(state), - } -} - -#[no_mangle] -pub extern "C" fn game_state_update(state: &mut FfiGameState, dt: f32) { - unsafe { - if let Some(s) = state.ptr.as_mut() { - s.update(dt); - } - } -} - -#[no_mangle] -pub extern "C" fn game_state_free(state: FfiGameState) { - if !state.ptr.is_null() { - unsafe { - drop(Box::from_raw(state.ptr)); - } - } -} -``` - -### Mozilla uniffi (iOS/Android) - -``` -// bindings/uniffi/interface.udl -namespace game_lib { - string get_version(); -}; - -interface GameState { - constructor(); - void update(f32 dt); - string serialize(); -}; -``` - -```rust -// bindings/uniffi/src/lib.rs -uniffi::setup_scaffolding!(); - -#[derive(uniffi::Object)] -pub struct GameState { - inner: core_lib::GameState, -} - -#[uniffi::export] -impl GameState { - #[uniffi::constructor] - pub fn new() -> Self { - Self { inner: core_lib::GameState::new() } - } - - pub fn update(&self, dt: f32) { - self.inner.update(dt); - } - - pub fn serialize(&self) -> String { - self.inner.serialize() - } -} - -#[uniffi::export] -pub fn get_version() -> String { - env!("CARGO_PKG_VERSION").to_string() -} -``` - -### swift-bridge (iOS) - -```rust -// bindings/swift/src/lib.rs -#[swift_bridge::bridge] -mod ffi { - extern "Rust" { - type GameState; - - #[swift_bridge(init)] - fn new() -> GameState; - - #[swift_bridge(swift_name = "update")] - fn update(&mut self, dt: f32); - - fn serialize(&self) -> String; - } -} - -pub struct GameState { - inner: core_lib::GameState, -} - -impl GameState { - pub fn new() -> Self { - Self { inner: core_lib::GameState::new() } - } - - pub fn update(&mut self, dt: f32) { - self.inner.update(dt); - } - - pub fn serialize(&self) -> String { - self.inner.serialize() - } -} -``` - -### wasm-bindgen (Web) - -```rust -// bindings/wasm/src/lib.rs -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -pub struct GameState { - inner: core_lib::GameState, -} - -#[wasm_bindgen] -impl GameState { - #[wasm_bindgen(constructor)] - pub fn new() -> GameState { - console_error_panic_hook::set_once(); - GameState { inner: core_lib::GameState::new() } - } - - pub fn update(&mut self, dt: f32) { - self.inner.update(dt); - } - - pub fn serialize(&self) -> String { - self.inner.serialize() - } -} -``` - ---- - -## Build System Integration - -### Android (Gradle + Cargo) - -```groovy -// build.gradle -android { - // ... - - externalNativeBuild { - cmake { - path "CMakeLists.txt" - } - } -} -``` - -```cmake -# CMakeLists.txt -cmake_minimum_required(VERSION 3.22) - -include(FetchContent) -FetchContent_Declare( - Corrosion - GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git - GIT_TAG v0.5 -) -FetchContent_MakeAvailable(Corrosion) - -corrosion_import_crate(MANIFEST_PATH ../../bindings/ffi/Cargo.toml) -``` - -### iOS (Xcode + cargo-xcode) - -```bash -# Install cargo-xcode -cargo install cargo-xcode - -# Generate Xcode project -cd bindings/swift -cargo xcode - -# Or use build script -cargo build --target aarch64-apple-ios --release -cargo build --target aarch64-apple-ios-sim --release -``` - -### Web (wasm-pack) - -```json -// package.json -{ - "scripts": { - "build": "wasm-pack build bindings/wasm --target web --out-dir ../../platforms/web/pkg", - "build:node": "wasm-pack build bindings/wasm --target nodejs" - } -} -``` - ---- - -## Testing Across Platforms - -### Core Tests (Platform-Agnostic) - -```rust -// crates/core/src/lib.rs -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_game_logic() { - let mut state = GameState::new(); - state.update(0.016); - assert!(state.is_valid()); - } -} -``` - -### Platform-Specific Tests - -```rust -// WASM tests -#[cfg(all(test, target_arch = "wasm32"))] -mod wasm_tests { - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - fn test_wasm_binding() { - let state = super::GameState::new(); - assert!(state.serialize().len() > 0); - } -} - -// Native tests with threading -#[cfg(all(test, not(target_arch = "wasm32")))] -mod native_tests { - #[test] - fn test_multithreaded() { - use std::thread; - let handles: Vec<_> = (0..4) - .map(|_| thread::spawn(|| super::GameState::new())) - .collect(); - for h in handles { - assert!(h.join().is_ok()); - } - } -} -``` - -### CI Configuration - -```yaml -# .github/workflows/cross-platform.yml -jobs: - test: - strategy: - matrix: - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: macos-latest - target: aarch64-apple-darwin - - os: windows-latest - target: x86_64-pc-windows-msvc - - os: ubuntu-latest - target: wasm32-unknown-unknown - - os: ubuntu-latest - target: aarch64-linux-android - - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - run: cargo test --target ${{ matrix.target }} --workspace -``` - ---- - -## Common Patterns - -### Unified Error Type - -```rust -// crates/core/src/error.rs -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Error { - InvalidState { reason: &'static str }, - NetworkError { message: String }, - ParseError { details: String }, -} - -// Platform-specific error conversion -#[cfg(target_arch = "wasm32")] -impl From for wasm_bindgen::JsValue { - fn from(e: Error) -> Self { - wasm_bindgen::JsValue::from_str(&format!("{:?}", e)) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for Error {} - -impl core::fmt::Display for Error { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:?}", self) - } -} -``` - -### Platform Abstraction Layer - -```rust -// crates/core/src/platform.rs - -/// Platform-specific functionality -pub trait Platform { - type Clock: Clock; - type Random: Random; - type Network: NetworkSocket; - - fn clock(&self) -> &Self::Clock; - fn random(&mut self) -> &mut Self::Random; - fn network(&mut self) -> &mut Self::Network; -} - -// Use in core logic -pub fn run_frame(platform: &mut P, state: &mut GameState) { - let now = platform.clock().now_millis(); - let random_value = platform.random().next_u32(); - state.update_with(now, random_value); -} -``` - -### Serialization Across Platforms - -```rust -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone)] -pub struct GameState { - pub frame: u64, - pub entities: Vec, - // Using Vec for cross-platform byte serialization - pub custom_data: Vec, -} - -impl GameState { - pub fn to_bytes(&self) -> Vec { - // Use a deterministic serialization format - bincode::serialize(self).expect("serialization should not fail") - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - bincode::deserialize(bytes) - .map_err(|e| Error::ParseError { details: e.to_string() }) - } -} -``` - ---- - -## Platform-Specific Considerations - -### Mobile (iOS/Android) - -- **Battery life**: Avoid busy loops, use system timers -- **Background handling**: Save state when app backgrounds -- **Memory**: Monitor heap usage, avoid large allocations -- **Touch input**: Abstract touch coordinates to logical units - -### WebAssembly - -- **No threads**: Use `async`/`await` or Web Workers -- **No filesystem**: Use IndexedDB via `idb` crate -- **Binary size**: Optimize with `opt-level = "z"`, strip, LTO -- **Startup time**: Use streaming compilation - -### Desktop - -- **Windowing**: Use `winit` for cross-platform windows -- **GPU**: Use `wgpu` for cross-platform graphics -- **Audio**: Use `cpal` for cross-platform audio - ---- - -## Mobile Development Tooling (2024-2025) - -### iOS Build Setup - -```bash -# Install targets -rustup target add aarch64-apple-ios # Device (ARM64) -rustup target add aarch64-apple-ios-sim # Simulator (Apple Silicon) -rustup target add x86_64-apple-ios # Simulator (Intel) - -# Build for device -cargo build --release --target aarch64-apple-ios - -# Build for simulator (detect your Mac's architecture) -cargo build --release --target aarch64-apple-ios-sim # M1/M2/M3 -cargo build --release --target x86_64-apple-ios # Intel -``` - -**Key iOS Tools:** - -| Tool | Purpose | -|------|---------| -| `cargo-swift` | Generate Swift Packages from UniFFI | -| `cargo-xcode` | Generate Xcode project files | -| `cbindgen` | Generate C/C++ headers for FFI | - -### Android Build Setup with cargo-ndk - -```bash -# Install cargo-ndk -cargo install cargo-ndk - -# Install targets -rustup target add aarch64-linux-android # ARM64 (most modern devices) -rustup target add armv7-linux-androideabi # ARMv7 (legacy devices) -rustup target add x86_64-linux-android # x86_64 emulator -rustup target add i686-linux-android # x86 emulator - -# Build for multiple ABIs, output to jniLibs -cargo ndk -t arm64-v8a -t armeabi-v7a -o ./app/src/main/jniLibs build --release - -# With specific platform level -cargo ndk --platform 24 -t arm64-v8a build --release -``` - -**Android Activity Types:** - -- **NativeActivity**: Simpler, full Rust app without Java/Kotlin -- **GameActivity**: Better input handling (AGDK-based), recommended for games - -### UniFFI Bindings (Recommended for Mobile) - -UniFFI generates Swift, Kotlin, and Python bindings from Rust code: - -```rust -// Using proc macros (modern approach) -uniffi::setup_scaffolding!(); - -#[derive(uniffi::Object)] -pub struct GameEngine { - state: GameState, -} - -#[uniffi::export] -impl GameEngine { - #[uniffi::constructor] - pub fn new() -> Self { - Self { state: GameState::default() } - } - - pub fn update(&mut self, delta_time: f32) { - self.state.update(delta_time); - } - - pub fn get_score(&self) -> u32 { - self.state.score - } -} - -#[uniffi::export] -pub fn get_version() -> String { - env!("CARGO_PKG_VERSION").to_string() -} -``` - -Generate bindings: - -```bash -# Swift (iOS) -cargo swift package -p my-game -n MyGame - -# Kotlin (Android) - typically via gradle plugin -``` - ---- - -## CI/CD for Cross-Platform Projects - -### GitHub Actions Multi-Platform Build - -```yaml -# .github/workflows/cross-platform.yml -name: Cross-Platform CI - -on: [push, pull_request] - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - # Desktop - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - use_cross: true - - target: x86_64-apple-darwin - os: macos-latest - - target: aarch64-apple-darwin - os: macos-latest - - target: x86_64-pc-windows-msvc - os: windows-latest - - # WASM - - target: wasm32-unknown-unknown - os: ubuntu-latest - - # Mobile (build only) - - target: aarch64-linux-android - os: ubuntu-latest - use_cross: true - - target: aarch64-apple-ios - os: macos-latest - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross - if: matrix.use_cross - run: cargo install cross --git https://github.com/cross-rs/cross - - - name: Install Linux dependencies - if: matrix.os == 'ubuntu-latest' && !matrix.use_cross && matrix.target != 'wasm32-unknown-unknown' - run: | - sudo apt-get update - sudo apt-get install -y libasound2-dev libudev-dev - - - name: Build (native) - if: ${{ !matrix.use_cross }} - run: cargo build --release --target ${{ matrix.target }} - - - name: Build (cross) - if: matrix.use_cross - run: cross build --release --target ${{ matrix.target }} - - - name: Test (native, non-mobile) - if: ${{ !matrix.use_cross && !contains(matrix.target, 'ios') && !contains(matrix.target, 'android') && matrix.target != 'wasm32-unknown-unknown' }} - run: cargo test --target ${{ matrix.target }} -``` - -### Cost-Optimized CI Strategy - -Cross-compile on cheap Linux runners, test on native hardware only when needed: - -```yaml -jobs: - # Fast Linux builds for all targets - cross-compile: - runs-on: ubuntu-latest - strategy: - matrix: - target: - - x86_64-unknown-linux-gnu - - aarch64-unknown-linux-gnu - - x86_64-pc-windows-gnu # MinGW - steps: - - uses: actions/checkout@v4 - - name: Install cross - run: cargo install cross --git https://github.com/cross-rs/cross - - name: Build - run: cross build --release --target ${{ matrix.target }} - - # Native runners only for final verification - verify-macos: - runs-on: macos-latest - needs: cross-compile # Only run after cross-compile succeeds - steps: - - uses: actions/checkout@v4 - - run: cargo test -``` - ---- - -## Tool Summary - -| Platform | Binding Tool | Build System | CI Runner | -|----------|-------------|--------------|-----------| -| Linux (native) | N/A | cargo | ubuntu-latest | -| Linux (cross) | N/A | cross-rs | ubuntu-latest | -| macOS | N/A | cargo | macos-latest | -| Windows | N/A | cargo | windows-latest | -| iOS | uniffi, swift-bridge | cargo-swift, Xcode | macos-latest | -| Android | uniffi, jni-rs | cargo-ndk, Gradle | ubuntu-latest + cross | -| Web | wasm-bindgen | wasm-pack, Trunk | ubuntu-latest | - ---- - -## Cross-Compilation Ecosystem Challenges - -### Understanding the Rust Cross-Compile Landscape - -Unlike Go, Rust cross-compilation requires explicit toolchain configuration: - -| Aspect | Go | Rust | -|--------|----|----| -| **libc dependency** | None (syscall shims) | Required for `std` | -| **Cross-compile setup** | `GOOS=linux GOARCH=amd64 go build` | Target + linker config | -| **Static by default** | Yes | No (glibc dynamic) | -| **Binary portability** | Excellent | Good with musl | - -### Known Challenges - -1. **Cargo's Linker Detection**: Cargo has no built-in logic for detecting the correct linker; defaults to `cc` requiring manual configuration -2. **libc Dependency**: Rust's `std` depends on libc; unlike Go, cannot easily bypass for syscalls -3. **LLD Bundling**: LLVM's `lld` linker not yet bundled with Rust, complicating cross-linking -4. **Platform-Specific Binaries**: No universal binary format; each OS requires separate builds - -### Recommended Solutions - -| Tool | Best For | Notes | -|------|----------|-------| -| **cross-rs** | Complete cross-compile | Docker-based, pre-configured | -| **cargo-zigbuild** | glibc version control | Uses Zig's linker, no Docker | -| **cargo-xwin** | Linux → Windows MSVC | Easy MSVC target without Windows | -| **musl targets** | Static Linux binaries | Portable but some crate issues | -| **GitHub Actions matrix** | Release builds | Native runners per platform | - -### Static Linux Binaries with musl - -```bash -rustup target add x86_64-unknown-linux-musl -cargo build --release --target x86_64-unknown-linux-musl -``` - -**musl Caveats:** - -- DNS resolution requires special handling (no NSS) -- Some crates with C dependencies may not compile cleanly -- May need `CC_x86_64_unknown_linux_musl=musl-gcc` environment variable - -### Windows MSVC Preference - -MSVC toolchain produces significantly smaller binaries than MinGW: - -| Toolchain | Binary Size | Reason | -|-----------|-------------|--------| -| `x86_64-pc-windows-gnu` | ~100 MB | Embeds debug symbols | -| `x86_64-pc-windows-msvc` | ~10 MB | Symbols in separate .pdb | - ---- - -## Common Pitfalls - -### Pitfall: Breaking Other Platforms Silently - -**Problem:** Changes compile on your platform but break others. - -**Solution:** CI must build all targets on every PR. - -### Pitfall: Global Cargo Config Conflicts - -**Problem:** `~/.cargo/config.toml` settings (like sccache) break in Docker. - -**Solution:** Use project-local `.cargo/config.toml`, avoid global settings in CI. - -### Pitfall: MinGW DLL Issues on Windows - -**Problem:** Missing DLLs at runtime. - -**Solution:** Ship DLLs with binary or use static linking where possible. - -### Pitfall: glibc Version Mismatch - -**Problem:** Binary requires newer glibc than target system has. - -**Solution:** Use `cargo-zigbuild` with explicit glibc version or musl target. - -```bash -cargo zigbuild --target x86_64-unknown-linux-gnu.2.17 -``` - -### Pitfall: Interior Mutability with UniFFI - -**Problem:** UniFFI assumes objects may be mutated from multiple threads. - -**Solution:** Use interior mutability with `Mutex` or `RwLock`: - -```rust -// ❌ Won't work with UniFFI -impl MyObject { - fn update(&mut self) { ... } -} - -// ✅ Use interior mutability -use std::sync::Mutex; - -#[derive(uniffi::Object)] -struct MyObject { - data: Mutex, -} - -#[uniffi::export] -impl MyObject { - fn update(&self) { - let mut data = self.data.lock().unwrap(); - // mutate data - } -} -``` - -### Pitfall: UniFFI Arc Requirements - -**Problem:** UniFFI constructors must return `Arc`. - -**Solution:** - -```rust -#[uniffi::export] -impl RouteAdapter { - #[uniffi::constructor] - pub fn new() -> Arc { - Arc::new(Self { /* ... */ }) - } -} -``` - ---- - -## Checklist - -### Project Setup - -- [ ] Workspace structure with core library -- [ ] Platform-specific binding crates -- [ ] Feature flags for optional capabilities -- [ ] Shared dependencies in workspace -- [ ] `rust-toolchain.toml` with all targets listed - -### Code Organization - -- [ ] Core logic uses traits for platform abstraction -- [ ] No `std` dependency in core (use `alloc` if needed) -- [ ] Errors implement platform-specific conversions -- [ ] Serialization is deterministic and portable -- [ ] `build.rs` uses modern `cargo::` syntax - -### Build & Test - -- [ ] CI tests all target platforms -- [ ] Platform-specific tests exist -- [ ] Release builds are optimized -- [ ] Documentation covers platform differences -- [ ] cross-rs or cargo-zigbuild configured for cross-compilation - -### Mobile - -- [ ] UniFFI bindings for Swift/Kotlin -- [ ] cargo-ndk for Android builds -- [ ] XCFramework generation for iOS - ---- - -*Cross-platform Rust enables writing high-performance code once and deploying everywhere.* diff --git a/.llm/skills/cross-platform.md b/.llm/skills/cross-platform.md new file mode 100644 index 00000000..9dee25ce --- /dev/null +++ b/.llm/skills/cross-platform.md @@ -0,0 +1,269 @@ + + +# Cross-Platform Rust Development + +## Target Matrix + +| Target Triple | Platform | Tier | CI Runner | +|---------------|----------|------|-----------| +| `x86_64-unknown-linux-gnu` | Linux x64 (glibc) | 1 | `ubuntu-latest` | +| `x86_64-unknown-linux-musl` | Linux x64 (static) | 2 | `ubuntu-latest` + cross | +| `aarch64-unknown-linux-gnu` | Linux ARM64 | 2 | `ubuntu-latest` + cross | +| `x86_64-apple-darwin` | macOS Intel | 1 | `macos-latest` | +| `aarch64-apple-darwin` | macOS Apple Silicon | 1 | `macos-latest` | +| `x86_64-pc-windows-msvc` | Windows x64 | 1 | `windows-latest` | +| `wasm32-unknown-unknown` | WebAssembly | 2 | `ubuntu-latest` | +| `aarch64-apple-ios` | iOS Device | 2 | `macos-latest` | +| `aarch64-linux-android` | Android ARM64 | 2 | `ubuntu-latest` + cross | + +## Cross-Compilation Tools + +| Tool | Best For | Notes | +|------|----------|-------| +| **cross-rs** | Full cross-compile + testing | Docker-based, 60+ targets, QEMU testing | +| **cargo-zigbuild** | glibc version control | Uses Zig linker, no Docker | +| **cargo-xwin** | Linux to Windows MSVC | Easy MSVC without Windows | +| **Native rustup** | Single-platform CI | Just needs linker | + +## Conditional Compilation Patterns + +### cfg Attributes + +```rust +// OS-specific +#[cfg(target_os = "linux")] +fn platform_init() { /* Linux */ } + +#[cfg(any(target_os = "ios", target_os = "android"))] +mod mobile_impl; + +// Architecture-specific +#[cfg(target_arch = "wasm32")] +mod wasm_impl; + +#[cfg(not(target_arch = "wasm32"))] +mod native_impl; + +// Combining conditions +#[cfg(all(target_arch = "wasm32", feature = "web"))] +mod web_impl; + +#[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))] +mod desktop_impl; +``` + +### Feature-Based Compilation + +```toml +[features] +default = ["std"] +std = [] +web = ["wasm-bindgen", "js-sys", "web-sys"] +mobile = ["std", "touch-input"] +networking = ["std"] +``` + +### Platform-Specific Dependencies + +```toml +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" +web-sys = { version = "0.3", features = ["console", "Window"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.0", features = ["rt-multi-thread", "net", "time"] } +``` + +### Custom cfg in build.rs (Rust 1.80+) + +```rust +fn main() { + // build.rs: Cargo guarantees these env vars exist + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + println!("cargo::rustc-check-cfg=cfg(is_mobile)"); + println!("cargo::rustc-check-cfg=cfg(has_threading)"); + match target_os.as_str() { + "ios" | "android" => println!("cargo::rustc-cfg=is_mobile"), + _ => {} + } + if target_arch != "wasm32" { + println!("cargo::rustc-cfg=has_threading"); + } + println!("cargo::rerun-if-changed=build.rs"); +} +``` + +## Project Architecture + +``` +project/ ++-- crates/core/ # Platform-agnostic Rust code ++-- bindings/ +| +-- ffi/ # C FFI for iOS/Android +| +-- wasm/ # wasm-bindgen for Web +| +-- uniffi/ # Mozilla uniffi bindings ++-- platforms/ +| +-- android/ # Android app +| +-- ios/ # iOS Xcode project +| +-- web/ # Web app ++-- Cargo.toml # Workspace root +``` + +### Trait-Based Platform Abstraction + +```rust +pub trait Platform { + type Clock: Clock; + type Random: Random; + type Network: NetworkSocket; + fn clock(&self) -> &Self::Clock; + fn random(&mut self) -> &mut Self::Random; + fn network(&mut self) -> &mut Self::Network; +} + +pub trait Clock { fn now_millis(&self) -> u64; } +pub trait NetworkSocket { + type Error; + fn send(&mut self, data: &[u8]) -> Result; + fn recv(&mut self, buf: &mut [u8]) -> Result; +} +``` + +## CI Matrix Strategy + +```yaml +strategy: + fail-fast: false # Run all platforms even if one fails + matrix: + include: + - { os: ubuntu-latest, target: x86_64-unknown-linux-gnu } + - { os: ubuntu-latest, target: wasm32-unknown-unknown } + - { os: macos-latest, target: aarch64-apple-darwin } + - { os: windows-latest, target: x86_64-pc-windows-msvc } +runs-on: ${{ matrix.os }} +steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - run: cargo build --release --target ${{ matrix.target }} +``` + +### Cost Optimization + +macOS runners cost 10x, Windows 2x more than Linux. Cross-compile on Linux, verify on native only when needed: + +```yaml +jobs: + cross-compile: + runs-on: ubuntu-latest + strategy: + matrix: + target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu] + steps: + - uses: actions/checkout@v4 + - run: cargo install cross --git https://github.com/cross-rs/cross + - run: cross build --release --target ${{ matrix.target }} + macos-verify: + runs-on: macos-latest + needs: cross-compile + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - run: cargo test +``` + +## Platform-Specific Gotchas + +| Gotcha | Problem | Solution | +|--------|---------|----------| +| Float determinism | Different FPU behavior across platforms | Use `libm` or fixed-point math | +| HashMap iteration | Order varies between runs/platforms | Use `BTreeMap` or sort before iteration | +| glibc mismatch | Binary needs newer glibc than target | `cargo zigbuild --target x86_64-unknown-linux-gnu.2.17` | +| MSVC vs MinGW | MinGW embeds debug symbols (~100MB vs ~10MB) | Prefer `x86_64-pc-windows-msvc` | +| musl DNS | No NSS support with musl | Special handling needed | +| UniFFI mutability | Assumes multi-thread mutation | Use `Mutex`/`RwLock` interior mutability | +| cross-rs images | Unstable `:main` tags break CI | Use env var passthrough, not custom images | +| Mobile memory | Aggressive background app killing | Save state on every pause | +| WASM no threads | Single-threaded execution | Use async/await or Web Workers | +| Audio on web | Browser blocks until interaction | Initialize audio on first click | + +### cross-rs: Environment Passthrough (Recommended) + +```toml +# Cross.toml -- avoid custom images, use env passthrough +[build.env] +passthrough = [ + "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER", + "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS", +] +``` + +## Game Engine Platform Support + +| Engine | Desktop | Web (WASM) | iOS | Android | +|--------|---------|------------|-----|---------| +| **Bevy** | Excellent | Good (WebGL2/WebGPU) | Good | Improved | +| **Macroquad** | Excellent | Excellent (WebGL1) | Experimental | Experimental | +| **godot-rust** | Excellent | Via Godot | Via Godot | Via Godot | + +## Binding Tools Summary + +| Platform | Binding Tool | Build System | +|----------|-------------|--------------| +| iOS | uniffi, swift-bridge | cargo-swift, Xcode | +| Android | uniffi, jni-rs | cargo-ndk, Gradle | +| Web | wasm-bindgen | wasm-pack, Trunk | + +## Mobile: Touch Input Abstraction + +```rust +pub struct Touch { pub id: u64, pub x: f32, pub y: f32, pub phase: TouchPhase } +pub enum TouchPhase { Started, Moved, Ended, Cancelled } +pub trait InputHandler { + fn on_touch(&mut self, touch: Touch); + fn on_key(&mut self, key: KeyCode, pressed: bool); +} +``` + +## WASM Size Optimization + +```toml +[profile.release] +opt-level = 'z' +lto = true +codegen-units = 1 +panic = 'abort' +strip = true +``` + +Post-build: `wasm-opt -Oz -o output.wasm input.wasm` + +## Testing Across Platforms + +```rust +#[cfg(all(test, target_arch = "wasm32"))] +mod wasm_tests { + use wasm_bindgen_test::*; + wasm_bindgen_test_configure!(run_in_browser); + #[wasm_bindgen_test] + fn test_wasm_binding() { /* ... */ } +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod native_tests { + #[test] + fn test_multithreaded() { /* ... */ } +} +``` + +### What Must Run Cross-Platform + +| Category | Cross-Platform? | Rationale | +|----------|----------------|-----------| +| Unit/integration tests | Required | Platform-specific logic bugs | +| Loom/Miri | Required | Threading/memory layout differs | +| Clippy/fmt/coverage | One platform OK | Platform-agnostic | +| Kani/security scanning | One platform OK | Platform-agnostic | diff --git a/.llm/skills/defensive-programming.md b/.llm/skills/defensive-programming.md index 0f867240..43ee3db1 100644 --- a/.llm/skills/defensive-programming.md +++ b/.llm/skills/defensive-programming.md @@ -1,1184 +1,146 @@ -# Defensive Programming — Zero-Panic Production Code - -> **This document defines the defensive programming standards for Fortress Rollback.** -> All production code, including library code and any editor/tooling code, MUST follow these practices. -> **This also applies to documentation examples** — rustdoc examples are compiled and should demonstrate correct error handling. - -## Core Philosophy - -**Production code must be 100% safe, predictable, and deterministic.** We achieve this through: - -1. **Zero-Panic Policy** — Never panic in production; return `Result` for all fallible operations -2. **Assume Nothing** — Validate all inputs and internal state; trust no assumptions -3. **Expose Errors, Don't Swallow** — Callers must handle potential failures explicitly -4. **Maintain Invariants** — Internal state must remain consistent even during error recovery -5. **Type-Safe APIs** — Use the type system to make invalid states unrepresentable - ---- + + +# Defensive Programming -- Zero-Panic Production Code ## Zero-Panic Policy (CRITICAL) -### Forbidden Panic Patterns - -The following patterns are **STRICTLY FORBIDDEN** in production code: +### Forbidden Patterns ```rust -// ❌ FORBIDDEN - Direct panic -panic!("something went wrong"); -panic!("{}", message); - -// ❌ FORBIDDEN - Implicit panics via unwrap/expect -value.unwrap(); -value.expect("should never be None"); -result.unwrap(); -result.expect("operation must succeed"); - -// ❌ FORBIDDEN - Index operations that can panic -array[index]; -slice[range]; -vec[index]; -string.chars().nth(i).unwrap(); - -// ❌ FORBIDDEN - Placeholder panics -todo!(); -todo!("implement later"); -unimplemented!(); -unimplemented!("not yet implemented"); - -// ❌ FORBIDDEN - Assertions in production paths (OK in tests) -assert!(condition); -assert_eq!(a, b); -assert_ne!(a, b); -debug_assert!(condition); // OK in debug builds, but prefer explicit handling - -// ❌ FORBIDDEN - unreachable! (unless TRULY unreachable by type system) -unreachable!(); -unreachable!("this should never happen"); +panic!(); value.unwrap(); value.expect("..."); array[index]; todo!(); unimplemented!(); +assert!(cond); // OK in tests only +unreachable!(); // Only when type system guarantees it ``` -### Documentation Examples Must Also Follow Zero-Panic - -**Rustdoc examples are compiled code.** They must demonstrate proper error handling, not panic shortcuts: - -```rust -// ❌ FORBIDDEN in doc examples — teaches bad habits -/// # Examples -/// -/// ``` -/// let session = SessionBuilder::new().build().unwrap(); -/// let result = session.advance_frame(); -/// if result.is_err() { -/// panic!("frame advance failed"); // NEVER show panic! as error handling -/// } -/// ``` - -// ✅ REQUIRED in doc examples — demonstrates proper patterns -/// # Examples -/// -/// ``` -/// # use fortress_rollback::*; -/// let session = SessionBuilder::new().build()?; -/// let requests = session.advance_frame()?; -/// for request in requests { -/// // Handle each request... -/// } -/// # Ok::<(), FortressError>(()) -/// ``` -``` - -**Why this matters:** - -- Doc examples are often copy-pasted by users -- Examples with `panic!` or `unwrap()` teach incorrect patterns -- Users learn from examples — show them the RIGHT way -- The `# Ok::<(), Error>(())` pattern enables `?` in doc tests - -### When Different Patterns Are Acceptable in Doc Examples - -While the general rule is "no panics in examples," there are nuanced cases where different patterns are appropriate based on what the example is teaching: - -#### Use `if let Some` for Defensive Fallback Patterns - -When demonstrating how users should handle optional state in their game loops: - -```rust -/// // Simulate a LoadGameState request handler -/// // LoadGameState is only requested for previously saved frames, -/// // but we handle None defensively to avoid crashing on library bugs -/// if let Some(loaded) = cell.load() { -/// current_state = loaded; -/// } -/// // If load() returns None, current_state is unchanged -``` - -**When to use:** Teaching defensive programming where the caller should gracefully handle missing data rather than crash. - -#### Use `.expect("reason")` for Provably-Present State - -When the example demonstrates a successful path where the state is guaranteed to exist by prior operations in the same example: - -```rust -/// // We just saved the state above, so we know it exists -/// let accessor = cell.data().expect("state was just saved"); -/// assert_eq!(accessor.player_name, "alex"); -``` - -**When to use:** When the example's control flow proves the value exists, AND the focus is on demonstrating the happy path. The `.expect()` message must explain WHY it's safe. - -**Important:** This is acceptable ONLY when: - -- The example itself proves the value exists (e.g., save then load) -- The message clearly explains the invariant -- The example demonstrates API usage, not error handling - -#### Use `.ok_or(Error)?` for Error Propagation Patterns - -When demonstrating how users should handle missing state as an error condition: - -```rust -/// let loaded = cell.load_or_err(frame)?; -/// // Or manually: -/// let loaded = cell.load() -/// .ok_or(FortressError::InvalidFrameStructured { -/// frame, -/// reason: InvalidFrameReason::MissingState, -/// })?; -``` - -**When to use:** Teaching error propagation where missing data is a genuine error that should be returned to the caller. - -#### Decision Guide for Doc Examples - -| Scenario | Pattern | Example | -|----------|---------|---------| -| Teaching defensive game loop handling | `if let Some` | `if let Some(s) = cell.load() { state = s; }` | -| Demonstrating happy path with proven state | `.expect("why")` | `cell.data().expect("just saved")` | -| Teaching error propagation | `.ok_or()?` | `cell.load().ok_or(Error::Missing)?` | -| General fallible operations | `?` operator | `session.advance_frame()?` | - -### Documentation Example Verification (CRITICAL) - -**ALWAYS verify that types, methods, and error variants used in documentation examples actually exist in the source code.** Fabricated examples that don't compile erode trust and waste users' time. - -#### Before Writing Doc Examples - -```bash -# Verify error variants exist -rg 'enum FortressError' -A 100 src/error.rs | head -120 - -# Verify a specific variant exists -rg 'DesyncDetected|InvalidFrame|NetworkError' src/error.rs - -# Verify struct/method exists -rg 'pub fn method_name|pub struct TypeName' --type rust -``` - -#### Common Doc Example Mistakes - -```rust -// ❌ FORBIDDEN: Using non-existent error variants -/// ``` -/// match result { -/// Err(FortressError::DesyncDetected) => { /* ... */ } // Does this exist? -/// } -/// ``` - -// ❌ FORBIDDEN: Incomplete match on #[non_exhaustive] enums -/// ``` -/// match event { -/// FortressEvent::Synchronizing { total, count, .. } => { /* ... */ } -/// FortressEvent::Disconnected { .. } => { /* ... */ } -/// // Missing other variants AND missing `_ =>` fallback! -/// } -/// ``` - -// ✅ REQUIRED: Verify variants exist, handle exhaustiveness -/// ``` -/// match event { -/// FortressEvent::Synchronizing { total, count, .. } => { /* ... */ } -/// FortressEvent::Disconnected { addr, .. } => { /* ... */ } -/// FortressEvent::NetworkInterrupted { addr, .. } => { /* ... */ } -/// // ... all other variants ... -/// _ => { /* Handle future variants gracefully */ } -/// } -/// ``` -``` - -#### Matching on `#[non_exhaustive]` Enums in Examples - -When demonstrating match statements on `#[non_exhaustive]` enums (like `FortressEvent`), you **must** include a wildcard arm: - -```rust -// ✅ REQUIRED for #[non_exhaustive] enums in doc examples -/// ``` -/// for event in session.events()? { -/// match event { -/// FortressEvent::Synchronizing { total, count, .. } => { -/// println!("Sync progress: {count}/{total}"); -/// } -/// FortressEvent::Disconnected { addr, .. } => { -/// println!("Player at {addr} disconnected"); -/// } -/// _ => { -/// // Handle other/future event types -/// } -/// } -/// } -/// ``` -``` - -**Why this matters:** - -- `#[non_exhaustive]` means new variants may be added without a breaking change -- Examples without `_ =>` won't compile (teaching users broken patterns) -- The wildcard arm shows users how to future-proof their code - -#### Verification Checklist for Doc Examples - -Before committing documentation with code examples: - -- [ ] All error variants used actually exist in `FortressError` -- [ ] All struct/method names are spelled correctly and exist -- [ ] Match statements on `#[non_exhaustive]` enums include `_ =>` arm -- [ ] Examples compile: `cargo test --doc` -- [ ] Examples follow zero-panic policy (no `unwrap()` without justification) - ### Required Patterns -All fallible operations MUST return `Result`: - ```rust -// ✅ REQUIRED - Convert Option to Result -value.ok_or(FortressError::MissingValue)? -value.ok_or_else(|| FortressError::NotFound { key: key.clone() })? - -// ✅ REQUIRED - Safe indexing -array.get(index).ok_or(FortressError::IndexOutOfBounds { index, len: array.len() })? -slice.get(range).ok_or(FortressError::RangeOutOfBounds)? - -// ✅ REQUIRED - Explicit error returns -if !valid { - return Err(FortressError::InvalidState { reason: "precondition violated" }); -} - -// ✅ REQUIRED - Transform and propagate errors -operation().map_err(|e| FortressError::OperationFailed { cause: e.to_string() })? - -// ✅ REQUIRED - Checked arithmetic (instead of panicking overflow) -a.checked_add(b).ok_or(FortressError::ArithmeticOverflow)? -a.checked_sub(b).ok_or(FortressError::ArithmeticUnderflow)? -a.checked_mul(b).ok_or(FortressError::ArithmeticOverflow)? +value.ok_or(FortressError::MissingValue)?; +array.get(index).ok_or(FortressError::IndexOutOfBounds { index, len: array.len() })?; +a.checked_add(b).ok_or(FortressError::ArithmeticOverflow)?; +operation().map_err(|e| FortressError::OperationFailed { cause: e.to_string() })?; ``` -### When `unreachable!()` Is Acceptable +### Doc Examples Must Also Follow Zero-Panic -Only use `unreachable!()` when the type system GUARANTEES it cannot be reached: +Use `?` with `# Ok::<(), FortressError>(())` pattern in doc tests. -```rust -// ✅ OK - Type system guarantees this arm is unreachable -enum State { A, B } -match state { - State::A => handle_a(), - State::B => handle_b(), - // No wildcard needed - all variants covered -} +| Scenario | Pattern | +|----------|---------| +| Teaching defensive handling | `if let Some(s) = cell.load() { state = s; }` | +| Happy path with proven state | `.expect("just saved")` (with justification) | +| Error propagation | `.ok_or(Error::Missing)?` | +| General fallible ops | `?` operator | -// ✅ OK - After exhaustive validation that changes types -let positive: NonZeroU32 = match value { - 0 => return Err(Error::ZeroNotAllowed), - n => NonZeroU32::new(n).expect("n is non-zero"), // OK: proven by match -}; +### Doc Example Verification -// ❌ NOT OK - Runtime assumption, not type-guaranteed -match self.state { - State::Connected => { /* ... */ } - _ => unreachable!(), // State could be anything! -} -``` - ---- +Always verify error variants, struct names, method names exist in source. Match on `#[non_exhaustive]` enums must include `_ =>` arm. ## Never Swallow Errors -### Forbidden Error-Hiding Patterns - ```rust -// ❌ FORBIDDEN - Ignoring Result entirely +// FORBIDDEN let _ = fallible_operation(); -fallible_operation(); // Warning: unused Result +let value = operation().unwrap_or_default(); // hides WHY -// ❌ FORBIDDEN - Silent fallback on error -let value = operation().unwrap_or(default); // Hides WHY it failed -let value = operation().unwrap_or_default(); - -// ❌ FORBIDDEN - Conditional success, silent failure -if let Ok(value) = operation() { - use(value); -} -// What happens on Err? Nothing? That's a bug. - -// ❌ FORBIDDEN - Matching away errors -match result { - Ok(v) => v, - Err(_) => return, // Where did the error go? -} -``` - -### Required Error Handling Patterns - -```rust -// ✅ REQUIRED - Propagate with ? +// REQUIRED fallible_operation()?; - -// ✅ REQUIRED - Transform and propagate -fallible_operation() - .map_err(|e| FortressError::Wrapped { source: e })?; - -// ✅ REQUIRED - Handle OR propagate, never ignore match fallible_operation() { Ok(value) => process(value), - Err(e) => { - // Either handle it meaningfully... - log::warn!("Operation failed: {e}, using fallback"); - use_fallback() - // ...OR propagate it - // return Err(e.into()); - } + Err(Error::NotFound) => DEFAULT, // explicitly acceptable + Err(e) => return Err(e.into()), } - -// ✅ REQUIRED - If using unwrap_or, document WHY it's safe -let value = operation() - .unwrap_or(DEFAULT); // OK only if DEFAULT is semantically correct for ALL errors - -// ✅ BETTER - Be explicit about acceptable errors -let value = match operation() { - Ok(v) => v, - Err(Error::NotFound) => DEFAULT, // Explicitly acceptable - Err(e) => return Err(e.into()), // Other errors propagate -}; ``` ---- - -## Validate Everything, Assume Nothing - -### Input Validation +## Input Validation All public APIs must validate inputs at the boundary: ```rust -// ❌ Avoid: Trusts caller -pub fn set_player_count(&mut self, count: usize) { - self.players = vec![Player::default(); count]; -} - -// ✅ Prefer: Validates at boundary pub fn set_player_count(&mut self, count: usize) -> Result<(), FortressError> { - if count == 0 { - return Err(FortressError::InvalidPlayerCount { - count, - reason: "must have at least one player", - }); - } - if count > MAX_PLAYERS { - return Err(FortressError::InvalidPlayerCount { - count, - reason: "exceeds maximum player limit", - }); + if count == 0 || count > MAX_PLAYERS { + return Err(FortressError::InvalidPlayerCount { count, reason: "out of range" }); } self.players = vec![Player::default(); count]; Ok(()) } ``` -### Internal State Validation - -Don't assume internal state is valid — verify before use: - -```rust -// ❌ Avoid: Assumes index is valid -fn current_player(&self) -> &Player { - &self.players[self.current_player_index] -} - -// ✅ Prefer: Returns Result, validates state -fn current_player(&self) -> Result<&Player, FortressError> { - self.players - .get(self.current_player_index) - .ok_or(FortressError::InvalidPlayerIndex { - index: self.current_player_index, - count: self.players.len(), - }) -} - -// ✅ Alternative: Debug assertion + safe access (for internal hot paths) -fn current_player(&self) -> Option<&Player> { - debug_assert!( - self.current_player_index < self.players.len(), - "invariant violated: index {} >= len {}", - self.current_player_index, - self.players.len() - ); - self.players.get(self.current_player_index) -} -``` - ---- - -## Maintain Invariants - -### State Consistency - -Operations must either succeed completely or leave state unchanged: - -```rust -// ❌ Avoid: Partial update on failure leaves inconsistent state -fn add_player(&mut self, player: Player) -> Result<(), Error> { - self.count += 1; // Updated - self.players.push(player); // What if this fails? - self.update_network()?; // Now count is wrong if this fails! - Ok(()) -} - -// ✅ Prefer: Prepare, then commit atomically -fn add_player(&mut self, player: Player) -> Result<(), Error> { - // Validate first - if self.count >= MAX_PLAYERS { - return Err(Error::TooManyPlayers); - } - - // Prepare the update (may fail) - self.prepare_network_update(&player)?; - - // Commit atomically (infallible operations only) - self.players.push(player); - self.count += 1; - Ok(()) -} - -// ✅ Alternative: Rollback on failure -fn add_player(&mut self, player: Player) -> Result<(), Error> { - self.players.push(player); - self.count += 1; - - if let Err(e) = self.update_network() { - // Rollback - self.players.pop(); - self.count -= 1; - return Err(e); - } - Ok(()) -} -``` - -### RAII for Cleanup - -Use Drop traits and guards for cleanup that MUST happen: - -```rust -// ✅ RAII guard ensures cleanup -struct ConnectionGuard<'a> { - session: &'a mut Session, - connection_id: ConnectionId, -} - -impl Drop for ConnectionGuard<'_> { - fn drop(&mut self) { - // Cleanup runs even if the operation panics (in tests) - // or returns early with an error - self.session.release_connection(self.connection_id); - } -} - -fn use_connection(session: &mut Session) -> Result<(), Error> { - let id = session.acquire_connection()?; - let _guard = ConnectionGuard { session, connection_id: id }; - - // If any of this fails, guard ensures cleanup - do_something()?; - do_another_thing()?; - - Ok(()) - // Guard dropped here, releasing connection -} -``` - ---- - -## Type-Safe API Design - -### Make Invalid States Unrepresentable - -```rust -// ❌ Avoid: Runtime validation needed -struct Session { - players: Vec, // Could be empty! - frame: i32, // Could be negative! -} - -impl Session { - fn current_frame(&self) -> Result { - if self.frame < 0 { - return Err(Error::InvalidFrame); - } - Ok(self.frame as u32) - } -} +## State Consistency -// ✅ Prefer: Invalid states impossible by construction -struct Session { - players: NonEmpty, // At least one player - frame: Frame, // Newtype wrapper, always valid -} +Operations must succeed completely or leave state unchanged (prepare-then-commit or rollback). -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct Frame(u32); // Can never be negative - -impl Frame { - pub const ZERO: Frame = Frame(0); - - pub fn new(value: u32) -> Self { - Frame(value) - } - - pub fn checked_add(self, delta: u32) -> Option { - self.0.checked_add(delta).map(Frame) - } -} -``` +## Error Categorization -### Use Enums Over Booleans +| Question | Category | +|----------|----------| +| Is invalid value from caller's argument? | `InvalidRequestStructured` | +| Is it from internal library state? | `InternalErrorStructured` | -```rust -// ❌ Avoid: What does true mean? -fn connect(addr: SocketAddr, encrypted: bool, compressed: bool) { } -connect(addr, true, false); // Unclear +**Quick test:** Can a user following docs correctly trigger this? YES -> `InvalidRequest`. NO -> `InternalError`. -// ✅ Prefer: Self-documenting -enum Encryption { Enabled, Disabled } -enum Compression { Enabled, Disabled } +### Unknown Fallback Variants -fn connect(addr: SocketAddr, encryption: Encryption, compression: Compression) { } -connect(addr, Encryption::Enabled, Compression::Disabled); // Crystal clear -``` +Include `Unknown` variant in error reason enums for safe fallback in mapping functions. Never use existing variants with placeholder values. -### Phantom Types for State Machines +## Safe Collection Access ```rust -// ✅ Compile-time state machine validation -struct Connection { - inner: ConnectionInner, - _state: PhantomData, -} - -trait ConnectionState {} -struct Disconnected; -struct Connecting; -struct Connected; - -impl ConnectionState for Disconnected {} -impl ConnectionState for Connecting {} -impl ConnectionState for Connected {} - -impl Connection { - fn connect(self) -> Connection { - // ... initiate connection - Connection { inner: self.inner, _state: PhantomData } - } -} - -impl Connection { - fn wait_connected(self) -> Result, Error> { - // ... wait for connection - Ok(Connection { inner: self.inner, _state: PhantomData }) - } -} - -impl Connection { - fn send(&mut self, data: &[u8]) -> Result<(), Error> { - // Only available when connected! - Ok(()) - } -} -``` +// Prefer iterators over indexing +for item in &items { process(item); } ---- - -## Safe Collection Access Patterns - -### Prefer Iterators Over Indexing - -```rust -// ❌ Avoid: Index-based loops -for i in 0..items.len() { - process(&items[i]); // Can panic if items modified -} - -// ✅ Prefer: Iterator-based -for item in &items { - process(item); -} - -// ✅ Prefer: With index if needed -for (i, item) in items.iter().enumerate() { - process_with_index(i, item); -} -``` - -### Pattern Matching for Collection Access - -```rust -// ❌ Avoid: Can panic -let first = &items[0]; -let last = &items[items.len() - 1]; - -// ✅ Prefer: Pattern matching -let first = items.first().ok_or(Error::EmptyCollection)?; -let last = items.last().ok_or(Error::EmptyCollection)?; - -// ✅ Prefer: Destructuring +// Pattern matching for first/last +let first = items.first().ok_or(Error::Empty)?; match items.as_slice() { - [] => return Err(Error::EmptyCollection), + [] => Err(Error::Empty), [only] => process_single(only), - [first, .., last] => process_range(first, last), -} - -// ✅ Prefer: Split operations -let (head, tail) = items.split_first().ok_or(Error::EmptyCollection)?; -``` - ---- - -## Error Design Guidelines - -### Rich Error Types - -```rust -// ✅ Good error design -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FortressError { - /// Player index is out of bounds - InvalidPlayerIndex { - index: usize, - player_count: usize, - }, - - /// Frame number is invalid for the current state - InvalidFrame { - requested: Frame, - current: Frame, - reason: &'static str, - }, - - /// Network operation failed - NetworkError { - operation: &'static str, - details: String, - }, - - /// Internal invariant violated (indicates a bug) - InvariantViolation { - invariant: &'static str, - context: String, - }, -} -``` - -### Error Context - -Always provide enough context to debug: - -```rust -// ❌ Avoid: No context -return Err(Error::InvalidIndex); - -// ✅ Prefer: Full context -return Err(FortressError::InvalidPlayerIndex { - index: player_idx, - player_count: self.players.len(), -}); -``` - -### Error Categorization: `InvalidRequest*` vs `InternalError*` - -Choosing the correct error category is critical for debugging and API contracts. - -**`InvalidRequestStructured` / `InvalidRequest`**: Use for **caller-provided invalid arguments**. -The caller made a mistake; this is expected and recoverable. - -**`InternalErrorStructured` / `InternalError`**: Use for **library bugs or invariant violations**. -Something went wrong inside the library that should never happen under normal API usage. - -**Decision tree:** - -``` -Is the invalid value provided by the caller as an argument? -├─ YES → InvalidRequestStructured (caller's responsibility) -└─ NO → Is it derived from internal library state? - ├─ YES → InternalErrorStructured (library bug) - └─ NO → Trace back: who created this value? - └─ Usually leads to one of the above -``` - -**Example: Division by zero** - -```rust -// Function signature: pub fn try_buffer_index(&self, buffer_size: usize) -> Result<...> - -// ❌ WRONG: buffer_size == 0 is caller's fault, not a library bug -if buffer_size == 0 { - return Err(FortressError::InternalErrorStructured { - kind: InternalErrorKind::DivisionByZero, // Wrong category! - }); -} - -// ✅ CORRECT: Caller passed invalid argument -if buffer_size == 0 { - return Err(FortressError::InvalidRequestStructured { - kind: InvalidRequestKind::ZeroBufferSize, - }); -} -``` - -**Why this matters:** - -- `InternalError` tells users: "Report this as a bug" — wrong if it's their fault -- `InvalidRequest` tells users: "Fix your input" — actionable guidance -- Incorrect categorization erodes trust and wastes debugging time - -**Quick test:** Ask "If a user follows the documented API correctly, can they ever trigger this error?" - -- **NO** (impossible with correct usage) → `InternalError` (indicates library bug) -- **YES** (possible with incorrect arguments) → `InvalidRequest` (user error) - -### Unknown/Fallback Variants in Error Reason Enums - -When creating "reason" enums for structured errors, include an `Unknown` or fallback variant -for cases where error mapping might not have complete information. - -```rust -// ❌ Avoid: No fallback for unexpected cases -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[non_exhaustive] -pub enum DeltaDecodeReason { - EmptyReference, - DataLengthMismatch { data_len: usize, reference_len: usize }, - ReferenceIndexOutOfBounds { index: usize, length: usize }, - DataIndexOutOfBounds { index: usize, length: usize }, - // What happens if we need to map an unexpected error type? -} - -// ✅ Prefer: Explicit Unknown variant for fallback cases -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[non_exhaustive] -pub enum RleDecodeReason { - BitfieldIndexOutOfBounds, - DestinationSliceOutOfBounds, - SourceSliceOutOfBounds, - TruncatedData { offset: usize, buffer_len: usize }, - /// An unknown or unexpected error occurred. - /// - /// This variant is used as a fallback when the underlying error cannot be - /// mapped to a more specific reason (e.g., when downcasting fails). - Unknown, -} -``` - -**Why `Unknown` is important:** - -1. **Error mapping functions** need a fallback when source errors don't match expected patterns -2. **Non-exhaustive enums** may have new variants added; code mapping them needs safety valves -3. **Defensive error handling** shouldn't panic when encountering unexpected error types - -**Anti-pattern: Misleading fallback values** - -Never use an existing variant with nonsensical placeholder values as a fallback — it misleads -debugging and masks the true cause: - -```rust -// ❌ FORBIDDEN: Misleading placeholder values -fn map_error(error: &FortressError) -> RleDecodeReason { - match error { - // ... specific mappings ... - _ => RleDecodeReason::TruncatedData { offset: 0, buffer_len: 0 }, // MISLEADING! - // This looks like a real truncation error but contains no useful info - } -} - -// ✅ REQUIRED: Explicit Unknown variant -fn map_error(error: &FortressError) -> RleDecodeReason { - match error { - // ... specific mappings ... - _ => RleDecodeReason::Unknown, // Honest about not knowing the cause - } -} -``` - -```rust -// ✅ Usage: Error mapping with Unknown fallback -fn map_rle_error_to_reason(error: &FortressError) -> RleDecodeReason { - match error { - FortressError::InternalErrorStructured { - kind: InternalErrorKind::RleDecodeError { reason }, - } => *reason, - _ => RleDecodeReason::Unknown, // Safe fallback for unexpected types - } + [first, rest @ ..] => { /* guaranteed safe */ } } ``` -**When to add Unknown:** - -- Error reason enums used in error mapping/conversion functions -- Enums that might receive values from external sources (deserialization, FFI) -- Enums marked `#[non_exhaustive]` where exhaustive matching isn't possible - -**When Unknown may not be needed:** - -- Internal enums where all variants are explicitly constructed in known code paths -- Enums where an existing `Custom(&'static str)` variant serves as the fallback - ---- - -## Advanced Defensive Patterns +## Advanced Patterns -### Use `TryFrom` Instead of `From` for Fallible Conversions +### `TryFrom` over `From` for Fallible Conversions -`From` implementations must never panic. If a conversion can fail, use `TryFrom`: - -```rust -// ❌ FORBIDDEN - From that panics violates zero-panic policy -impl From for Frame { - fn from(value: i32) -> Self { - if value < 0 { - panic!("Frame cannot be negative"); // NEVER DO THIS - } - Frame(value as u32) - } -} - -// ✅ REQUIRED - TryFrom for fallible conversions -impl TryFrom for Frame { - type Error = FortressError; - - fn try_from(value: i32) -> Result { - if value < 0 { - return Err(FortressError::InvalidFrame { - reason: "frame cannot be negative", - }); - } - Ok(Frame(value as u32)) - } -} -``` +`From` must never panic. Use `TryFrom` if conversion can fail. ### Safe Numeric Conversions -Never use `as` for numeric conversions that can truncate or lose sign: - -```rust -// ❌ FORBIDDEN - Silent truncation/overflow -let small: i8 = big_number as i8; -let unsigned: u32 = signed_value as u32; - -// ✅ REQUIRED - Explicit conversion with error handling -let small = i8::try_from(big_number) - .map_err(|_| FortressError::NumericOverflow { value: big_number })?; - -// ✅ OK - Infallible widening conversions -let big: i64 = small_number.into(); -let wider: u64 = u32::from(narrow); -``` - -### Avoid `..Default::default()` — Use Explicit Field Initialization - -Using `..Default::default()` hides new fields when structs evolve: - -```rust -// ❌ Avoid: New fields silently get defaults -struct SessionConfig { - max_prediction_frames: usize, - input_delay: usize, - disconnect_timeout_ms: u64, // Added later - silently becomes 0! -} - -let config = SessionConfig { - max_prediction_frames: 8, - input_delay: 2, - ..Default::default() // Hides disconnect_timeout_ms -}; - -// ✅ Prefer: Explicit initialization — compiler errors on new fields -let config = SessionConfig { - max_prediction_frames: 8, - input_delay: 2, - disconnect_timeout_ms: 5000, -}; - -// ✅ Alternative: Destructure-then-override (when defaults are appropriate) -let SessionConfig { - max_prediction_frames: _, - input_delay: _, - disconnect_timeout_ms: _, // Compiler errors if field added -} = SessionConfig::default(); +Never `as` for lossy conversions. Use `i8::try_from(big)?.into()` or infallible widening `.into()`. -let config = SessionConfig { - max_prediction_frames: 8, - input_delay: 2, - ..SessionConfig::default() // Now safe - all fields acknowledged -}; -``` +### Avoid `..Default::default()` -### Exhaustive Destructuring in Trait Implementations +New fields silently get defaults. Prefer explicit field initialization or destructure-then-override. -When implementing `PartialEq`, `Hash`, `Debug`, etc., destructure to catch new fields: +### Exhaustive Destructuring in Trait Impls ```rust -// ❌ Avoid: New fields silently excluded from comparison -impl PartialEq for PlayerState { - fn eq(&self, other: &Self) -> bool { - self.frame == other.frame && self.input == other.input - // prediction_count added later - silently ignored! - } -} - -// ✅ Prefer: Destructure forces handling all fields impl PartialEq for PlayerState { fn eq(&self, other: &Self) -> bool { let Self { frame, input, checksum } = self; - let Self { - frame: other_frame, - input: other_input, - checksum: other_checksum, - } = other; - // Adding a field causes compile error here - - frame == other_frame && input == other_input && checksum == other_checksum - } -} -``` - -### Named Placeholders for Ignored Fields - -Use `field_name: _` instead of just `_` or `..`: - -```rust -// ❌ Avoid: No warning if field removed -let NetworkMessage { sequence, payload, .. } = msg; - -// ✅ Prefer: Explicit acknowledgment of ignored fields -let NetworkMessage { - sequence, - payload, - timestamp: _, // Compiler error if timestamp removed -} = msg; -``` - -### Defensive Constructors - -Prevent invalid construction with private fields or `#[non_exhaustive]`: - -```rust -// ❌ Avoid: Public fields allow invalid construction -pub struct PlayerHandle { - pub index: usize, // Anyone can create PlayerHandle { index: 999 } -} - -// ✅ Prefer: Private fields + validated constructor -pub struct PlayerHandle { - index: usize, - _private: (), // Prevents construction outside module -} - -impl PlayerHandle { - pub fn new(index: usize, session: &Session) -> Result { - if index >= session.player_count() { - return Err(FortressError::InvalidPlayerIndex { - index, - count: session.player_count(), - }); - } - Ok(Self { index, _private: () }) - } - - pub fn index(&self) -> usize { - self.index + // Adding a field causes a compile error here, forcing you to update + *frame == other.frame && *input == other.input && *checksum == other.checksum } } - -// ✅ For library enums that may grow: #[non_exhaustive] -#[non_exhaustive] -pub enum SessionEvent { - PlayerJoined { player: PlayerHandle }, - PlayerLeft { player: PlayerHandle }, - // New variants won't break downstream matches -} ``` ### `#[must_use]` on Important Types -Prevent accidental ignoring of critical values: - ```rust -// ✅ Force callers to handle important return values #[must_use = "frame advance result contains requests that must be processed"] -pub struct FrameAdvanceResult { - pub requests: Vec, - pub skip_frame: bool, -} - -// ✅ On methods returning important values -impl Session { - #[must_use] - pub fn advance_frame(&mut self) -> FrameAdvanceResult { - // ... - } -} - -// ✅ On builders -#[must_use = "builders do nothing until .build() is called"] -pub struct SessionBuilder { /* ... */ } -``` - -### Temporary Mutability Pattern - -Shadow to freeze values after initialization: - -```rust -// ✅ Prevent accidental mutation after setup -fn build_config() -> SessionConfig { - let mut config = SessionConfig::default(); - config.max_prediction_frames = 8; - config.input_delay = calculate_delay(); - - let config = config; // Shadow: now immutable - // config.input_delay = 0; // Compile error! - - validate(&config); - config -} - -// ✅ Scope block variant for complex initialization -let config = { - let mut config = SessionConfig::default(); - let temp = compute_settings(); - config.apply_settings(temp); - config // Returned immutable; temp not accessible -}; -``` - -### Parameter Structs for Many Options - -Replace multiple parameters with a configuration struct: - -```rust -// ❌ Avoid: Easy to mix up parameter order -fn start_session( - num_players: usize, - input_delay: usize, - max_prediction: usize, - disconnect_timeout: Duration, -) -> Result { /* ... */ } - -start_session(4, 2, 8, Duration::from_secs(5))?; // Which is which? - -// ✅ Prefer: Self-documenting struct -pub struct SessionConfig { - pub num_players: usize, - pub input_delay: usize, - pub max_prediction: usize, - pub disconnect_timeout: Duration, -} - -fn start_session(config: SessionConfig) -> Result { /* ... */ } - -start_session(SessionConfig { - num_players: 4, - input_delay: 2, - max_prediction: 8, - disconnect_timeout: Duration::from_secs(5), -})?; -``` - -### Enhanced Slice Pattern Matching - -Use full slice patterns instead of check-then-index: - -```rust -// ❌ Avoid: Check and index are separate operations -fn process_inputs(inputs: &[Input]) -> Result<(), Error> { - if inputs.is_empty() { - return Err(Error::NoInputs); - } - let first = inputs[0]; // Decoupled from check - let rest = &inputs[1..]; // Can panic with 1 element - // ... -} - -// ✅ Prefer: Compiler-enforced patterns -fn process_inputs(inputs: &[Input]) -> Result<(), Error> { - match inputs { - [] => Err(Error::NoInputs), - [single] => process_single(single), - [first, second] => process_pair(first, second), - [first, rest @ ..] => { - // first: &Input, rest: &[Input] - guaranteed safe - process_first(first); - for item in rest { - process_rest(item)?; - } - Ok(()) - } - } -} +pub struct FrameAdvanceResult { /* ... */ } ``` -### Safe Debug for Sensitive Data +### Temporary Mutability -Redact sensitive fields in Debug implementations: +Shadow to freeze: `let config = config;` after setup. -```rust -// ✅ REQUIRED - Destructure to catch new sensitive fields -impl std::fmt::Debug for Credentials { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { username, password: _, api_key: _ } = self; - f.debug_struct("Credentials") - .field("username", username) - .field("password", &"[REDACTED]") - .field("api_key", &"[REDACTED]") - .finish() - } -} -``` +### Parameter Structs ---- +Replace many params with a config struct for self-documenting call sites. ## Recommended Clippy Lints -Enable these lints for automated enforcement: - -```rust -// In lib.rs or main.rs -#![deny( - clippy::unwrap_used, - clippy::expect_used, - clippy::panic, - clippy::indexing_slicing, - clippy::arithmetic_side_effects, - clippy::cast_possible_truncation, - clippy::cast_sign_loss, - clippy::fallible_impl_from, -)] - -#![warn( - clippy::must_use_candidate, - clippy::return_self_not_must_use, - clippy::wildcard_enum_match_arm, - clippy::default_trait_access, -)] -``` - -Or in `Cargo.toml`: - ```toml [lints.clippy] unwrap_used = "deny" @@ -1190,31 +152,17 @@ cast_possible_truncation = "deny" cast_sign_loss = "deny" fallible_impl_from = "deny" must_use_candidate = "warn" -return_self_not_must_use = "warn" -wildcard_enum_match_arm = "warn" ``` ---- - -## Summary Checklist - -Before committing any production code, verify: +## Checklist -- [ ] No `unwrap()`, `expect()`, `panic!()`, `todo!()`, `unimplemented!()` -- [ ] No direct index access `[]` — use `.get()` with error handling -- [ ] No `as` for lossy numeric conversions — use `TryFrom` -- [ ] All `Result` values are handled (not ignored with `let _ =`) -- [ ] All public functions validate inputs at the boundary -- [ ] State changes are atomic or rolled back on failure -- [ ] Error types provide sufficient context for debugging -- [ ] Types make invalid states unrepresentable where possible -- [ ] Assertions only in test code, not production paths -- [ ] No `..Default::default()` without explicit field acknowledgment +- [ ] No `unwrap()`, `expect()`, `panic!()`, `todo!()` +- [ ] No direct `[]` indexing -- use `.get()` with error handling +- [ ] No `as` for lossy numeric conversions +- [ ] All `Result` values handled +- [ ] Public functions validate inputs +- [ ] State changes atomic or rolled back +- [ ] Error types provide context +- [ ] No `..Default::default()` without field acknowledgment - [ ] Custom trait impls use exhaustive destructuring - [ ] `#[must_use]` on important return types -- [ ] Sensitive data redacted in `Debug` implementations -- [ ] `cargo doc --no-deps` passes — no broken intra-doc links - ---- - -*This policy applies to all code: library, examples, tools, and editor integrations.* diff --git a/.llm/skills/dependency-management.md b/.llm/skills/dependency-management.md index 3e04b1a3..63e7c640 100644 --- a/.llm/skills/dependency-management.md +++ b/.llm/skills/dependency-management.md @@ -1,477 +1,134 @@ -# Dependency Management — Sustainable Rust Crate Dependencies + + -> **This document provides comprehensive guidance for managing dependencies in Rust crates.** -> Every dependency is a liability, not an asset. Choose wisely, update regularly, and minimize where possible. +# Dependency Management -## TL;DR — Quick Reference - -```bash -# Dependency analysis -cargo tree # View full dependency graph -cargo tree -d # Find duplicate dependencies -cargo tree -f "{p} {f}" # Show features per package -cargo tree -i some_crate # Why is this crate included? - -# Maintenance tools -cargo install cargo-udeps cargo-outdated cargo-audit cargo-deny -cargo +nightly udeps # Find unused dependencies -cargo outdated # Check for newer versions -cargo update # Update to latest compatible versions - -# Security -cargo audit # Check RustSec advisories -cargo deny check # Comprehensive dependency checks -``` - -**Key Principles:** - -1. **Dependencies are liabilities** — Each adds compile time, complexity, and attack surface -2. **Prefer std** — Use standard library before reaching for crates -3. **Never pin exact versions** — Leads to outdated, vulnerable dependencies -4. **Update regularly** — Run `cargo update` frequently, automate with Dependabot -5. **Audit security** — Run `cargo audit` in CI on every build +Every dependency is a liability: compile time, binary size, attack surface, maintenance burden. --- -## Dependencies as Liabilities - -### The True Cost of Dependencies +## Evaluation Checklist -Every dependency you add comes with hidden costs: +Before adding any dependency: -| Cost | Impact | -|------|--------| -| **Compile time** | Each crate adds to build time, especially with proc-macros | -| **Binary size** | More code means larger binaries | -| **Complexity** | More code to understand and debug | -| **Documentation** | Users must understand dep interactions | -| **Attack surface** | More code means more potential vulnerabilities | -| **Maintenance** | You inherit the dep's maintenance burden | -| **Breaking changes** | API changes can break your code | +- [ ] Can this be done with std in <100 lines? +- [ ] Does the crate have >1M downloads? +- [ ] Is the license compatible (MIT/Apache-2.0)? +- [ ] Updated in last 6 months? +- [ ] Is it 1.0+ (or stable despite <1.0)? +- [ ] What transitive deps does it pull in? +- [ ] No RustSec advisories against it? +- [ ] From a known/trusted author or organization? -```rust -// ❌ ANTI-PATTERN: Adding a dependency for trivial functionality -// Cargo.toml: is-even = "1.0" - -fn check_even(n: u32) -> bool { - is_even::is_even(n) // Pulled in a whole crate for this? -} - -// ✅ CORRECT: Write trivial functionality yourself -fn check_even(n: u32) -> bool { - n % 2 == 0 -} -``` +### When Dependencies ARE Worth It -### When Dependencies Make Sense - -Dependencies ARE worth it when: - -- **Complex domain** — Cryptography, compression, parsing formats -- **Battle-tested** — Widely used, well-audited code (e.g., `serde`, `tokio`) -- **Expertise gap** — You lack domain expertise to implement correctly -- **Time-critical** — Business needs outweigh long-term maintenance cost -- **Security-sensitive** — Professional implementations of crypto, auth, etc. - ---- - -## Evaluating Dependencies - -### Decision Framework - -Before adding any dependency, answer these questions: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ DEPENDENCY EVALUATION CHECKLIST │ -├─────────────────────────────────────────────────────────────────┤ -│ □ Can this be done with std? │ -│ □ Is the functionality complex enough to justify a dep? │ -│ □ Does the crate have > 1 million downloads? │ -│ □ Is the license compatible (MIT/Apache-2.0)? │ -│ □ Has it been updated in the last 6 months? │ -│ □ Are issues being addressed? │ -│ □ Is there a RustSec advisory against it? │ -│ □ Is it from a known/trusted author or organization? │ -│ □ Is it version 1.0+ (or stable despite < 1.0)? │ -│ □ What transitive dependencies does it pull in? │ -└─────────────────────────────────────────────────────────────────┘ -``` +Complex domains (crypto, compression, parsing), battle-tested code (serde, tokio), security-sensitive implementations, expertise gaps. ### Research Resources -| Resource | URL | Use Case | -|----------|-----|----------| -| **blessed.rs** | | Curated list of recommended crates | -| **lib.rs** | | Better crate discovery than crates.io | -| **crates.io** | | Official registry, check downloads/versions | -| **RustSec** | | Security advisories database | -| **docs.rs** | | Documentation quality check | - -### Checking Crate Health - -```bash -# Check downloads and recent versions on crates.io -# Look for: consistent releases, growing downloads - -# Check GitHub/GitLab repository -# Look for: recent commits, issues being addressed, PR activity - -# Check transitive dependencies -cargo tree -i suspect_crate - -# Example output showing why rand is included: -# rand v0.8.5 -# ├── my_crate v0.1.0 -# │ └── some_dep v1.2.3 -# └── another_dep v2.0.0 -``` - -### License Compatibility - -```rust -// ✅ SAFE - Permissive licenses, compatible with any project -// MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Zlib - -// ⚠️ CAUTION - Copyleft licenses, may have implications -// MPL-2.0 (file-level copyleft) -// LGPL-2.1, LGPL-3.0 (library copyleft) - -// ❌ AVOID - Strong copyleft, makes your entire crate copyleft -// GPL-2.0, GPL-3.0, AGPL-3.0 -``` +| Resource | Purpose | +|----------|---------| +| [blessed.rs](https://blessed.rs) | Curated recommended crates | +| [lib.rs](https://lib.rs) | Better crate discovery | +| [RustSec](https://rustsec.org) | Security advisories | --- -## Limiting Dependencies - -### Prefer Standard Library - -The Rust standard library provides many utilities people reach for crates to get: - -```rust -// ❌ ANTI-PATTERN: Using external crates for std functionality - -// itertools for basic iteration? std has most of it -use itertools::Itertools; -vec.into_iter().unique().collect(); - -// ✅ CORRECT: Use std where possible -use std::collections::HashSet; -let unique: HashSet<_> = vec.into_iter().collect(); -let unique: Vec<_> = unique.into_iter().collect(); - -// ❌ ANTI-PATTERN: External crate for HashMap -use hashbrown::HashMap; - -// ✅ CORRECT: std HashMap (backed by hashbrown anyway since Rust 1.36) -use std::collections::HashMap; - -// ❌ ANTI-PATTERN: rand for simple cases where not needed -use rand::random; -let id = random::(); - -// ✅ CORRECT: If truly random not needed, use deterministic approach -// Or if randomness IS needed, rand is a reasonable choice -``` - -### Standard Library Collections - -`std::collections` provides: - -- `Vec` — Growable array -- `VecDeque` — Double-ended queue -- `LinkedList` — Doubly-linked list -- `HashMap` — Hash table -- `BTreeMap` — Sorted map -- `HashSet` — Hash set -- `BTreeSet` — Sorted set -- `BinaryHeap` — Priority queue - -### Finding Unused Dependencies +## Analysis Commands ```bash -# Install cargo-udeps (requires nightly) -cargo install cargo-udeps - -# Find unused dependencies -cargo +nightly udeps - -# Example output: -# unused dependencies: -# `my_crate v0.1.0` -# └── serde_json -# └── deprecated_crate +cargo tree # Full dependency graph +cargo tree -d # Duplicate dependencies +cargo tree -f "{p} {f}" # Show features per package +cargo tree -i some_crate # Why is this crate included? +cargo tree -e features # Feature graph +cargo +nightly udeps # Unused dependencies +cargo outdated # Newer versions available ``` --- ## Version Management -### Never Pin Exact Versions - ```toml -# ❌ ANTI-PATTERN: Pinning exact versions -[dependencies] -serde = "=1.0.152" # Locked forever, misses security fixes -tokio = "=1.25.0" # Can't get bug fixes -rand = "=0.8.5" # Stuck on old version +# Use semver ranges -- NEVER pin exact versions +serde = "1.0" # Gets 1.0.x patches +tokio = "1" # Gets 1.x.x updates -# ✅ CORRECT: Use semver ranges -[dependencies] -serde = "1.0" # Gets 1.0.x patches automatically -tokio = "1" # Gets 1.x.x updates -rand = "0.8" # Gets 0.8.x updates - -# ✅ ACCEPTABLE: Minor version constraint when needed -[dependencies] -some_crate = "1.5" # Requires features from 1.5, gets 1.5.x patches +# AVOID +serde = "=1.0.152" # Locked, misses security fixes ``` -### Regular Updates +Pre-1.0 crates (`0.x.y`) can break in minor versions. Prefer 1.0+ when available. ```bash -# Update to latest compatible versions (respects Cargo.toml constraints) -cargo update - -# Check for outdated dependencies -cargo install cargo-outdated -cargo outdated - -# Example output: -# Name Project Compat Latest Kind -# ---- ------- ------ ------ ---- -# serde 1.0.152 1.0.193 1.0.193 Normal -# tokio 1.25.0 1.35.1 1.35.1 Normal -# old_crate 0.5.0 0.5.0 1.0.0 Normal ← Major update available -``` - -### Automated Updates - -Configure Dependabot in `.github/dependabot.yml`: - -```yaml -version: 2 -updates: - - package-ecosystem: "cargo" - directory: "/" - schedule: - interval: "weekly" - # Group minor/patch updates to reduce PR noise - groups: - rust-dependencies: - patterns: - - "*" - update-types: - - "minor" - - "patch" - # Limit open PRs - open-pull-requests-limit: 10 - # Add reviewers - reviewers: - - "your-team" - # Labels for filtering - labels: - - "dependencies" - - "rust" -``` - -### Handling Major Version Updates - -```bash -# Don't skip major versions - harder to upgrade later -# If you're on 1.x and 3.0 is out, upgrade to 2.0 first - -# Check CHANGELOG for breaking changes -# Test thoroughly after major updates - -# Example migration workflow: -cargo update -p some_crate --precise 2.0.0 # Update specific crate -cargo test # Run full test suite -cargo clippy # Check for new warnings -``` - ---- - -## Stability Considerations - -### Pre-1.0 vs 1.0+ Crates - -```toml -# 1.0+ crates follow semver strictly: -# - 1.0.x → patch releases, bug fixes only -# - 1.x.0 → minor releases, new features, backward compatible -# - x.0.0 → major releases, breaking changes allowed - -# Pre-1.0 crates (0.x.y) can break in MINOR versions: -# - 0.1.x → patch releases -# - 0.x.0 → can contain breaking changes! - -[dependencies] -# ✅ Stable - follows semver -serde = "1.0" # Breaking changes only in 2.0 -tokio = "1" # Stable since 1.0 - -# ⚠️ Less stable - breaking changes possible in 0.x -rand = "0.8" # Well-maintained but technically pre-1.0 -hyper = "0.14" # Stable in practice despite version -``` - -### Evaluating Real Stability - -Version number isn't everything. Consider: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ STABILITY EVALUATION │ -├─────────────────────────────────────────────────────────────────┤ -│ Positive Signals: │ -│ • Frequent maintenance commits │ -│ • Active issue triage │ -│ • Clear deprecation policy │ -│ • Good documentation │ -│ • Used by major projects │ -│ • Backed by organization (not just individual) │ -│ │ -│ Warning Signs: │ -│ • No commits in 1+ year │ -│ • Issues piling up without response │ -│ • Frequent breaking changes │ -│ • Single maintainer with no succession plan │ -│ • Deprecated without replacement │ -└─────────────────────────────────────────────────────────────────┘ +cargo update # Update all compatible +cargo update -p crate_name # Update specific crate +cargo update -p crate_name --precise X # Update to specific version ``` --- ## Feature Management -### Disable Unnecessary Features - ```toml -# ❌ ANTI-PATTERN: Using default features when you don't need them -[dependencies] -tokio = "1" # Pulls in ALL default features -reqwest = "0.11" # Includes default TLS, cookies, etc. - -# ✅ CORRECT: Disable defaults, add only what you need -[dependencies] +# Disable unnecessary defaults tokio = { version = "1", default-features = false, features = ["rt", "net"] } -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } - -# Example: serde without derive macro (if not needed) -serde = { version = "1.0", default-features = false } - -# Example: serde with derive (most common) -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", default-features = false, features = ["derive"] } ``` -### Auditing Features - ```bash -# See what features are enabled for each dependency +# Audit enabled features cargo tree -f "{p} {f}" - -# Example output: -# my_crate v0.1.0 (features: []) -# ├── serde v1.0.193 (features: [derive, std]) -# ├── tokio v1.35.1 (features: [rt-multi-thread, net, sync, macros]) -# │ ├── tokio-macros v2.2.0 (features: []) -# │ └── ... - -# Find why a feature is enabled -cargo tree -f "{p} {f}" -i tokio - -# Check if a feature can be disabled -# Try building without it and see what breaks +cargo tree -f "{p} {f}" -i tokio # Why is a feature enabled? ``` ### Feature Propagation ```toml -# Your crate can expose features that control dependencies [features] default = ["std"] std = ["serde/std", "dep-crate/std"] -serde = ["dep:serde"] # Optional dependency via feature +serde = ["dep:serde"] # Optional via feature [dependencies] serde = { version = "1.0", optional = true } -dep-crate = { version = "1.0", default-features = false } ``` --- ## Security -### Cargo Audit +### cargo-audit ```bash -# Install -cargo install cargo-audit - -# Run security audit -cargo audit - -# Example output: -# Crate: smallvec -# Version: 0.6.10 -# Warning: unsound -# Title: smallvec creates uninitialized value of any type -# Solution: upgrade to >= 0.6.14 OR >= 1.0.0 -# -# error: 1 vulnerability found! - -# Fix by updating -cargo update -p smallvec +cargo audit # Check RustSec advisories +cargo audit fix # Auto-fix if possible ``` -### Cargo Deny - -`cargo-deny` provides comprehensive dependency checking: +### cargo-deny ```bash -# Install -cargo install cargo-deny - -# Initialize config -cargo deny init - -# Run all checks -cargo deny check +cargo deny init # Initialize config +cargo deny check # Run all checks ``` -Configure in `deny.toml`: - ```toml +# deny.toml [advisories] -db-path = "~/.cargo/advisory-db" -db-urls = ["https://github.com/rustsec/advisory-db"] vulnerability = "deny" unmaintained = "warn" yanked = "warn" -notice = "warn" [licenses] unlicensed = "deny" -allow = [ - "MIT", - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "ISC", - "Zlib", -] +allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Zlib"] [bans] multiple-versions = "warn" wildcards = "deny" -highlight = "all" - -# Ban specific crates deny = [ - # Example: ban openssl in favor of rustls - { name = "openssl" }, + { name = "openssl" }, # Prefer rustls { name = "openssl-sys" }, ] @@ -481,292 +138,129 @@ unknown-git = "warn" allow-registry = ["https://github.com/rust-lang/crates.io-index"] ``` -### CI Integration +### License Compatibility + +| Safe | Caution | Avoid | +|------|---------|-------| +| MIT, Apache-2.0, BSD, ISC, Zlib | MPL-2.0, LGPL | GPL, AGPL | + +### Unsafe Code Audit + +```bash +cargo geiger # Shows unsafe usage per crate +``` + +--- -Add to your CI workflow (`.github/workflows/ci.yml`): +## Supply Chain Security CI ```yaml jobs: security: - name: Security Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Install cargo-audit - run: cargo install cargo-audit - - - name: Run cargo audit - run: cargo audit - - - name: Install cargo-deny - run: cargo install cargo-deny - - - name: Run cargo deny - run: cargo deny check -``` - -### Unsafe Code Audit - -```bash -# Install cargo-geiger -cargo install cargo-geiger - -# Audit unsafe code in dependencies -cargo geiger - -# Example output shows unsafe usage per crate: -# Functions Coverage Crate -# 0/0 100.0% my_crate -# 2/5 60.0% some_dep ← Has unsafe code + - run: cargo install cargo-audit cargo-deny + - run: cargo audit + - run: cargo deny check ``` --- ## Workspace Dependencies -### Centralized Version Management - -For workspaces with multiple crates, use `[workspace.dependencies]`: - ```toml -# Root Cargo.toml -[workspace] -members = ["crates/*"] -resolver = "2" - +# Root Cargo.toml -- define versions once [workspace.dependencies] -# Define versions once at workspace level serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } -thiserror = "1.0" -anyhow = "1.0" -tracing = "0.1" - -# Internal crates my-core = { path = "crates/my-core" } -my-utils = { path = "crates/my-utils" } -``` - -```toml -# crates/my-app/Cargo.toml -[package] -name = "my-app" -version = "0.1.0" +# Member Cargo.toml -- inherit [dependencies] -# Inherit from workspace serde = { workspace = true } -tokio = { workspace = true } -thiserror = { workspace = true } - -# Internal dependency my-core = { workspace = true } - # Can override features while inheriting version tracing = { workspace = true, features = ["log"] } ``` -### Benefits of Workspace Dependencies - -1. **Consistent versions** — All crates use the same dependency versions -2. **Single update point** — Update once, applies everywhere -3. **Reduced conflicts** — Prevents version mismatches between crates -4. **Cleaner manifests** — Member Cargo.toml files are simpler +Benefits: consistent versions, single update point, cleaner manifests. --- ## Git Dependencies -### When to Use Git Dependencies - -```toml -# Use git dependencies for: -# - Testing unreleased fixes/features -# - Using forks with patches -# - Private crates not on crates.io -# - Pre-release testing before publishing - -[dependencies] -# Specific branch -some-crate = { git = "https://github.com/org/some-crate", branch = "main" } - -# Specific tag -some-crate = { git = "https://github.com/org/some-crate", tag = "v1.0.0" } - -# Specific commit (most reproducible) -some-crate = { git = "https://github.com/org/some-crate", rev = "abc123" } - -# Private repository (uses SSH) -private-crate = { git = "git@github.com:org/private-crate.git" } -``` - -### Cautions with Git Dependencies - ```toml -# ⚠️ CAUTION: Git dependencies have drawbacks - -# 1. Not publishable to crates.io -# If you want to publish, you must use crates.io dependencies - -# 2. Branch references can change -# Using branch = "main" means builds aren't reproducible -# Prefer rev = "commit-hash" for reproducibility - -# 3. Can break unexpectedly -# The upstream repo can be deleted, rebased, or changed - -# 4. Slower builds -# Git deps are fetched fresh more often than cached crates.io deps +# Use for: unreleased fixes, forks with patches, private crates +some-crate = { git = "https://github.com/org/repo", rev = "abc123" } +# Prefer rev= for reproducibility; branch= is not reproducible +# Cannot publish to crates.io with git dependencies ``` --- -## Dependency Replacement Strategies - -### When to Replace Dependencies +## Replacement Strategies -Replace dependencies proactively when: - -- **Deprecated** — Maintainer announced deprecation -- **Unmaintained** — No updates for 1+ years, issues ignored -- **Security issues** — Unpatched vulnerabilities -- **Better alternatives** — Newer crates with better design -- **Bloated** — Dependency pulls too many transitive deps - -### Finding Alternatives - -```bash -# Check blessed.rs for recommended alternatives -# Check lib.rs for similar crates -# Search GitHub for "awesome rust" lists - -# Compare candidates: -# - Feature parity -# - Dependency count -# - Maintenance activity -# - Community adoption -``` - -### Migration Example - -```rust -// Example: Migrating from failure to thiserror - -// ❌ OLD: Using deprecated failure crate -use failure::{Error, Fail}; - -#[derive(Debug, Fail)] -enum MyError { - #[fail(display = "IO error: {}", _0)] - Io(#[cause] std::io::Error), -} - -// ✅ NEW: Using thiserror (modern, maintained) -use thiserror::Error; - -#[derive(Debug, Error)] -enum MyError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} -``` +Replace dependencies proactively when: deprecated, unmaintained (1+ year), unpatched vulnerabilities, better alternatives exist, too many transitive deps. --- -## Fortress-Specific Guidelines +## Fortress-Specific Requirements -### Required Dependencies Review +All new dependencies must: -All new dependencies in Fortress Rollback must: +1. Pass `cargo deny check` +2. Support `no_std` (or be behind a feature flag) +3. Be deterministic (no hidden randomness) +4. Minimize features (only enable what's needed) +5. Be documented in Cargo.toml with a comment -1. **Pass `cargo deny check`** — No license or security issues -2. **Support `no_std`** — Or be behind a feature flag if std-only -3. **Be deterministic** — No hidden randomness or non-determinism -4. **Minimize features** — Only enable what's actually needed -5. **Be documented** — Comment why the dependency is needed - -### Cargo.toml Documentation - -```toml -[dependencies] -# Serialization - required for network protocol and save states -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } - -# Error handling - zero-cost error types with derives -thiserror = "1.0" - -# Deterministic hashing - required for game state verification -# NOTE: Must use fixed-state hasher, not RandomState -ahash = { version = "0.8", default-features = false } - -# Property testing - dev only -[dev-dependencies] -proptest = "1.0" -``` - -### Recommended Crates for Game Networking +### Recommended Crates | Purpose | Crate | Notes | |---------|-------|-------| -| Serialization | `serde`, `bincode` | Use for network protocol | +| Serialization | `serde`, `bincode` | Network protocol, save states | | Hashing | `ahash`, `xxhash-rust` | Deterministic, fast | | Compression | `lz4_flex` | Fast, pure Rust | -| Networking | `quinn`, `laminar` | QUIC, game-oriented UDP | -| Crypto | `ring`, `rustls` | If encryption needed | +| Networking | `quinn`, `laminar` | QUIC, game UDP | --- -## Checklist for AI Agents +## Automated Updates -When adding or updating dependencies: - -```markdown -## Pre-Addition Checklist -- [ ] Searched for std alternative first -- [ ] Checked blessed.rs for recommendations -- [ ] Verified license compatibility (MIT/Apache-2.0) -- [ ] Checked crate downloads (> 100k preferred) -- [ ] Checked recent maintenance activity -- [ ] Ran `cargo audit` - no advisories -- [ ] Reviewed transitive dependencies with `cargo tree` -- [ ] Disabled unnecessary default features -- [ ] Added comment explaining why dependency is needed - -## Post-Update Checklist -- [ ] Ran full test suite -- [ ] Ran `cargo clippy` -- [ ] Ran `cargo deny check` -- [ ] Verified no new duplicate dependencies -- [ ] Updated any version comments if needed +```yaml +# .github/dependabot.yml +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: { interval: "weekly" } + groups: + rust-dependencies: + patterns: ["*"] + update-types: ["minor", "patch"] + open-pull-requests-limit: 10 ``` --- -## Common Commands Reference +## Agent Checklist -```bash -# === Analysis === -cargo tree # Full dependency tree -cargo tree -d # Duplicate dependencies -cargo tree -f "{p} {f}" # With features -cargo tree -i crate_name # Inverse (why included) -cargo tree -e features # Feature graph - -# === Maintenance === -cargo update # Update all compatible -cargo update -p crate_name # Update specific crate -cargo update -p crate_name --precise X # Update to specific version -cargo +nightly udeps # Find unused deps -cargo outdated # Check for newer versions - -# === Security === -cargo audit # RustSec advisories -cargo audit fix # Auto-fix if possible -cargo deny check # Comprehensive checks -cargo geiger # Unsafe code audit - -# === Inspection === -cargo metadata --format-version 1 # Machine-readable dep info -cargo pkgid crate_name # Get exact package ID -``` +### Adding a Dependency + +- [ ] Searched for std alternative +- [ ] Checked blessed.rs +- [ ] Verified license (MIT/Apache-2.0) +- [ ] Checked downloads (>100k preferred) +- [ ] Checked recent maintenance +- [ ] `cargo audit` -- no advisories +- [ ] `cargo tree` -- reviewed transitives +- [ ] Disabled unnecessary default features +- [ ] Added comment explaining why needed + +### After Updating + +- [ ] Full test suite passes +- [ ] `cargo clippy` clean +- [ ] `cargo deny check` passes +- [ ] No new duplicate dependencies diff --git a/.llm/skills/determinism-guide.md b/.llm/skills/determinism-guide.md deleted file mode 100644 index 8a997e57..00000000 --- a/.llm/skills/determinism-guide.md +++ /dev/null @@ -1,1118 +0,0 @@ -# Determinism in Rust Game Development - -> **A guide to achieving and verifying determinism in Rust games, essential for rollback netcode and replay systems.** - -## Why Determinism Matters - -Determinism means: **given identical inputs, the game produces bit-identical outputs on every run, on every machine.** - -This is required for: - -- **Rollback netcode** — Peers must reach the same state -- **Replay systems** — Must reproduce exact gameplay -- **Competitive integrity** — Identical game state for all players -- **Debugging** — Reproducible bugs are fixable bugs - ---- - -## Sources of Non-Determinism - -### 🔴 Critical: These WILL Cause Desyncs - -| Source | Problem | Solution | -|--------|---------|----------| -| `HashMap` / `HashSet` iteration | Random ordering per run | Use `BTreeMap` / `BTreeSet` | -| `f32::sin()`, `cos()`, etc. | Different implementations | Use `libm` or fixed-point | -| `rand::random()` | System entropy | Seeded deterministic RNG | -| `Instant::now()` / `SystemTime` | Wall clock varies | Frame counters only | -| Thread execution order | Scheduler-dependent | Single-threaded simulation | -| Memory addresses | ASLR, allocator behavior | Never use pointers in logic | -| `usize` | 32 vs 64-bit differences | Use explicit `u32`/`u64` | - -### 🟡 Moderate: Platform-Dependent - -| Source | Problem | Solution | -|--------|---------|----------| -| Floating-point precision | x87 vs SSE vs ARM | Control FPU settings or use fixed-point | -| Compiler optimizations | May reorder operations | Consistent compiler flags | -| FMA (fused multiply-add) | Different intermediate precision | Disable or ensure consistency | -| Denormal handling | Performance vs precision | Flush denormals to zero | - -### 🟢 Low Risk: Usually Safe - -| Source | Notes | -|--------|-------| -| Integer arithmetic | Deterministic unless overflow | -| Bitwise operations | Fully deterministic | -| Array/Vec indexing | Deterministic if indices are | -| `BTreeMap` / `BTreeSet` | Deterministic iteration by key | -| `IndexMap` / `IndexSet` | Deterministic insertion order | - ---- - -## Floating-Point Determinism - -### The Problem - -Floating-point math is NOT deterministic across platforms: - -```rust -// These may produce DIFFERENT results on different machines: -let a = (0.1_f32 + 0.2_f32) * 0.3_f32; -let b = x.sin(); // Transcendental functions vary by implementation -let c = (a * b) + c; // FMA may or may not be used -``` - -### Solution 1: Use `libm` (Easiest) - -```toml -# Cargo.toml - for Bevy projects -[dependencies] -glam = { version = "0.29", features = ["libm"] } -``` - -This: - -- Disables SIMD optimizations -- Uses portable software implementations -- Trades performance for determinism - -### Solution 2: Fixed-Point Math (Most Reliable) - -```toml -# Cargo.toml -[dependencies] -fixed = "1.29" -cordic = "0.1" # For sin/cos/sqrt/atan2 -``` - -```rust -use fixed::types::I32F32; - -/// Fixed-point position - guaranteed deterministic -#[derive(Clone, Copy, PartialEq, Eq)] -struct Position { - x: I32F32, - y: I32F32, -} - -impl Position { - fn new(x: f32, y: f32) -> Self { - Self { - x: I32F32::from_num(x), - y: I32F32::from_num(y), - } - } - - fn distance_squared(&self, other: &Self) -> I32F32 { - let dx = self.x - other.x; - let dy = self.y - other.y; - dx * dx + dy * dy - } -} - -/// Deterministic trigonometry via CORDIC -fn rotate_point(x: I32F32, y: I32F32, angle: I32F32) -> (I32F32, I32F32) { - let (sin, cos) = cordic::sin_cos(angle); - let new_x = x * cos - y * sin; - let new_y = x * sin + y * cos; - (new_x, new_y) -} -``` - -### Solution 3: Integer-Only Physics - -For maximum determinism, use integers directly: - -```rust -/// Position in 1/256ths of a pixel -#[derive(Clone, Copy, PartialEq, Eq)] -struct SubpixelPosition { - x: i32, // x * 256 - y: i32, // y * 256 -} - -impl SubpixelPosition { - const SCALE: i32 = 256; - - fn from_pixels(x: f32, y: f32) -> Self { - Self { - x: (x * Self::SCALE as f32) as i32, - y: (y * Self::SCALE as f32) as i32, - } - } - - fn to_pixels(&self) -> (f32, f32) { - ( - self.x as f32 / Self::SCALE as f32, - self.y as f32 / Self::SCALE as f32, - ) - } - - fn add_velocity(&mut self, vx: i32, vy: i32) { - self.x = self.x.wrapping_add(vx); - self.y = self.y.wrapping_add(vy); - } -} -``` - ---- - -## Deterministic Random Numbers - -### Required: Seeded RNG - -```rust -use rand_pcg::Pcg64; -use rand::SeedableRng; -use rand::Rng; - -/// Game state includes RNG -struct GameState { - rng: Pcg64, - // ... other state -} - -impl GameState { - fn new(seed: u64) -> Self { - Self { - // All peers use same seed - rng: Pcg64::seed_from_u64(seed), - } - } - - fn random_range(&mut self, min: i32, max: i32) -> i32 { - self.rng.gen_range(min..max) - } -} -``` - -### Important: RNG State Must Be Saved - -```rust -impl GameState { - fn save(&self) -> SavedState { - SavedState { - rng: self.rng.clone(), // ← Don't forget! - // ... other state - } - } - - fn load(&mut self, saved: &SavedState) { - self.rng = saved.rng.clone(); // ← Restore RNG too! - // ... other state - } -} -``` - -### Recommended RNG Libraries - -| Library | Type | Notes | -|---------|------|-------| -| `rand_pcg` | Pcg64, Pcg32 | Fast, portable, deterministic | -| `rand_chacha` | ChaCha12Rng | Cryptographic, very portable | -| `fastrand` | WyRand | Fastest, but check portability | - ---- - -## Deterministic Collections - -### HashMap → BTreeMap - -```rust -use std::collections::{HashMap, BTreeMap}; - -// ❌ WRONG: Non-deterministic iteration -let mut scores: HashMap = HashMap::new(); -for (id, score) in &scores { - // Order varies between runs! -} - -// ✅ CORRECT: Deterministic iteration by key -let mut scores: BTreeMap = BTreeMap::new(); -for (id, score) in &scores { - // Always iterates in sorted order -} -``` - -### HashSet → BTreeSet - -```rust -use std::collections::{HashSet, BTreeSet}; - -// ❌ WRONG -let mut active: HashSet = HashSet::new(); - -// ✅ CORRECT -let mut active: BTreeSet = BTreeSet::new(); -``` - -### IndexMap for Insertion Order - -```rust -use indexmap::IndexMap; - -// Maintains insertion order (deterministic if insertion order is) -let mut items: IndexMap = IndexMap::new(); -items.insert(ItemId(1), item1); -items.insert(ItemId(2), item2); - -for (id, item) in &items { - // Iterates in insertion order: 1, 2 -} -``` - -### Sorting Before Iteration - -When you must use HashMap for performance: - -```rust -let map: HashMap = /* ... */; - -// Sort keys before iterating -let mut keys: Vec<_> = map.keys().collect(); -keys.sort(); - -for key in keys { - let entity = &map[key]; - // Now deterministic -} -``` - ---- - -## WebAssembly for Cross-Platform Determinism - -### Why WASM Helps - -WebAssembly provides **stronger determinism guarantees** than native code: - -| Challenge | Native Code Problem | WASM Solution | -|-----------|---------------------|---------------| -| Float inconsistency | x87 80-bit vs SSE 64-bit | IEEE 754 specified semantics | -| Compiler differences | GCC vs Clang optimizations | Single IR, consistent codegen | -| Platform ABI | Calling conventions vary | Canonical ABI | -| Endianness | Big vs little endian | Little endian specified | -| Undefined behavior | C/C++ UB varies | Fully specified semantics | - -### WASM Float Determinism - -WASM specifies IEEE 754-2019 compliance with canonical NaN handling: - -```rust -// In native code: may differ across platforms -let result = (0.1_f32 + 0.2_f32) * 0.3_f32; - -// In WASM: guaranteed identical on all conforming runtimes -// Same binary → same results everywhere -``` - -### NaN Canonicalization - -WASM uses canonical NaN values, but be careful at boundaries: - -```rust -// When serializing float state across platforms: -fn canonicalize_float(x: f32) -> f32 { - if x.is_nan() { - f32::NAN // Use canonical NaN - } else { - x - } -} - -// Or use integer representation for state transfer -fn to_bits_safe(x: f32) -> u32 { - if x.is_nan() { - 0x7FC00000 // Canonical quiet NaN - } else { - x.to_bits() - } -} -``` - -### Architecture: WASM for Simulation - -Consider compiling game logic to WASM for guaranteed determinism: - -```rust -// Core game logic - compile to both native and WASM -// WASM version guarantees cross-platform determinism -mod game_logic { - pub fn advance_frame(state: &mut GameState, inputs: &[Input]) { - // All computation is deterministic in WASM - for input in inputs { - state.apply_input(input); - } - state.physics_tick(); // Float math works! - } -} - -// Platform-specific code - stays native for performance -mod platform { - pub fn render(state: &GameState) { /* GPU calls */ } - pub fn play_audio(events: &[AudioEvent]) { /* Audio API */ } -} -``` - -### WASM Determinism Caveats - -⚠️ **Threading breaks determinism** — Avoid WASM threads in game logic: - -```rust -// ❌ NON-DETERMINISTIC: Thread scheduling varies -use rayon::prelude::*; -let sum: f64 = values.par_iter().sum(); - -// ✅ DETERMINISTIC: Sequential in WASM -let sum: f64 = values.iter().sum(); -``` - -⚠️ **Host imports may not be deterministic**: - -```rust -// ❌ Host-provided time is non-deterministic -let now = js_sys::Date::now(); - -// ✅ Use frame counters for game logic -let game_time = frame_number * TICK_DURATION; -``` - -### See Also - -For complete WASM development guidance: - -- [wasm-rust-guide.md](wasm-rust-guide.md) — Rust to WASM compilation -- [wasm-threading.md](wasm-threading.md) — Threading and concurrency in WASM -- [wasm-portability.md](wasm-portability.md) — WASM determinism and sandboxing -- [cross-platform-rust.md](cross-platform-rust.md) — Multi-platform architecture -- [no-std-guide.md](no-std-guide.md) — `no_std` patterns for WASM - ---- - -## ECS Determinism (Bevy) - -### Query Iteration is Non-Deterministic - -```rust -// ❌ WRONG: Order not guaranteed -fn update_system(query: Query<&mut Position>) { - for mut pos in &mut query { - // Order may differ between frames, runs, and peers! - } -} -``` - -### Solution: Rollback Marker Component - -```rust -/// Stable ID for deterministic ordering -#[derive(Component, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct Rollback(pub u32); - -/// Always sort by Rollback ID -fn update_system(mut query: Query<(&Rollback, &mut Position)>) { - let mut items: Vec<_> = query.iter_mut().collect(); - items.sort_by_key(|(rb, _)| *rb); - - for (_, mut pos) in items { - // Now deterministic! - } -} -``` - -### Helper Trait for Deterministic Queries - -```rust -/// Extension trait for deterministic query iteration -pub trait DeterministicIter<'w, 's> { - type Item; - fn iter_sorted(&'w mut self) -> Vec; -} - -impl<'w, 's, T: Component> DeterministicIter<'w, 's> - for Query<'w, 's, (&Rollback, &mut T)> -{ - type Item = (Mut<'w, T>,); - - fn iter_sorted(&'w mut self) -> Vec { - let mut items: Vec<_> = self.iter_mut().collect(); - items.sort_by_key(|(rb, _)| rb.0); - items.into_iter().map(|(_, t)| (t,)).collect() - } -} -``` - ---- - -## Time Handling - -### Never Use Wall Clock Time - -```rust -// ❌ FORBIDDEN in game logic -let now = std::time::Instant::now(); -let elapsed = start.elapsed(); -let time = std::time::SystemTime::now(); - -// ❌ FORBIDDEN in Bevy -fn bad_system(time: Res