From 54a3e052c972001736fef9573d64e60d35b8eb6f Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 16 Aug 2026 19:27:13 +0200 Subject: [PATCH 1/3] ci: enforce coverage and harden Silver checks --- .github/workflows/ci.yml | 24 +- .github/workflows/codeql.yml | 16 +- .gitignore | 6 + ARCHITECTURE.md | 102 ++++++ ASSURANCE.md | 109 ++++++ CONTRIBUTING.md | 44 ++- ROADMAP.md | 24 ++ SECURITY.md | 40 ++- scripts/invoke-npm-coverage.ps1 | 64 ++++ scripts/invoke-pester-coverage.ps1 | 77 +++++ scripts/rust-production-coverage.py | 316 ++++++++++++++++++ .../coverage/test_rust_production_coverage.py | 298 +++++++++++++++++ 12 files changed, 1111 insertions(+), 9 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 ASSURANCE.md create mode 100644 scripts/invoke-npm-coverage.ps1 create mode 100644 scripts/invoke-pester-coverage.ps1 create mode 100644 scripts/rust-production-coverage.py create mode 100644 tests/coverage/test_rust_production_coverage.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d6fa2b..12b2a66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # upstream commit with: toolchain: 1.97.1 - components: rustfmt, clippy + components: rustfmt, clippy, llvm-tools-preview - run: cargo fmt -- --check - run: cargo check --workspace --all-targets - run: cargo test @@ -32,6 +32,17 @@ jobs: shell: pwsh run: cargo install cargo-deny --version 0.20.2 --locked - run: cargo deny check + - name: Install cargo-llvm-cov + shell: pwsh + run: cargo install cargo-llvm-cov --version 0.8.7 --locked + - name: Test the production coverage analyzer + shell: pwsh + run: python -m unittest discover -s tests/coverage + - name: Enforce Rust production coverage >= 80% + shell: pwsh + run: | + cargo llvm-cov --workspace --json --output-path target/llvm-cov-export.json + python scripts/rust-production-coverage.py target/llvm-cov-export.json --threshold 80 --json target/production-coverage.json powershell: runs-on: windows-latest @@ -47,9 +58,9 @@ jobs: - name: Parse and lint PowerShell shell: pwsh run: ./scripts/validate-powershell.ps1 - - name: Run Pester tests + - name: Run Pester tests with coverage gate shell: pwsh - run: Invoke-Pester -Path ./tests/powershell -Output Detailed + run: ./scripts/invoke-pester-coverage.ps1 distribution: strategy: @@ -83,7 +94,14 @@ jobs: } } - name: Run the npm bootstrap unit tests + # The canonical Node 24 leg also enforces the coverage gate; the + # 22/26 legs only check compatibility with the plain test run. + if: matrix.node-version != '24' run: node --test "tests/npm/**/*.test.mjs" + - name: Run the npm bootstrap tests with coverage gate + if: matrix.node-version == '24' + shell: pwsh + run: ./scripts/invoke-npm-coverage.ps1 - name: Validate the npm tarball allow-list shell: pwsh run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1eb9abb..55521d1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -15,7 +15,7 @@ permissions: jobs: analyze: name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} permissions: actions: read contents: read @@ -24,9 +24,17 @@ jobs: fail-fast: false matrix: include: + # The Rust extractor runs on Windows because the crate is + # Windows-only (windows_sys / console APIs); extraction quality is + # significantly better against the real target platform. - language: rust + os: windows-latest build-mode: none - language: actions + os: ubuntu-latest + build-mode: none + - language: javascript-typescript + os: ubuntu-latest build-mode: none steps: - name: Checkout @@ -34,6 +42,12 @@ jobs: with: persist-credentials: false + - name: Install Rust toolchain + if: matrix.language == 'rust' + uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # upstream commit + with: + toolchain: 1.97.1 + - name: Initialize CodeQL uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: diff --git a/.gitignore b/.gitignore index 2395060..88ddf13 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ # Test, coverage and generated reports coverage/ +# ...but the coverage analyzer unit tests are tracked source. +!tests/coverage/ coverage.lcov TestResults/ *.trx @@ -44,3 +46,7 @@ devnav-*.sha256 *.pem *.pfx *.p12 + +# Python tooling caches +__pycache__/ +*.pyc diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..e4567f5 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,102 @@ +# Architecture + +DevNav is a Windows-only folder navigator: a native Rust TUI binary driven from +PowerShell through a thin integration module. This document describes what +exists in the repository today; it does not describe planned work (see +[ROADMAP.md](ROADMAP.md)). + +## Components + +### Rust native navigator (`src/`) + +A single binary (`dev`) plus a small library crate, with no runtime +dependencies beyond `windows-sys`: + +- `main.rs` — CLI entry point: `--version`, config-only commands + (`--set-language`, `--detect-language`, `--set-shortcut`, `--clear-shortcut`), + root resolution (`--root` argument, then configured root, then `DEV_HOME`, + then the user profile), TUI launch, and result-file writing. +- `app.rs` — application state machine: modes (normal, help, filter, path, + alias, command, confirm-root, command manager/editor/delete confirmation), + directory listing, fuzzy filtering, favorites/aliases, and frame generation + (`render_rows` is pure string generation; `render` wraps it with the real + terminal handle). +- `config.rs` — the `config.tsv` parser/writer (see below) with atomic saves + (`ReplaceFileW` on Windows) and the `Shortcut` model shared by the lib and + bin targets. +- `i18n.rs` — dependency-free Spanish/English localization. +- `input.rs` / `terminal.rs` — Win32 console input and raw-mode handling + (Windows-only by design). +- `model.rs` — the `ShellResult` payload contract (`cd` / `exec` / `update`). +- `render.rs` — ANSI frame diffing renderer. + +### PowerShell integration (`powershell/DevNav.psm1`) + +The module the user actually invokes (`dev`). Responsibilities: + +- Locate the installed executable (managed install vs. portable layouts). +- Language detection/init, startup update check, and config helpers that read + and write the same `config.tsv` as the Rust binary. +- Run the TUI with a `--result` temp file, then interpret the result: change + directory, execute a command in the selected folder, or run the updater — + actions that only a shell can perform on the user's session. +- Shortcut management (`Set-DevShortcut`, `Remove-DevShortcut`, …) and the + self-updater (`Update-DevNavigator`). + +### Installer and profile integration + +- `installer/DevNav.iss` — Inno Setup script producing per-user x64/ARM64 + installers into `%LOCALAPPDATA%\Programs\DevNav`. +- `installer/ProfileIntegration.ps1` — installs/removes the PowerShell module + and profile hook for the installing user. +- `install.ps1` — bootstraps a managed installation from a release. + +### Distribution channels + +The GitHub Release is the canonical source of every artifact; all other +channels derive from it: + +- **npm bootstrap** (`packaging/npm/bin/devnav.mjs`) — a transient delivery + channel with no runtime dependencies. The tarball embeds the canonical Inno + installers plus `release-manifest.json`; the bootstrap verifies SHA-256 + hashes before delegating to Inno. It never owns installed files. +- **Scoop** — portable artifacts in the release feed the separate + `JacobOptimiza/scoop-bucket` repository (`packaging/scoop/`). +- **WinGet** — immutable versioned manifests with SHA-256 + (`packaging/winget/`), submitted to `microsoft/winget-pkgs`. + +## `dev` invocation flow + +1. The user runs `dev` in PowerShell; the module resolves the executable and + passes `--result ` (and optionally `--root`). +2. The Rust binary reads `%LOCALAPPDATA%\DevNav\config.tsv`, enters the TUI on + the current console, and handles all interaction locally. +3. On exit it writes one NUL-separated record to the result file: + `cd\0\0`, `exec\0\0` or `update\0\0`. +4. The PowerShell module reads the file and performs the action in the user's + session (or triggers the updater). The temp file is removed afterwards. + +## Configuration (`config.tsv`) + +Single flat TSV file owned jointly by the Rust binary and the PowerShell +module, read and written by both through stable key prefixes: `root`, +`show_favorites`, `check_updates`, `language`, `favorite\0t`, +`alias\0t\0t`, and `shortcut\0t\0t\0t` +(1–9, executed as Shift+digit in the TUI). The Rust writer escapes `%`, tab +and newline and replaces the file atomically. + +## Trust boundaries + +- **Rust binary**: local process with console and filesystem access only. It + never executes commands itself; it only records intents in the result file. +- **PowerShell module**: the trust pivot. It executes the `cd`/`exec` the + binary requested — commands typed by the user in the TUI or configured + shortcuts from `config.tsv` — in the user's own session and privileges. +- **Network** is used in exactly two places: the startup/friendly update check + and updater (GitHub Releases API over TLS), and package-manager downloads + during initial installation (npm registry / Scoop / WinGet). The TUI itself, + navigation, favorites, aliases, and shortcuts are fully offline. +- **Integrity of downloaded artifacts**: the npm bootstrap verifies SHA-256 + hashes from `release-manifest.json` before running the installer; Scoop and + WinGet manifests carry release SHA-256 hashes. Checksums verify integrity, + not authenticity (see [SECURITY.md](SECURITY.md)). diff --git a/ASSURANCE.md b/ASSURANCE.md new file mode 100644 index 0000000..c33152f --- /dev/null +++ b/ASSURANCE.md @@ -0,0 +1,109 @@ +# Assurance case + +This page states verifiable claims about DevNav, the evidence that backs them, +and the residual limitations that remain. It is intentionally narrow: only +properties the repository itself can demonstrate. + +## Build and test quality + +**Claim.** The Rust workspace builds warning-free and its full test suite +passes on the pinned toolchain (1.97.1). + +**Evidence.** CI `validate` job: `cargo fmt --check`, `cargo check +--workspace --all-targets`, `cargo test --workspace`, `cargo clippy +--workspace --all-targets -- -D warnings`, `cargo deny check`. PowerShell: +parser + PSScriptAnalyzer + Pester (`.github/workflows/ci.yml`). + +**Residual limitation.** None for this claim. + +## Test coverage ≥ 80% (per language) + +**Claim.** Test coverage stays at or above 80% for Rust production code, the +PowerShell integration, and the npm bootstrap, enforced in CI. + +**Evidence.** +- Rust: `scripts/rust-production-coverage.py` computes production-only + coverage from `cargo llvm-cov` 0.8.7 JSON, excluding only `#[cfg(test)]` + items (analyzed by its own unit tests, `tests/coverage/`). The CI `validate` + job fails below 80% lines or regions. Current: 87.60% lines / 86.30% + regions. +- PowerShell: `scripts/invoke-pester-coverage.ps1` runs Pester 6.1.0 once + with JaCoCo coverage over `powershell/DevNav.psm1`, `install.ps1` and + `installer/ProfileIntegration.ps1`; the CI `powershell` job fails below 80% + commands or lines. Current: 82.19% / 82.87%. +- JavaScript: `scripts/invoke-npm-coverage.ps1` runs the bootstrap tests under + Node's native test runner with `--experimental-test-coverage`; the + Node 24 leg of the CI `distribution` job fails below 80% lines. Current: + 93.79% lines. (Node reports *line* coverage; the historical + 93.78% *statement* figure is kept only as a baseline reference.) + +**Residual limitation.** Rust coverage excludes host-bound code paths +(`Terminal::enter/size/drop`, `read_key`, `Renderer::draw`, `run()/main()`, +`detect_system_locale`) from the numerator only because they require a real +interactive console; they remain in the denominator and are exercised by +manual use. + +## Dependency policy + +**Claim.** Dependencies are pinned (`Cargo.lock`), license-audited and +vulnerability-checked on every push. + +**Evidence.** `cargo deny check` (advisories, bans, licenses, sources) in the +CI `validate` job; Dependabot configuration; the npm bootstrap has zero +runtime dependencies. + +**Residual limitation.** Advisory data depends on the RustSec/advisory-database +feed at check time. + +## Static analysis + +**Claim.** CodeQL analyzes Rust, GitHub Actions and JavaScript/TypeScript on +pushes to `main` and pull requests. + +**Evidence.** `.github/workflows/codeql.yml`; results upload to +security/code-scanning. + +**Residual limitation.** Rust extraction runs in `build-mode: none`, which +cannot expand macros; a residual set of extraction errors remains for +macro-heavy files. A green CodeQL run is therefore not evidence of +extraction-clean analysis; see SECURITY.md for the current residual. + +## Fuzzing + +**Claim.** The `config.tsv` parser is fuzzed continuously. + +**Evidence.** ClusterFuzzLite (`.clusterfuzzlite/`, `.github/workflows/cflite_pr.yml`) +on pull requests affecting its Rust source or fuzzing integration. + +**Residual limitation.** Fuzzing covers the parser target only; it does not +replace the unit suites. + +## Release integrity and provenance + +**Claim.** Release artifacts are built once by the release workflow, checksums +(SHA-256) are published for every artifact, channels derive from the canonical +GitHub Release, and future artifacts receive GitHub build attestations. + +**Evidence.** `.github/workflows/release.yml`; `release-manifest.json` +verification in the npm bootstrap; Scoop/WinGet manifests carry release +SHA-256 hashes; version consistency across `Cargo.toml`, tag, release, +`DevNav.psd1`, `package.json` and the Scoop template is enforced by CI. + +**Residual limitation.** SHA-256 checksums verify integrity, not authenticity. +A build attestation proves workflow provenance and is not equivalent to a +legacy code-signing signature on the binaries. + +## Package and version consistency + +**Claim.** One version is shared by every channel and checked mechanically. + +**Evidence.** CI consistency checks abort the release on any mismatch; npm and +Scoop enforce a `0.10.0` floor for multichannel packaging. + +**Residual limitation.** None for this claim. + +## Not automatable + +The following remain human responsibilities and are not covered by any +automated gate: code review quality, issue triage, the decision to publish a +release, and manual verification of the interactive TUI on real consoles. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eeb469..85c982b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,11 +27,53 @@ cargo check --workspace --all-targets cargo test --workspace cargo clippy --workspace --all-targets -- -D warnings cargo deny check +# Rust production-only coverage gate (requires cargo-llvm-cov 0.8.7): +cargo llvm-cov --workspace --json --output-path target/llvm-cov-export.json +python scripts/rust-production-coverage.py target/llvm-cov-export.json --threshold 80 +python -m unittest discover -s tests/coverage # analyzer unit tests ./scripts/validate-powershell.ps1 -Invoke-Pester -Path ./tests/powershell +./scripts/invoke-pester-coverage.ps1 # Pester + coverage gate node --test "tests/npm/**/*.test.mjs" +./scripts/invoke-npm-coverage.ps1 # Node coverage gate ``` +## Coding standards + +### Rust + +- `cargo fmt` formatting is mandatory; `clippy -D warnings` must stay clean. +- Use `Result`/`Option` and explicit error propagation (`?`) instead of + panicking in recoverable paths; keep `unwrap`/`expect` to tests and truly + invariant conditions. +- New behavior needs unit tests next to the module (`#[cfg(test)]`); bug fixes + need regression tests. Coverage gates enforce >= 80% production-only lines + and regions. +- `unsafe` is allowed only where a Win32 API boundary requires it and must + carry a `// SAFETY:` justification comment. + +### PowerShell + +- Must pass the repository PSScriptAnalyzer settings + (`./scripts/validate-powershell.ps1`) and Pester tests with the coverage + gate. +- Prefer testable, non-interactive functions; use `SupportsShouldProcess` + (`-WhatIf`/`-Confirm`) on cmdlets that change state. + +### JavaScript (npm bootstrap) + +- Node `>= 22` (CI tests 22, 24, 26; Node 24 is the release baseline); use + standard-library APIs only — no runtime dependencies. +- The bootstrap tests (`tests/npm`) must pass; line coverage is gated at + >= 80% on the Node 24 leg. + +### General + +- Keep changes small, scoped to one purpose, and consistent with existing + style; behavior changes require behavior tests. +- GitHub Actions references must be pinned to full commit SHAs. +- Never edit published release artifacts, versions, or the npm package in + place; versions are immutable once public. + The repository also fuzzes the `config.tsv` parser with ClusterFuzzLite on pull requests that affect its Rust source or fuzzing integration. Fuzzing is a supplement to, not a replacement for, the ordinary test suite. diff --git a/ROADMAP.md b/ROADMAP.md index dc05299..c0d4389 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,6 +5,30 @@ and the public Scoop bucket. Release payloads are checksum-verified with SHA-256; checksums verify integrity and are not digital signatures. +## Current maintenance and quality baseline + +- [x] Rust, PowerShell and npm bootstrap test suites green on every push and + pull request (pinned toolchains: Rust 1.97.1, Pester 6.1.0, + PSScriptAnalyzer 1.25.0, Node 22/24/26 with 24 as the release baseline). +- [x] Coverage floors enforced in CI at >= 80% for all three languages: + Rust production-only lines and regions, PowerShell commands and lines, and + npm bootstrap lines (Node native coverage). +- [x] CodeQL static analysis for Rust, GitHub Actions and + JavaScript/TypeScript; ClusterFuzzLite fuzzing of the `config.tsv` parser; + `cargo deny` advisory/license/ban checks. +- [x] Version consistency across all channels enforced mechanically. + +## Near-term quality and security work + +- [ ] Keep reducing CodeQL Rust extraction errors if upstream extractor + support improves (current residual is macro-expansion-related and documented + in [SECURITY.md](SECURITY.md)). +- [ ] Revisit OpenSSF Scorecard findings that require organizational decisions + (branch protection coverage, review and maintenance signals) rather than + code changes. +- [ ] Validate a real cross-version Scoop upgrade on a future release and + automate WinGet submissions once a catalog installation is verified. + ## Distribution status - [x] GitHub releases with x64 and ARM64 application binaries. diff --git a/SECURITY.md b/SECURITY.md index 645131c..f2d91ae 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,7 +20,39 @@ and keep the reporter informed as we investigate. ## Security controls -CodeQL analyzes Rust and GitHub Actions on pushes to `main` and pull requests. -OpenSSF Scorecard runs on `main` and publishes its current result through the -repository badge. These controls complement, but do not replace, private -vulnerability reporting. +CodeQL analyzes Rust, GitHub Actions and JavaScript/TypeScript on pushes to +`main` and pull requests. The Rust lane runs on Windows (the only supported +target platform) in `build-mode: none`; because that mode does not compile the +crate, macro-heavy Rust files can still produce extraction diagnostics, which +are tracked rather than hidden. OpenSSF Scorecard runs on `main` and publishes +its current result through the repository badge. + +Additional automated controls: + +- `cargo deny check` (advisories, bans, licenses, sources) on every push. +- ClusterFuzzLite fuzzing of the `config.tsv` parser on qualifying pull + requests. +- Coverage floors enforced in CI (>= 80%): Rust production-only lines and + regions (`scripts/rust-production-coverage.py`), PowerShell commands and + lines (`scripts/invoke-pester-coverage.ps1`), and npm bootstrap lines + (`scripts/invoke-npm-coverage.ps1`). +- Release integrity: every artifact ships a SHA-256 checksum; the npm + bootstrap verifies installers against `release-manifest.json` before + executing them, and Scoop/WinGet manifests pin release hashes. Checksums + verify integrity, not authenticity. +- Future release artifacts receive GitHub build attestations; an attestation + proves build provenance and is not equivalent to a legacy code-signing + signature. + +These controls complement, but do not replace, private vulnerability +reporting. + +## Trust boundaries and threat surface + +See [ARCHITECTURE.md](ARCHITECTURE.md) for the component layout. In summary: +the Rust binary is a local console application that never executes commands; +the PowerShell module is the component that acts on the user's session using +intents the user typed or configured locally; network access is limited to +GitHub Releases for update checks/downloads and to package managers during +initial installation. Configuration lives in a single per-user file +(`%LOCALAPPDATA%\DevNav\config.tsv`). diff --git a/scripts/invoke-npm-coverage.ps1 b/scripts/invoke-npm-coverage.ps1 new file mode 100644 index 0000000..943bd1f --- /dev/null +++ b/scripts/invoke-npm-coverage.ps1 @@ -0,0 +1,64 @@ +<# +.SYNOPSIS + Runs the npm bootstrap tests with Node's native test coverage and enforces + the coverage gate. + +.DESCRIPTION + Single `node --test --experimental-test-coverage` run over tests/npm. + Node's test runner reports *line* coverage (not statement coverage); that + is the metric this gate enforces, per covered source file. Exits non-zero + if the tests fail or any covered file drops below the threshold + (default 80%). No extra npm dependencies are required. + +.EXAMPLE + ./scripts/invoke-npm-coverage.ps1 +#> +[CmdletBinding()] +param( + [double]$Threshold = 80.0 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$output = & node --test --experimental-test-coverage "tests/npm/**/*.test.mjs" 2>&1 +$testsExit = $LASTEXITCODE +$output | ForEach-Object { $_ } + +if ($testsExit -ne 0) { + Write-Host "FAIL: npm bootstrap tests failed (exit $testsExit)." + exit 1 +} + +$rows = @() +foreach ($line in $output) { + $text = $line -replace '^[^a-zA-Z0-9]*', '' + if ($text -match '^\s*(\S+\.(?:mjs|cjs|js))\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|') { + $rows += [pscustomobject]@{ + File = $Matches[1] + LinePct = [double]$Matches[2] + BranchPct = [double]$Matches[3] + FuncPct = [double]$Matches[4] + } + } +} + +if ($rows.Count -eq 0) { + Write-Host "FAIL: no coverage rows found in node --test output." + exit 1 +} + +foreach ($row in $rows) { + Write-Host ("JavaScript coverage: {0} lines {1}%, branches {2}%, functions {3}%" -f + $row.File, $row.LinePct, $row.BranchPct, $row.FuncPct) +} + +$below = @($rows | Where-Object { $_.LinePct -lt $Threshold }) +if ($below.Count -gt 0) { + foreach ($row in $below) { + Write-Host "FAIL: line coverage for $($row.File) is $($row.LinePct)% < $Threshold%" + } + exit 1 +} +Write-Host "OK: npm bootstrap tests pass and line coverage meets the $Threshold% threshold." +exit 0 diff --git a/scripts/invoke-pester-coverage.ps1 b/scripts/invoke-pester-coverage.ps1 new file mode 100644 index 0000000..950769e --- /dev/null +++ b/scripts/invoke-pester-coverage.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + Runs the DevNav Pester suite once and enforces the coverage gate. + +.DESCRIPTION + Single Pester 6.1.0 run over tests/powershell with code coverage measured + against exactly these production files: + + powershell/DevNav.psm1 + install.ps1 + installer/ProfileIntegration.ps1 + + Prints commands/lines coverage and exits non-zero if the tests fail or if + either metric drops below the threshold (default 80%). + +.EXAMPLE + ./scripts/invoke-pester-coverage.ps1 +#> +[CmdletBinding()] +param( + [double]$Threshold = 80.0, + [string]$CoverageOutputPath = 'TestResults/pester-coverage.xml' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +Import-Module Pester -MinimumVersion 6.0 -Force + +$config = New-PesterConfiguration +$config.Run.Path = './tests/powershell' +$config.Run.PassThru = $true +$config.Output.Verbosity = 'Detailed' +$config.CodeCoverage.Enabled = $true +$config.CodeCoverage.Path = @( + 'powershell/DevNav.psm1' + 'install.ps1' + 'installer/ProfileIntegration.ps1' +) +$config.CodeCoverage.OutputPath = $CoverageOutputPath + +$result = Invoke-Pester -Configuration $config + +$cc = $result.CodeCoverage +$commandsCovered = $cc.CommandsExecutedCount +$commandsTotal = $cc.CommandsAnalyzedCount +$commandsPercent = [math]::Round(100.0 * $commandsCovered / $commandsTotal, 2) + +# Parse the JaCoCo report written to disk (the in-memory CoverageReport +# string includes a DOCTYPE that [xml] rejects; the file copy parses fine). +$xml = [xml](Get-Content $CoverageOutputPath -Raw) +# XPath is StrictMode-safe (adapted XML properties are not). +$lineCounter = $xml.SelectNodes('/report/counter') | Where-Object { $_.type -eq 'LINE' } +$linesCovered = [int]$lineCounter.covered +$linesTotal = $linesCovered + [int]$lineCounter.missed +$linesPercent = [math]::Round(100.0 * $linesCovered / $linesTotal, 2) + +Write-Host "PowerShell coverage: commands $commandsCovered/$commandsTotal ($commandsPercent%), lines $linesCovered/$linesTotal ($linesPercent%)" + +$failed = $false +if ($result.FailedCount -gt 0) { + Write-Host "FAIL: $($result.FailedCount) Pester test(s) failed." + $failed = $true +} +if ($commandsPercent -lt $Threshold) { + Write-Host "FAIL: command coverage $commandsPercent% < $Threshold%" + $failed = $true +} +if ($linesPercent -lt $Threshold) { + Write-Host "FAIL: line coverage $linesPercent% < $Threshold%" + $failed = $true +} +if ($failed) { + exit 1 +} +Write-Host "OK: PowerShell tests pass and coverage meets the $Threshold% threshold." +exit 0 diff --git a/scripts/rust-production-coverage.py b/scripts/rust-production-coverage.py new file mode 100644 index 0000000..75ec27d --- /dev/null +++ b/scripts/rust-production-coverage.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python3 +"""Production-only coverage report for the DevNav Rust workspace. + +Consumes the JSON export produced by `cargo llvm-cov --json` (validated with +cargo-llvm-cov 0.8.7) and computes coverage over production code only, by +excluding exclusively items that are compiled solely under `#[cfg(test)]`: + +- `#[cfg(test)] mod tests { ... }` modules (brace-matched); +- individual `#[cfg(test)]` helper functions/items. + +Everything else, including uncovered host/terminal code, stays in the +denominator. Files instrumented by both the lib and the bin target (e.g. +config.rs) are deduplicated, as are generic instantiations of one function. + +Metrics: +- regions: function regions with expanded-file-id 0 of production functions + (matches llvm-cov's per-file region summary); +- lines: lines spanned by production function regions, covered when the + innermost region executing them ran (matches llvm-cov line semantics); +- functions: production functions after dedup (reported, not gated). + +Exit code is non-zero when the configurable thresholds are not met, and the +script fails closed (error exit) when the Rust source analysis is ambiguous +instead of silently excluding too much. + +Usage: + python scripts/rust-production-coverage.py coverage.json \ + [--threshold 80] [--regions-threshold N] [--json out.json] +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path + +CRATE_RE = re.compile(r"Cs[0-9a-zA-Z]{11,13}_\d+dev(?:_nav)?") +GENERIC_MARKERS = ("ARe", "INt") + + +class SourceError(Exception): + """Raised when Rust source analysis is ambiguous; the caller fails closed.""" + + +def _strip_noncode(source: str) -> str: + """Replace comments, strings, raw strings and char literals with spaces, + preserving length and line structure, so brace matching never sees them.""" + out = list(source) + i, n = 0, len(source) + + def blank(a: int, b: int) -> None: + for k in range(a, b): + if out[k] != "\n": + out[k] = " " + + while i < n: + c = source[i] + if c == "/" and i + 1 < n and source[i + 1] == "/": + j = source.find("\n", i) + j = n if j < 0 else j + blank(i, j) + i = j + elif c == "/" and i + 1 < n and source[i + 1] == "*": + depth, j = 1, i + 2 + while j < n and depth: + if source.startswith("/*", j): + depth += 1 + j += 2 + elif source.startswith("*/", j): + depth -= 1 + j += 2 + else: + j += 1 + if depth: + raise SourceError("unterminated block comment") + blank(i, j) + i = j + elif c == "r" and i + 1 < n and source[i + 1] in '"#': + j = i + 1 + hashes = 0 + while j < n and source[j] == "#": + hashes += 1 + j += 1 + if j < n and source[j] == '"': + closer = '"' + "#" * hashes + k = source.find(closer, j + 1) + if k < 0: + raise SourceError("unterminated raw string") + blank(i, k + len(closer)) + i = k + len(closer) + else: + i += 1 + elif c == '"': + j = i + 1 + while j < n: + if source[j] == "\\": + j += 2 + elif source[j] == '"': + break + elif source[j] == "\n": + raise SourceError("unterminated string literal") + else: + j += 1 + if j >= n: + raise SourceError("unterminated string literal") + blank(i, j + 1) + i = j + 1 + elif c == "'": + # char literal ('x', '\n', '{') vs lifetime ('a) + m = re.match(r"'(\\.|[^\\'])'", source[i:]) + if m: + blank(i, i + len(m.group(0))) + i += len(m.group(0)) + else: + i += 1 + else: + i += 1 + return "".join(out) + + +CFG_TEST_RE = re.compile(r"#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]") + + +def find_test_ranges(path: str) -> list[tuple[int, int]]: + """1-based inclusive (start, end) line ranges of #[cfg(test)] items.""" + try: + source = Path(path).read_text(encoding="utf-8") + except OSError as exc: + raise SourceError(f"cannot read {path}: {exc}") from exc + code = _strip_noncode(source) + lines = code.splitlines() + # map: does a line carry real code? + ranges: list[tuple[int, int]] = [] + i, n = 0, len(lines) + while i < n: + if CFG_TEST_RE.search(lines[i]): + j = i + 1 + # skip further attributes/comments between the attribute and item + while j < n and ( + lines[j].strip().startswith("#") or not lines[j].strip() + ): + if not lines[j].strip() and not source.splitlines()[j].strip(): + break + j += 1 + k = j + while k < n and "{" not in lines[k]: + k += 1 + if k >= n: + raise SourceError( + f"{path}: #[cfg(test)] at line {i + 1} has no braced item" + ) + depth, end = 0, -1 + for m in range(k, n): + depth += lines[m].count("{") - lines[m].count("}") + if depth == 0: + end = m + break + if end < 0: + raise SourceError( + f"{path}: unbalanced braces for #[cfg(test)] item at line {i + 1}" + ) + ranges.append((i + 1, end + 1)) + i = end + 1 + else: + i += 1 + # sanity: braces of the whole file must balance + whole = "\n".join(lines) + if whole.count("{") != whole.count("}"): + raise SourceError(f"{path}: unbalanced braces in file") + return ranges + + +def _norm_name(name: str) -> str: + """Normalize a mangled symbol: drop the crate disambiguator (lib dev_nav vs + bin dev) and collapse generic instantiations of one function.""" + n = CRATE_RE.sub("@", name) + cut = min((i for i in (n.find(m) for m in GENERIC_MARKERS) if i >= 0), + default=len(n)) + return n[:cut] + + +def analyze(export: dict) -> dict: + data = export["data"][0] + per_file: dict[str, dict] = {} + for fn in data["functions"]: + regions0 = [r for r in fn["regions"] if r[5] == 0] + if not regions0: + continue + fname = fn["filenames"][0] + ls = min(r[0] for r in regions0) + le = max(r[2] for r in regions0) + key = (ls, le, _norm_name(fn["name"])) + g = per_file.setdefault(fname, {}) + if key not in g: + g[key] = {"count": 0, "regions": [list(r) for r in regions0]} + cur = g[key] + cur["count"] = max(cur["count"], fn["count"]) + if len(cur["regions"]) == len(regions0): + for cr, nr in zip(cur["regions"], regions0): + cr[4] = max(cr[4], nr[4]) + + files_report = [] + totals = {"lines": [0, 0], "regions": [0, 0], "functions": [0, 0]} + for fname in sorted(per_file): + ranges = find_test_ranges(fname) + prod = { + k: v + for k, v in per_file[fname].items() + if not any(a <= k[0] <= b for a, b in ranges) + } + line_cov: dict[int, int] = {} + for (ls, le, _n), info in prod.items(): + for r in info["regions"]: + for line in range(r[0], r[2] + 1): + line_cov[line] = max(line_cov.get(line, 0), r[4]) + ln = len(line_cov) + lc = sum(1 for v in line_cov.values() if v > 0) + rn = sum(len(i["regions"]) for i in prod.values()) + rc = sum(1 for i in prod.values() for r in i["regions"] if r[4] > 0) + fnn = len(prod) + fc = sum(1 for i in prod.values() if i["count"] > 0) + files_report.append( + { + "file": os.path.basename(fname), + "lines": {"covered": lc, "count": ln}, + "regions": {"covered": rc, "count": rn}, + "functions": {"covered": fc, "count": fnn}, + } + ) + totals["lines"][0] += lc + totals["lines"][1] += ln + totals["regions"][0] += rc + totals["regions"][1] += rn + totals["functions"][0] += fc + totals["functions"][1] += fnn + return {"files": files_report, "totals": totals} + + +def _pct(covered: int, count: int) -> float: + return 100.0 * covered / count if count else 100.0 + + +def main(argv: list[str]) -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("export", help="cargo llvm-cov --json output file") + ap.add_argument("--threshold", type=float, default=80.0, + help="minimum production line coverage percent") + ap.add_argument("--regions-threshold", type=float, default=None, + help="minimum production region coverage percent " + "(defaults to --threshold)") + ap.add_argument("--json", dest="json_out", default=None, + help="write machine-readable summary to this path") + args = ap.parse_args(argv) + regions_threshold = ( + args.threshold if args.regions_threshold is None else args.regions_threshold + ) + + try: + with open(args.export, encoding="utf-8") as fh: + export = json.load(fh) + report = analyze(export) + except (OSError, KeyError, json.JSONDecodeError) as exc: + print(f"error: cannot analyze coverage export: {exc}", file=sys.stderr) + return 2 + except SourceError as exc: + print(f"error: ambiguous source analysis (failing closed): {exc}", + file=sys.stderr) + return 2 + + for f in report["files"]: + lp = _pct(f["lines"]["covered"], f["lines"]["count"]) + rp = _pct(f["regions"]["covered"], f["regions"]["count"]) + fp = _pct(f["functions"]["covered"], f["functions"]["count"]) + print( + f"{f['file']:12s} lines {f['lines']['covered']}/{f['lines']['count']}" + f" ({lp:.2f}%) regions {f['regions']['covered']}/{f['regions']['count']}" + f" ({rp:.2f}%) functions {f['functions']['covered']}/{f['functions']['count']}" + f" ({fp:.2f}%)" + ) + t = report["totals"] + lc, ln = t["lines"] + rc, rn = t["regions"] + fc, fnn = t["functions"] + lp, rp, fp = _pct(lc, ln), _pct(rc, rn), _pct(fc, fnn) + print(f"TOTAL lines {lc}/{ln} ({lp:.2f}%) regions {rc}/{rn} ({rp:.2f}%)" + f" functions {fc}/{fnn} ({fp:.2f}%)") + + if args.json_out: + summary = { + "lines": {"covered": lc, "count": ln, "percent": round(lp, 4)}, + "regions": {"covered": rc, "count": rn, "percent": round(rp, 4)}, + "functions": {"covered": fc, "count": fnn, "percent": round(fp, 4)}, + "thresholds": {"lines": args.threshold, "regions": regions_threshold}, + } + Path(args.json_out).write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8" + ) + + ok = True + if lp < args.threshold: + print(f"FAIL: production line coverage {lp:.2f}% < {args.threshold:.2f}%") + ok = False + if rp < regions_threshold: + print(f"FAIL: production region coverage {rp:.2f}% < {regions_threshold:.2f}%") + ok = False + if ok: + print(f"OK: production coverage meets thresholds " + f"(lines >= {args.threshold:.2f}%, regions >= {regions_threshold:.2f}%)") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/coverage/test_rust_production_coverage.py b/tests/coverage/test_rust_production_coverage.py new file mode 100644 index 0000000..0abeb90 --- /dev/null +++ b/tests/coverage/test_rust_production_coverage.py @@ -0,0 +1,298 @@ +"""Unit tests for scripts/rust-production-coverage.py. + +Run with: python -m unittest discover -s tests/coverage +""" + +import importlib.util +import json +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "rust-production-coverage.py" +spec = importlib.util.spec_from_file_location("rust_production_coverage", SCRIPT) +rpc = importlib.util.module_from_spec(spec) +spec.loader.exec_module(rpc) + + +def write_rs(directory: Path, name: str, source: str) -> str: + path = directory / name + path.write_text(source, encoding="utf-8") + return str(path) + + +class FindTestRangesTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.dir = Path(self._tmp.name) + + def ranges(self, source: str): + path = write_rs(self.dir, "sample.rs", source) + return rpc.find_test_ranges(path) + + def test_whole_tests_module_is_one_range(self): + source = "\n".join( + [ + "pub fn production_a() {}", + "", + "#[cfg(test)]", + "mod tests {", + " #[test]", + " fn t() {}", + "}", + "", + "pub fn production_b() {}", + ] + ) + # The module span covers the attribute line through the closing brace. + self.assertEqual(self.ranges(source), [(3, 7)]) + + def test_individual_cfg_test_helper(self): + source = "\n".join( + [ + "pub fn visible() {}", + "", + "#[cfg(test)]", + "fn fixture() -> u32 { 7 }", + "", + "pub fn other() {}", + ] + ) + self.assertEqual(self.ranges(source), [(3, 4)]) + + def test_production_before_and_after_is_kept(self): + source = "\n".join( + [ + "fn first() {}", + "#[cfg(test)]", + "mod tests { fn inner() {} }", + "fn last() {}", + ] + ) + ranges = self.ranges(source) + self.assertEqual(len(ranges), 1) + start, end = ranges[0] + self.assertLess(start, 4) + self.assertGreaterEqual(end, 3) + + def test_nested_braces_do_not_close_the_module_early(self): + source = "\n".join( + [ + "#[cfg(test)]", + "mod tests {", + " struct S { inner: Vec }", + " fn make() -> S {", + " let closure = |v: u8| { v + 1 };", + ' S { inner: vec![format!("{}", closure(1))] }', + " }", + "}", + "pub fn real() {}", + ] + ) + self.assertEqual(self.ranges(source), [(1, 8)]) + + def test_braces_inside_comments_are_ignored(self): + source = "\n".join( + [ + "pub fn a() {} // } unbalanced in line comment", + "/* { } /* nested comment braces */ */", + "#[cfg(test)]", + "mod tests {", + " // } fake close", + " /* } */ fn t() {}", + "}", + ] + ) + self.assertEqual(self.ranges(source), [(3, 7)]) + + def test_braces_inside_strings_are_ignored(self): + source = "\n".join( + [ + "#[cfg(test)]", + "mod tests {", + " fn t() {", + ' let s = "}{ \\" {";', + " let c = '{';", + " }", + "}", + ] + ) + self.assertEqual(self.ranges(source), [(1, 7)]) + + def test_braces_inside_raw_strings_are_ignored(self): + source = "\n".join( + [ + "#[cfg(test)]", + "mod tests {", + " fn t() {", + ' let raw = r#"{ " } "#;', + ' let raw2 = r##"#" { "##;', + " }", + "}", + ] + ) + self.assertEqual(self.ranges(source), [(1, 7)]) + + def test_multiple_test_only_items(self): + source = "\n".join( + [ + "#[cfg(test)]", + "fn helper_one() {}", + "", + "pub fn production() {}", + "", + "#[cfg(test)]", + "mod more_tests {", + " fn inner() {}", + "}", + ] + ) + self.assertEqual(self.ranges(source), [(1, 2), (6, 9)]) + + def test_malformed_unbalanced_module_fails_closed(self): + source = "#[cfg(test)]\nmod tests {\n fn broken() {\n" + with self.assertRaises(rpc.SourceError): + self.ranges(source) + + def test_malformed_unterminated_string_fails_closed(self): + source = '#[cfg(test)]\nmod tests { fn s() { let x = "unterminated; } }\n' + with self.assertRaises(rpc.SourceError): + self.ranges(source) + + +def make_export(functions): + """functions: list of (name, count, filename, [(sl, sc, el, ec, count)])""" + return { + "data": [ + { + "functions": [ + { + "name": name, + "count": count, + "filenames": [filename], + "regions": [ + [sl, sc, el, ec, cnt, 0, 0, 0] + for (sl, sc, el, ec, cnt) in regions + ], + } + for (name, count, filename, regions) in functions + ] + } + ] + } + + +class AnalyzeTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.dir = Path(self._tmp.name) + + def test_cfg_test_items_are_excluded_from_metrics(self): + path = write_rs( + self.dir, + "a.rs", + "\n".join( + [ + "pub fn production() {}", # line 1 + "#[cfg(test)]", + "mod tests {", + " fn t() {}", # line 4: test-only + "}", + ] + ), + ) + export = make_export( + [ + ("prod", 1, path, [(1, 1, 1, 26, 1)]), + ("test_t", 1, path, [(4, 5, 4, 14, 1)]), + ] + ) + report = rpc.analyze(export) + totals = report["totals"] + # Only the production function's 1 line / 1 region counts. + self.assertEqual(totals["lines"], [1, 1]) + self.assertEqual(totals["regions"], [1, 1]) + self.assertEqual(totals["functions"], [1, 1]) + + def test_uncovered_production_stays_in_the_denominator(self): + path = write_rs(self.dir, "a.rs", "pub fn production() {}\n") + export = make_export([("prod", 0, path, [(1, 1, 1, 26, 0)])]) + totals = rpc.analyze(export)["totals"] + self.assertEqual(totals["lines"], [0, 1]) + self.assertEqual(totals["regions"], [0, 1]) + + def test_same_source_duplicate_functions_are_deduplicated(self): + path = write_rs(self.dir, "a.rs", "pub fn production() {}\n") + # Same function compiled into the lib (dev_nav) and bin (dev) crates: + # different manglings, identical normalized name and regions. + export = make_export( + [ + ("Csaaaaaaaaaaa_1dev_nav3foo", 1, path, [(1, 1, 1, 26, 1)]), + ("Csaaaaaaaaaaa_1dev3foo", 1, path, [(1, 1, 1, 26, 1)]), + ] + ) + totals = rpc.analyze(export)["totals"] + self.assertEqual(totals["functions"], [1, 1]) + self.assertEqual(totals["lines"], [1, 1]) + + def test_generic_instantiations_collapse_to_one_function(self): + path = write_rs( + self.dir, + "a.rs", + "pub fn generic(x: T) -> T { x }\n", + ) + export = make_export( + [ + ("Csabc12345678_dev_navINtT_generic", 1, path, [(1, 1, 1, 33, 1)]), + ("Csabc12345678_dev_navINtU_generic", 0, path, [(1, 1, 1, 33, 0)]), + ] + ) + totals = rpc.analyze(export)["totals"] + self.assertEqual(totals["functions"], [1, 1]) + + +class MainThresholdTests(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.dir = Path(self._tmp.name) + self.rs = write_rs(self.dir, "a.rs", "pub fn production() {}\n") + # 1 covered line out of 2 total -> 50%. + self.export = make_export( + [ + ("covered", 1, self.rs, [(1, 1, 1, 26, 1)]), + ("missed", 0, self.rs, [(2, 1, 2, 26, 0)]), + ] + ) + self.export_path = self.dir / "export.json" + self.export_path.write_text(json.dumps(self.export), encoding="utf-8") + + def run_main(self, *extra): + argv = [str(self.export_path), *extra] + code = rpc.main(argv) + return code + + def test_threshold_pass_returns_zero(self): + self.assertEqual(self.run_main("--threshold", "40"), 0) + + def test_threshold_fail_returns_one(self): + self.assertEqual(self.run_main("--threshold", "99"), 1) + + def test_default_threshold_is_eighty(self): + # 50% < 80% default -> failure. + self.assertEqual(self.run_main(), 1) + + def test_ambiguous_source_fails_closed_with_two(self): + bad_rs = write_rs(self.dir, "bad.rs", "#[cfg(test)]\nmod tests {\n") + export = make_export([("f", 1, bad_rs, [(1, 1, 1, 10, 1)])]) + path = self.dir / "bad-export.json" + path.write_text(json.dumps(export), encoding="utf-8") + code = rpc.main([str(path), "--threshold", "10"]) + self.assertEqual(code, 2) + + +if __name__ == "__main__": + unittest.main() From 88540091ad1281779e8e8443eeb93b58678eaad6 Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 16 Aug 2026 19:38:43 +0200 Subject: [PATCH 2/3] ci: keep Rust CodeQL on Ubuntu after Windows extraction trial --- .github/workflows/codeql.yml | 16 ++++++---------- ASSURANCE.md | 9 +++++---- SECURITY.md | 13 ++++++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 55521d1..ccf260b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,11 +24,13 @@ jobs: fail-fast: false matrix: include: - # The Rust extractor runs on Windows because the crate is - # Windows-only (windows_sys / console APIs); extraction quality is - # significantly better against the real target platform. + # Rust runs the buildless extractor (build-mode: none). It was tried + # on windows-latest in #43 with no extraction improvement (9/10 + # files with macro-expansion errors on both platforms), so the + # simpler Ubuntu lane is kept. The residual is documented in + # SECURITY.md. - language: rust - os: windows-latest + os: ubuntu-latest build-mode: none - language: actions os: ubuntu-latest @@ -42,12 +44,6 @@ jobs: with: persist-credentials: false - - name: Install Rust toolchain - if: matrix.language == 'rust' - uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # upstream commit - with: - toolchain: 1.97.1 - - name: Initialize CodeQL uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: diff --git a/ASSURANCE.md b/ASSURANCE.md index c33152f..8285b41 100644 --- a/ASSURANCE.md +++ b/ASSURANCE.md @@ -63,10 +63,11 @@ pushes to `main` and pull requests. **Evidence.** `.github/workflows/codeql.yml`; results upload to security/code-scanning. -**Residual limitation.** Rust extraction runs in `build-mode: none`, which -cannot expand macros; a residual set of extraction errors remains for -macro-heavy files. A green CodeQL run is therefore not evidence of -extraction-clean analysis; see SECURITY.md for the current residual. +**Residual limitation.** Rust extraction runs the buildless extractor +(`build-mode: none`) and currently reports 9 of 10 files with macro-expansion +diagnostics; running the lane on `windows-latest` was tried and did not reduce +them. A green CodeQL run is therefore not evidence of extraction-clean +analysis. ## Fuzzing diff --git a/SECURITY.md b/SECURITY.md index f2d91ae..51c2183 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -21,11 +21,14 @@ and keep the reporter informed as we investigate. ## Security controls CodeQL analyzes Rust, GitHub Actions and JavaScript/TypeScript on pushes to -`main` and pull requests. The Rust lane runs on Windows (the only supported -target platform) in `build-mode: none`; because that mode does not compile the -crate, macro-heavy Rust files can still produce extraction diagnostics, which -are tracked rather than hidden. OpenSSF Scorecard runs on `main` and publishes -its current result through the repository badge. +`main` and pull requests. The Rust lane uses the buildless extractor +(`build-mode: none`) on Ubuntu; it was tried on `windows-latest` with no +extraction improvement, so the simpler lane is kept. Because buildless +extraction does not compile the crate, 9 of the 10 Rust files currently +produce macro-expansion diagnostics (`matches!`, `format!`, …); this residual +is an extractor limitation, is tracked rather than hidden, and a green CodeQL +run is not evidence of extraction-clean Rust analysis. OpenSSF Scorecard runs +on `main` and publishes its current result through the repository badge. Additional automated controls: From bdda4fa66f505de6f6f304892edf56044779fa7a Mon Sep 17 00:00:00 2001 From: Jacob Date: Sun, 16 Aug 2026 21:07:40 +0200 Subject: [PATCH 3/3] docs: correct Silver evidence wording --- ARCHITECTURE.md | 4 ++-- ASSURANCE.md | 9 +++++---- ROADMAP.md | 4 ++-- SECURITY.md | 3 ++- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e4567f5..065dea5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -80,8 +80,8 @@ channels derive from it: Single flat TSV file owned jointly by the Rust binary and the PowerShell module, read and written by both through stable key prefixes: `root`, -`show_favorites`, `check_updates`, `language`, `favorite\0t`, -`alias\0t\0t`, and `shortcut\0t\0t\0t` +`show_favorites`, `check_updates`, `language`, `favorite\t`, +`alias\t\t`, and `shortcut\t\t\t` (1–9, executed as Shift+digit in the TUI). The Rust writer escapes `%`, tab and newline and replaces the file atomically. diff --git a/ASSURANCE.md b/ASSURANCE.md index 8285b41..70c60a9 100644 --- a/ASSURANCE.md +++ b/ASSURANCE.md @@ -40,13 +40,13 @@ PowerShell integration, and the npm bootstrap, enforced in CI. **Residual limitation.** Rust coverage excludes host-bound code paths (`Terminal::enter/size/drop`, `read_key`, `Renderer::draw`, `run()/main()`, `detect_system_locale`) from the numerator only because they require a real -interactive console; they remain in the denominator and are exercised by -manual use. +interactive console; they remain in the denominator and are not automatically +covered by this suite. ## Dependency policy **Claim.** Dependencies are pinned (`Cargo.lock`), license-audited and -vulnerability-checked on every push. +vulnerability-checked in CI on pushes to `main` and pull requests. **Evidence.** `cargo deny check` (advisories, bans, licenses, sources) in the CI `validate` job; Dependabot configuration; the npm bootstrap has zero @@ -71,7 +71,8 @@ analysis. ## Fuzzing -**Claim.** The `config.tsv` parser is fuzzed continuously. +**Claim.** The `config.tsv` parser is fuzz-tested automatically on qualifying +pull requests. **Evidence.** ClusterFuzzLite (`.clusterfuzzlite/`, `.github/workflows/cflite_pr.yml`) on pull requests affecting its Rust source or fuzzing integration. diff --git a/ROADMAP.md b/ROADMAP.md index c0d4389..3457ae5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,8 +7,8 @@ and are not digital signatures. ## Current maintenance and quality baseline -- [x] Rust, PowerShell and npm bootstrap test suites green on every push and - pull request (pinned toolchains: Rust 1.97.1, Pester 6.1.0, +- [x] Rust, PowerShell and npm bootstrap test suites green in CI on pushes to + `main` and on pull requests (pinned toolchains: Rust 1.97.1, Pester 6.1.0, PSScriptAnalyzer 1.25.0, Node 22/24/26 with 24 as the release baseline). - [x] Coverage floors enforced in CI at >= 80% for all three languages: Rust production-only lines and regions, PowerShell commands and lines, and diff --git a/SECURITY.md b/SECURITY.md index 51c2183..e53db37 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,7 +32,8 @@ on `main` and publishes its current result through the repository badge. Additional automated controls: -- `cargo deny check` (advisories, bans, licenses, sources) on every push. +- `cargo deny check` (advisories, bans, licenses, sources) in CI on pushes to + `main` and pull requests. - ClusterFuzzLite fuzzing of the `config.tsv` parser on qualifying pull requests. - Coverage floors enforced in CI (>= 80%): Rust production-only lines and