diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 000000000..eb6778834 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,6 @@ +[profile.default] +fail-fast = false +retries = 0 +status-level = "fail" +final-status-level = "fail" +slow-timeout = { period = "60s", terminate-after = 2 } diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..05e98c72e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,14 @@ +[run] +branch = True +relative_files = True +source = + scripts.check_workspace_contract + scripts.check_docstrings + scripts.check_coverage + +[report] +fail_under = 100 +show_missing = True +skip_covered = False +exclude_lines = + pragma: no cover diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..32ed50f49 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,172 @@ +name: Rust Foundation CI + +on: + pull_request: + paths: + - "Cargo.toml" + - "rust-toolchain.toml" + - "deny.toml" + - "requirements-quality.txt" + - ".coveragerc" + - ".config/**" + - "crates/**" + - "scripts/**" + - "tests/quality/**" + - ".github/workflows/ci.yml" + push: + branches: + - main + paths: + - "Cargo.toml" + - "rust-toolchain.toml" + - "deny.toml" + - "requirements-quality.txt" + - ".coveragerc" + - ".config/**" + - "crates/**" + - "scripts/**" + - "tests/quality/**" + - ".github/workflows/ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: tepp-rust-foundation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: -Dwarnings + RUSTDOCFLAGS: -Dwarnings + CARGO_NEXTEST_VERSION: "0.9.140" + CARGO_DENY_VERSION: "0.19.7" + CARGO_LLVM_COV_VERSION: "0.8.6" + +jobs: + repository-contracts: + name: Repository contracts and Python branch coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact head + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + persist-credentials: false + - name: Install pinned Python quality dependency + run: python3 -m pip install --disable-pip-version-check --no-deps -r requirements-quality.txt + - name: Exercise repository tooling with branch coverage + run: python3 -m coverage run --branch -m unittest discover -s tests/quality -p 'test_*.py' + - name: Enforce repository tooling coverage + run: python3 -m coverage report --fail-under=100 --show-missing + - name: Validate workspace contract + run: python3 scripts/check_workspace_contract.py + - name: Validate Rust documentation contract + run: python3 scripts/check_docstrings.py + + rust-quality: + name: Format, lint, test, rustdoc, and dependency policy + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout exact head + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + persist-credentials: false + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.97.1 --profile minimal --component clippy --component rustfmt + - name: Restore pinned Rust quality tools + id: rust-tools-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.cargo/bin/cargo-nextest + ~/.cargo/bin/cargo-deny + key: ${{ runner.os }}-${{ runner.arch }}-tepp-rust-tools-nextest-${{ env.CARGO_NEXTEST_VERSION }}-deny-${{ env.CARGO_DENY_VERSION }} + - name: Install cargo-nextest + if: steps.rust-tools-cache.outputs.cache-hit != 'true' + run: cargo install cargo-nextest --locked --version "$CARGO_NEXTEST_VERSION" + - name: Install cargo-deny + if: steps.rust-tools-cache.outputs.cache-hit != 'true' + run: cargo install cargo-deny --locked --version "$CARGO_DENY_VERSION" + - name: Verify pinned Rust quality tool versions + run: | + cargo nextest --version | grep -F "$CARGO_NEXTEST_VERSION" + cargo deny --version | grep -F "$CARGO_DENY_VERSION" + - name: Check formatting + run: cargo fmt --all -- --check + - name: Compile all targets + run: cargo check --workspace --all-targets --all-features + - name: Run Clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: Run test suite without retries + run: cargo nextest run --workspace --all-features + - name: Run doctests separately + run: cargo test --doc --workspace --all-features + - name: Build warning-free documentation + run: cargo doc --workspace --all-features --no-deps + - name: Enforce dependency, license, advisory, and source policy + run: cargo deny check + + line-coverage: + name: Production line coverage + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout exact head + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + persist-credentials: false + - name: Install pinned Rust toolchain with LLVM tools + run: rustup toolchain install 1.97.1 --profile minimal --component llvm-tools-preview + - name: Restore pinned cargo-llvm-cov + id: llvm-cov-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cargo/bin/cargo-llvm-cov + key: ${{ runner.os }}-${{ runner.arch }}-tepp-cargo-llvm-cov-${{ env.CARGO_LLVM_COV_VERSION }} + - name: Install cargo-llvm-cov + if: steps.llvm-cov-cache.outputs.cache-hit != 'true' + run: cargo install cargo-llvm-cov --locked --version "$CARGO_LLVM_COV_VERSION" + - name: Verify pinned cargo-llvm-cov version + run: cargo llvm-cov --version | grep -F "$CARGO_LLVM_COV_VERSION" + - name: Generate exact line coverage + id: line-report + run: cargo llvm-cov --workspace --all-features --json --summary-only --output-path coverage.json + - name: Enforce complete line coverage + run: python3 scripts/check_coverage.py coverage.json --kind lines + - name: Show exact missing line diagnostics + if: ${{ failure() && steps.line-report.outcome == 'success' }} + run: cargo llvm-cov report --text --show-missing-lines + + branch-coverage: + name: Production branch coverage on pinned nightly + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout exact head + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + persist-credentials: false + - name: Install pinned nightly with LLVM tools + run: rustup toolchain install nightly-2026-08-01 --profile minimal --component llvm-tools-preview + - name: Restore pinned cargo-llvm-cov + id: llvm-cov-cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cargo/bin/cargo-llvm-cov + key: ${{ runner.os }}-${{ runner.arch }}-tepp-cargo-llvm-cov-${{ env.CARGO_LLVM_COV_VERSION }} + - name: Install cargo-llvm-cov + if: steps.llvm-cov-cache.outputs.cache-hit != 'true' + run: cargo install cargo-llvm-cov --locked --version "$CARGO_LLVM_COV_VERSION" + - name: Verify pinned cargo-llvm-cov version + run: cargo llvm-cov --version | grep -F "$CARGO_LLVM_COV_VERSION" + - name: Generate exact branch coverage + id: branch-report + run: cargo +nightly-2026-08-01 llvm-cov --branch --workspace --all-features --json --summary-only --output-path coverage-branches.json + - name: Enforce complete branch coverage + run: python3 scripts/check_coverage.py coverage-branches.json --kind branches + - name: Show exact missing branch diagnostics + if: ${{ failure() && steps.branch-report.outcome == 'success' }} + run: cargo +nightly-2026-08-01 llvm-cov report --branch --text --show-missing-lines diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2dac7290e --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target/ +/coverage/ +/.coverage +coverage.json +coverage-branches.json +__pycache__/ +*.py[cod] +.pytest_cache/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3d201e745..7332d1b0d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,6 +43,43 @@ flowchart LR Every boundary must be independently usable and expose versioned contracts for integration with organization repositories, `naruon`, and `contextual-orchestrator`. +## Implemented foundation topology + +Task 1 materializes the first storage-independent workspace boundaries. The +crate names are stable implementation identifiers, while the broader service +boundaries above remain the target modular MSA architecture. + +| Rust crate | Initial responsibility | +|---|---| +| `evidence_core` | immutable evidence domain primitives | +| `temporal_core` | typed clocks, intervals, and temporal reasoning | +| `event_core` | event instances, mentions, roles, and provenance | +| `relation_graph` | typed relations and forward-transition validation | +| `membership_core` | time-varying cross-classified multiple membership | +| `persistence_postgres` | PostgreSQL repositories and migrations | +| `corpus_split` | cutoff-safe, relation-aware partitioning | +| `tepp_simulation` | known-truth temporal/event data generation | +| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics | +| `tepp_api` | versioned DTO, schema, and export contracts | + +No crate exposes placeholder production behavior in Task 1. This prevents an +empty façade from becoming a de facto public API before its invariants and tests +exist. + +## Quality architecture + +The workspace centralizes package metadata and Rust/Clippy lints. Every member +inherits `unsafe_code = "forbid"`, `missing_docs = "deny"`, and warning denial. +Repository contract scripts independently verify the approved crate set, +workspace inheritance, action SHA pinning, absence of LLM credentials from +ordinary CI, and complete Rust documentation. + +Stable Rust 1.97.1 is the compile, lint, test, and line-coverage reference. +Branch coverage runs in a pinned nightly lane because LLVM branch coverage +remains unstable in Rust. `cargo-nextest` runs tests without retries, while +doctests remain a separate `cargo test --doc` gate. `cargo-deny` enforces +advisory, license, ban, and source policy. + ## Temporal invariants TEPP stores event/valid time, assertion time, document time, system time, available time, and knowledge cutoff independently. A historical analysis may include a document only when: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fca7b5ee..e4dde690d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - Topic correlation, consensus clustering, TDT, CHRONOS, and evidence-grounded LLM interpretation requirements. - APA 7th research traceability, source archive manifests, ADRs, governance, security, and contribution contracts. - Hourly centralized PR-maintenance workflow and a documented requirement for a future credential-separated NVIDIA NIM/OpenCode product-development loop. +- Rust 1.97.1 virtual Cargo workspace with ten explicit modular foundation crates. +- Repository contract, public-rustdoc, line-coverage, and nightly branch-coverage gates. +- Pinned `cargo-nextest` 0.9.140, `cargo-llvm-cov` 0.8.6, `cargo-deny` 0.19.7, and Coverage.py 7.15.2 quality tooling. +- Task 1 architecture decision and workspace-foundation validation report. +- Version-keyed, executable-only GitHub Actions caches for pinned Rust quality tools. ### Security @@ -22,10 +27,15 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang - Removed the bootstrap branch's credential-co-resident OpenCode workflow: no model process may receive repository-write authority, and scheduled product development remains disabled until proposal, independent verification, and late publication authority are separated across fresh jobs. - Removed completed bootstrap materializers, encoded payload fragments, readiness sentinels, and push probes from the reviewable tree. - Required full-commit GitHub Action pins, minimum permissions, concurrency controls, immutable audit evidence, SBOM, and provenance. +- Kept ordinary Rust CI free of LLM and reviewer credentials and disabled persisted checkout credentials. +- Refused to cache mutable Cargo registry, Git source, or target trees; cached quality binaries are keyed and checked by exact version. ### Quality - Required 100% production line and branch coverage and complete public API docstrings. - Required true-parameter recovery, RMSE, bias, interval coverage, temporal leakage, graph recovery, invariance, and CPU/GPU parity evidence. +- Added 100% statement and branch coverage for the repository quality-gate scripts. +- Made a zero executable-code coverage denominator explicit for the skeleton-only slice rather than treating it as evidence of implemented behavior. +- Denied warnings, missing public documentation, and unsafe Rust across the workspace. [Unreleased]: https://github.com/ContextualWisdomLab/TEPP/compare/HEAD...HEAD diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..67f4b5a6b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,53 @@ +[workspace] +resolver = "2" +members = [ + "crates/evidence_core", + "crates/temporal_core", + "crates/event_core", + "crates/relation_graph", + "crates/membership_core", + "crates/persistence_postgres", + "crates/corpus_split", + "crates/tepp_simulation", + "crates/validation_core", + "crates/tepp_api", +] +default-members = [ + "crates/evidence_core", + "crates/temporal_core", + "crates/event_core", + "crates/relation_graph", + "crates/membership_core", + "crates/persistence_postgres", + "crates/corpus_split", + "crates/tepp_simulation", + "crates/validation_core", + "crates/tepp_api", +] + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.97.1" +license = "Apache-2.0" +authors = ["Contextual Wisdom Lab"] +repository = "https://github.com/ContextualWisdomLab/TEPP" +homepage = "https://github.com/ContextualWisdomLab/TEPP" +readme = "README.md" +keywords = ["psychometrics", "temporal", "events", "topic-modeling", "rust"] +categories = ["science", "algorithms"] + +[workspace.lints.rust] +unsafe_code = "forbid" +missing_docs = "deny" +warnings = "deny" +rust_2018_idioms = { level = "deny", priority = -1 } +unused_lifetimes = "deny" +unused_qualifications = "deny" + +[workspace.lints.clippy] +all = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } +multiple_crate_versions = "allow" +module_name_repetitions = "allow" diff --git a/README.md b/README.md index 854d3572a..ae74015d3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,58 @@ # TEPP -Temporal Event Psychometrics Platform repository initialization. +TEPP is the **Temporal Event Psychometrics Platform**: a multilingual, temporal, +relational measurement system whose statistical and psychometric arithmetic is +implemented in Rust. + +## Current implementation state + +This branch establishes the Task 1 Rust workspace and quality-gate foundation. +The ten bounded crates compile independently but intentionally expose no +placeholder production APIs. Domain behavior begins in Task 2 with immutable +evidence identifiers and source records. + +```text +crates/evidence_core +crates/temporal_core +crates/event_core +crates/relation_graph +crates/membership_core +crates/persistence_postgres +crates/corpus_split +crates/tepp_simulation +crates/validation_core +crates/tepp_api +``` + +## Local verification + +```bash +python3 scripts/check_workspace_contract.py +python3 scripts/check_docstrings.py +python3 -m coverage run --branch -m unittest discover -s tests/quality -p 'test_*.py' +python3 -m coverage report --fail-under=100 --show-missing + +cargo fmt --all -- --check +cargo check --workspace --all-targets --all-features +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo nextest run --workspace --all-features +cargo test --doc --workspace --all-features +cargo doc --workspace --all-features --no-deps +cargo deny check +``` + +Stable Rust line coverage is measured with `cargo-llvm-cov`. Branch coverage is +measured in a separately pinned nightly lane because Rust branch coverage remains +an unstable compiler capability. A zero denominator is reported explicitly for +this skeleton-only slice; it must never conceal uncovered production behavior. + +## Normative documents + +- `AGENTS.md` +- `ARCHITECTURE.md` +- `docs/product/prd-v0.4-approved.md` +- `docs/superpowers/plans/2026-08-05-temporal-event-foundation.md` +- `docs/research/standards-and-literature.md` + +No release, production-readiness, GPU, database, or statistical-recovery claim is +made by this foundation slice. diff --git a/crates/corpus_split/Cargo.toml b/crates/corpus_split/Cargo.toml new file mode 100644 index 000000000..a1dc4dfac --- /dev/null +++ b/crates/corpus_split/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "corpus_split" +description = "Leakage-safe corpus snapshots and relation-aware data splits." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/corpus_split/src/lib.rs b/crates/corpus_split/src/lib.rs new file mode 100644 index 000000000..a45cc7d50 --- /dev/null +++ b/crates/corpus_split/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Leakage-safe corpus snapshots and relation-aware data splits. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/corpus_split/tests/crate_contract.rs b/crates/corpus_split/tests/crate_contract.rs new file mode 100644 index 000000000..d202216cb --- /dev/null +++ b/crates/corpus_split/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `corpus_split` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "corpus_split"); +} diff --git a/crates/event_core/Cargo.toml b/crates/event_core/Cargo.toml new file mode 100644 index 000000000..b01270562 --- /dev/null +++ b/crates/event_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "event_core" +description = "Versioned event instances, mentions, roles, subevents, and provenance." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/event_core/src/lib.rs b/crates/event_core/src/lib.rs new file mode 100644 index 000000000..880f8523e --- /dev/null +++ b/crates/event_core/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Versioned event instances, mentions, roles, subevents, and provenance. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/event_core/tests/crate_contract.rs b/crates/event_core/tests/crate_contract.rs new file mode 100644 index 000000000..91563e192 --- /dev/null +++ b/crates/event_core/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `event_core` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "event_core"); +} diff --git a/crates/evidence_core/Cargo.toml b/crates/evidence_core/Cargo.toml new file mode 100644 index 000000000..cf1ebfde5 --- /dev/null +++ b/crates/evidence_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "evidence_core" +description = "Immutable source-evidence identifiers, records, and exact spans." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/evidence_core/src/lib.rs b/crates/evidence_core/src/lib.rs new file mode 100644 index 000000000..bd1cb4f07 --- /dev/null +++ b/crates/evidence_core/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Immutable source-evidence identifiers, records, and exact spans. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/evidence_core/tests/crate_contract.rs b/crates/evidence_core/tests/crate_contract.rs new file mode 100644 index 000000000..6700a7782 --- /dev/null +++ b/crates/evidence_core/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `evidence_core` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "evidence_core"); +} diff --git a/crates/membership_core/Cargo.toml b/crates/membership_core/Cargo.toml new file mode 100644 index 000000000..5d6276d3f --- /dev/null +++ b/crates/membership_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "membership_core" +description = "Time-varying cross-classified and multiple-membership assignments." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/membership_core/src/lib.rs b/crates/membership_core/src/lib.rs new file mode 100644 index 000000000..4ff8c6615 --- /dev/null +++ b/crates/membership_core/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Time-varying cross-classified and multiple-membership assignments. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/membership_core/tests/crate_contract.rs b/crates/membership_core/tests/crate_contract.rs new file mode 100644 index 000000000..f0b1ea846 --- /dev/null +++ b/crates/membership_core/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `membership_core` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "membership_core"); +} diff --git a/crates/persistence_postgres/Cargo.toml b/crates/persistence_postgres/Cargo.toml new file mode 100644 index 000000000..8a1c5aab4 --- /dev/null +++ b/crates/persistence_postgres/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "persistence_postgres" +description = "PostgreSQL adapters for bitemporal TEPP domain contracts." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/persistence_postgres/src/lib.rs b/crates/persistence_postgres/src/lib.rs new file mode 100644 index 000000000..48807d144 --- /dev/null +++ b/crates/persistence_postgres/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! `PostgreSQL` adapters for bitemporal TEPP domain contracts. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/persistence_postgres/tests/crate_contract.rs b/crates/persistence_postgres/tests/crate_contract.rs new file mode 100644 index 000000000..22c3c49b0 --- /dev/null +++ b/crates/persistence_postgres/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `persistence_postgres` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "persistence_postgres"); +} diff --git a/crates/relation_graph/Cargo.toml b/crates/relation_graph/Cargo.toml new file mode 100644 index 000000000..e960f2f07 --- /dev/null +++ b/crates/relation_graph/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "relation_graph" +description = "Typed document, segment, event, entity, and transition relations." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/relation_graph/src/lib.rs b/crates/relation_graph/src/lib.rs new file mode 100644 index 000000000..d5a29616e --- /dev/null +++ b/crates/relation_graph/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Typed document, segment, event, entity, and transition relations. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/relation_graph/tests/crate_contract.rs b/crates/relation_graph/tests/crate_contract.rs new file mode 100644 index 000000000..01f2f4d5d --- /dev/null +++ b/crates/relation_graph/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `relation_graph` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "relation_graph"); +} diff --git a/crates/temporal_core/Cargo.toml b/crates/temporal_core/Cargo.toml new file mode 100644 index 000000000..d5160604d --- /dev/null +++ b/crates/temporal_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "temporal_core" +description = "Six-clock temporal values, uncertain intervals, and temporal relations." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/temporal_core/src/lib.rs b/crates/temporal_core/src/lib.rs new file mode 100644 index 000000000..22f9441d3 --- /dev/null +++ b/crates/temporal_core/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Six-clock temporal values, uncertain intervals, and temporal relations. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/temporal_core/tests/crate_contract.rs b/crates/temporal_core/tests/crate_contract.rs new file mode 100644 index 000000000..3aa799584 --- /dev/null +++ b/crates/temporal_core/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `temporal_core` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "temporal_core"); +} diff --git a/crates/tepp_api/Cargo.toml b/crates/tepp_api/Cargo.toml new file mode 100644 index 000000000..fdf8738be --- /dev/null +++ b/crates/tepp_api/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tepp_api" +description = "Versioned service DTOs, schemas, and export contracts." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs new file mode 100644 index 000000000..47f16386f --- /dev/null +++ b/crates/tepp_api/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Versioned service DTOs, schemas, and export contracts. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/tepp_api/tests/crate_contract.rs b/crates/tepp_api/tests/crate_contract.rs new file mode 100644 index 000000000..11a8d0caf --- /dev/null +++ b/crates/tepp_api/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `tepp_api` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "tepp_api"); +} diff --git a/crates/tepp_simulation/Cargo.toml b/crates/tepp_simulation/Cargo.toml new file mode 100644 index 000000000..252010e71 --- /dev/null +++ b/crates/tepp_simulation/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tepp_simulation" +description = "Realistic temporal and event truth simulation for recovery studies." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/tepp_simulation/src/lib.rs b/crates/tepp_simulation/src/lib.rs new file mode 100644 index 000000000..d1b957704 --- /dev/null +++ b/crates/tepp_simulation/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Realistic temporal and event truth simulation for recovery studies. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/tepp_simulation/tests/crate_contract.rs b/crates/tepp_simulation/tests/crate_contract.rs new file mode 100644 index 000000000..a209cb3f9 --- /dev/null +++ b/crates/tepp_simulation/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `tepp_simulation` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "tepp_simulation"); +} diff --git a/crates/validation_core/Cargo.toml b/crates/validation_core/Cargo.toml new file mode 100644 index 000000000..03424f3b4 --- /dev/null +++ b/crates/validation_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "validation_core" +description = "Recovery, calibration, graph, and Monte Carlo validation metrics." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/validation_core/src/lib.rs b/crates/validation_core/src/lib.rs new file mode 100644 index 000000000..d6106b7f0 --- /dev/null +++ b/crates/validation_core/src/lib.rs @@ -0,0 +1,6 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Recovery, calibration, graph, and Monte Carlo validation metrics. +//! +//! This crate intentionally exposes no production behavior in the workspace-foundation +//! slice. Domain APIs are introduced test-first in the corresponding implementation task. diff --git a/crates/validation_core/tests/crate_contract.rs b/crates/validation_core/tests/crate_contract.rs new file mode 100644 index 000000000..7f4c31626 --- /dev/null +++ b/crates/validation_core/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `validation_core` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "validation_core"); +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 000000000..964e4ef6b --- /dev/null +++ b/deny.toml @@ -0,0 +1,36 @@ +[graph] +all-features = true +exclude-dev = false + +[advisories] +version = 2 +yanked = "deny" +ignore = [] + +[licenses] +version = 2 +confidence-threshold = 0.8 +allow = [ + "Apache-2.0", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Zlib", +] +exceptions = [] + +[bans] +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +deny = [] +skip = [] +skip-tree = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/docs/adr/0007-rust-workspace-quality-gates.md b/docs/adr/0007-rust-workspace-quality-gates.md new file mode 100644 index 000000000..056c25e94 --- /dev/null +++ b/docs/adr/0007-rust-workspace-quality-gates.md @@ -0,0 +1,100 @@ +# ADR 0007: Explicit Rust workspace and exact quality gates + +- **Status:** Accepted +- **Date:** 2026-08-05 +- **Decision owners:** Contextual Wisdom Lab +- **Supersedes:** None + +## Context + +Every later TEPP estimator depends on stable crate boundaries, deterministic +tooling, complete documentation, and exact validation evidence. An implicit +`crates/*` workspace can silently absorb experimental packages. Warning-only +lints permit quality regressions. Stable Rust line coverage is available, but +Rust branch coverage remains an unstable LLVM/compiler capability and therefore +cannot be honestly represented as a stable-only gate. + +The initial planning repository contained no Rust workspace. Task 1 must create +the build and validation substrate without inventing placeholder APIs or +claiming that the temporal, event, database, GPU, or psychometric layers exist. + +## Decision + +1. Use a virtual Cargo workspace with an explicit ordered member list. +2. Pin the stable compiler to Rust 1.97.1, including the LLVM miscompilation + correction published by the Rust Release Team. +3. Centralize package metadata and lints. Every member inherits: + - `unsafe_code = "forbid"`; + - `missing_docs = "deny"`; + - warning denial; and + - strict Clippy `all`, `pedantic`, and `cargo` groups with documented, + minimal exceptions. +4. Create ten focused crates corresponding to the approved Temporal/Event + Foundation plan. Skeleton crates contain module-level rustdoc and no public + placeholder behavior. +5. Run `cargo-nextest` 0.9.140 without retries and run doctests separately. +6. Enforce stable line coverage with `cargo-llvm-cov` 0.8.6. +7. Enforce branch coverage with the same tool on pinned + `nightly-2026-08-01`; parse LLVM JSON and require `covered == count`. +8. Report a zero denominator explicitly. It is valid only while the slice has + no executable production units and must not be used as evidence that a + domain implementation exists. +9. Enforce advisories, licenses, dependency bans, and source origins with + `cargo-deny` 0.19.7. +10. Test repository-quality Python scripts at 100% statement and branch + coverage with pinned Coverage.py 7.15.2. +11. Require all GitHub Actions and reusable workflows to use a full commit SHA. +12. Cache only pinned Cargo quality-tool executables. Cache keys include the OS, + architecture, and exact tool versions; cached binaries are version-checked + before use. Mutable Cargo registry, Git source, and build-output trees are + deliberately excluded from the cache boundary. +13. Do not expose `NVIDIA_NIM_API_KEY`, reviewer credentials, publication + credentials, or any LLM secret to ordinary Rust CI. + +## Consequences + +- Crate boundaries and package identities are reviewable before domain logic is + introduced. +- A new crate cannot enter the workspace accidentally. +- Branch coverage is reproducible but depends on a separate nightly lane; stable + compiler behavior remains the numerical and build reference. +- Cold CI still compiles pinned quality tools once, while later commits in the + same protected PR lineage restore only verified binaries. Dependency source + trees and build outputs remain fresh and reviewable. +- The foundation PR cannot claim scientific correctness, database readiness, + GPU parity, or release readiness. Those require their later plan tasks and + exact-head evidence. + +## Validation + +- Repository contract tests cover valid and hostile workspace states. +- Public-rustdoc tests cover crate-level and item-level documentation. +- LLVM coverage JSON validation rejects malformed, impossible, incomplete, or + missing line/branch totals. +- CI runs formatting, compile, Clippy, nextest, doctest, rustdoc, + cargo-deny, stable line coverage, and nightly branch coverage. +- The CI contract rejects unpinned Actions, forbidden credentials, and mutable + Cargo registry/Git cache paths. + +## References + +The Cargo Team. (n.d.). *Workspaces*. In *The Cargo Book*. Retrieved August 5, +2026, from https://doc.rust-lang.org/cargo/reference/workspaces.html + +GitHub. (2026). *Cache* (Version 5.0.5) [GitHub Action]. +https://github.com/actions/cache + +The Rust Release Team. (2026, July 16). *Announcing Rust 1.97.1*. Rust Blog. +https://blog.rust-lang.org/2026/07/16/Rust-1.97.1/ + +Batchelder, N., & contributors. (2026). *Coverage.py* (Version 7.15.2) +[Computer software]. https://coverage.readthedocs.io/ + +Embark Studios. (2026). *cargo-deny* (Version 0.19.7) [Computer software]. +GitHub. https://github.com/EmbarkStudios/cargo-deny + +Endo, T. (2026). *cargo-llvm-cov* (Version 0.8.6) [Computer software]. GitHub. +https://github.com/taiki-e/cargo-llvm-cov + +Nextest contributors. (2026). *cargo-nextest* (Version 0.9.140) +[Computer software]. https://nexte.st/ diff --git a/docs/adr/README.md b/docs/adr/README.md index 969ec5b3f..79299e6f7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,5 +10,6 @@ TEPP uses numbered ADRs for decisions that constrain latent-variable meaning, te | 0004 | One shared multilingual latent space with explicit invariance status | | 0005 | Posterior-aware ESEM/DSEM and valid compositional coordinates | | 0006 | VRAM-adaptive GPU compute and NVIDIA NIM/OpenCode orchestration boundary | +| 0007 | Explicit Rust workspace, pinned toolchains, and exact quality gates | ADR status changes require a pull request, source traceability, tests for affected invariants, and corresponding PRD/architecture updates where the approved measurement target changes. diff --git a/docs/research/rust-quality-tooling.md b/docs/research/rust-quality-tooling.md new file mode 100644 index 000000000..e2d06ff6a --- /dev/null +++ b/docs/research/rust-quality-tooling.md @@ -0,0 +1,68 @@ +# Rust workspace and quality-tooling register + +This engineering doctoring note records the first-party sources and exact +versions used by Temporal/Event Foundation Task 1. It supplements +`standards-and-literature.md`; it does not replace the scientific references +required by later estimators. + +## Rust compiler + +TEPP pins Rust 1.97.1 for the stable build reference. The point release repairs +an LLVM optimization miscompilation and therefore supersedes 1.97.0 for this +foundation. A future compiler update requires exact-head formatting, Clippy, +rustdoc, test, coverage, and numerical-parity evidence before adoption. + +## Cargo workspace + +The workspace uses an explicit member list and workspace-inherited package +metadata and lints. Cargo's official workspace reference defines `members`, +`default-members`, and `[workspace.lints]`; TEPP does not use a wildcard member +glob because accidental crate admission would silently expand the trusted build +surface. + +## Test and coverage tooling + +- `cargo-nextest` 0.9.140 runs process-isolated tests without retries. +- Doctests run separately because nextest does not currently execute doctests. +- `cargo-llvm-cov` 0.8.6 produces stable line coverage. +- Branch coverage uses the same tool on `nightly-2026-08-01` because the + upstream project identifies Rust branch coverage as unstable and + nightly-only. +- Coverage thresholds are evaluated from LLVM JSON totals. A nonzero line or + branch denominator passes only when all units are covered. +- Coverage.py 7.15.2 measures the repository-quality Python scripts at 100% + statement and branch coverage. + +## Dependency policy + +`cargo-deny` 0.19.7 checks advisories, yanked packages, licenses, duplicate or +wildcard dependencies, and unapproved registries or Git sources. This check is +a policy gate rather than legal advice; procurement and release review still +own final license acceptance. + +## Security boundary + +Ordinary Rust CI has read-only repository permission, does not persist checkout +credentials, and receives no LLM, reviewer, publisher, or deployment secret. +The dedicated `NVIDIA_NIM_API_KEY` remains reserved for reviewed LLM workflows +and is not needed for deterministic foundation validation. + +## APA 7th references + +Batchelder, N., & contributors. (2026). *Coverage.py* (Version 7.15.2) +[Computer software]. https://coverage.readthedocs.io/ + +Embark Studios. (2026). *cargo-deny* (Version 0.19.7) [Computer software]. +GitHub. https://github.com/EmbarkStudios/cargo-deny + +Endo, T. (2026). *cargo-llvm-cov* (Version 0.8.6) [Computer software]. GitHub. +https://github.com/taiki-e/cargo-llvm-cov + +Nextest contributors. (2026). *cargo-nextest* (Version 0.9.140) +[Computer software]. https://nexte.st/ + +The Cargo Team. (n.d.). *Workspaces*. In *The Cargo Book*. Retrieved August 5, +2026, from https://doc.rust-lang.org/cargo/reference/workspaces.html + +The Rust Release Team. (2026, July 16). *Announcing Rust 1.97.1*. Rust Blog. +https://blog.rust-lang.org/2026/07/16/Rust-1.97.1/ diff --git a/docs/validation/workspace-foundation.md b/docs/validation/workspace-foundation.md new file mode 100644 index 000000000..14686d74f --- /dev/null +++ b/docs/validation/workspace-foundation.md @@ -0,0 +1,70 @@ +# Temporal/Event Foundation Task 1 validation + +## Scope + +This report covers only the Rust workspace and quality-gate foundation. It does +not validate temporal algebra, event ontology, PostgreSQL migrations, GPU +kernels, psychometric estimation, true-parameter recovery, deployment, or a +release artifact. + +## Test-first evidence + +The repository-contract tests were authored to fail on the pre-Task-1 state: +there was no root Cargo workspace, approved crate set, Rust CI workflow, +dependency policy, documentation gate, or coverage gate. The permanent tests +retain those fail-closed cases for missing manifests, missing crates, unpinned +Actions, forbidden credentials, undocumented Rust APIs, malformed LLVM coverage +JSON, impossible counts, and incomplete coverage. + +## Local verification + +The implementation environment did not provide a Rust toolchain, so Rust +compilation and LLVM coverage remain GitHub-hosted exact-head gates. The +repository tooling was executed locally: + +```text +python3 scripts/check_workspace_contract.py +TEPP workspace contract: PASS + +python3 scripts/check_docstrings.py +Rust documentation contract: PASS + +python3 -m unittest discover -s tests/quality -p 'test_*.py' +16 tests passed + +python3 -m coverage run --branch -m unittest discover -s tests/quality -p 'test_*.py' +python3 -m coverage report --show-missing +226 statements, 100% +110 branches, 100% +``` + +## Rust coverage interpretation + +Task 1 crate roots contain documentation and lint attributes but no executable +production behavior. LLVM may therefore report zero production lines or +branches. The coverage checker permits this only with an explicit +`0 executable units` message. It rejects every nonzero denominator unless +`covered == count`. + +This is a coverage property of the skeleton-only slice, not evidence that TEPP +has implemented its planned domain or statistical behavior. + +## Exact-head gates + +The PR is ready to merge only after all of the following succeed on its current +head: + +- repository contract and Python statement/branch coverage; +- Rust formatting, all-target compile, strict Clippy, and warning-free rustdoc; +- cargo-nextest without retries and separate doctests; +- cargo-deny advisory, license, ban, and source checks; +- stable Rust production line coverage; +- pinned-nightly production branch coverage; +- repository Security Scan, Semgrep, and independent review. + +## Next implementation slice + +Task 2 introduces immutable evidence identifiers, content hashes, source +artifacts, and exact UTF-8/page/layout spans. That behavior must begin with +failing unit and property tests and must turn the current zero coverage +denominator into measured executable production coverage. diff --git a/requirements-quality.txt b/requirements-quality.txt new file mode 100644 index 000000000..e389a676d --- /dev/null +++ b/requirements-quality.txt @@ -0,0 +1 @@ +coverage==7.15.2 diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..66708947e --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.97.1" +profile = "minimal" +components = ["clippy", "rustfmt", "llvm-tools-preview"] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 000000000..122d9b619 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1 @@ +"""Repository quality-gate helpers for TEPP.""" diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py new file mode 100644 index 000000000..a58d06e94 --- /dev/null +++ b/scripts/check_coverage.py @@ -0,0 +1,82 @@ +"""Fail closed unless an LLVM coverage report is exactly complete.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Iterable, Mapping, Sequence + + +def load_totals(path: Path) -> Mapping[str, Any]: + """Load the single-report totals mapping from LLVM coverage JSON.""" + + payload = json.loads(path.read_text(encoding="utf-8")) + data = payload.get("data") + if not isinstance(data, list) or len(data) != 1: + raise ValueError("coverage JSON must contain exactly one data entry") + totals = data[0].get("totals") + if not isinstance(totals, Mapping): + raise ValueError("coverage JSON data entry must contain totals") + return totals + + +def validate_kind(totals: Mapping[str, Any], kind: str) -> str: + """Return a stable success message or raise for incomplete *kind* coverage.""" + + summary = totals.get(kind) + if not isinstance(summary, Mapping): + raise ValueError(f"coverage totals do not contain {kind}") + count = summary.get("count") + covered = summary.get("covered") + if not isinstance(count, int) or not isinstance(covered, int): + raise ValueError(f"{kind} count and covered values must be integers") + if count < 0 or covered < 0 or covered > count: + raise ValueError(f"{kind} coverage counts are invalid") + if covered != count: + raise ValueError(f"{kind} coverage is incomplete: {covered}/{count}") + if count == 0: + return f"{kind} coverage: PASS (0 executable units in this foundation slice)" + return f"{kind} coverage: PASS ({covered}/{count}, 100%)" + + +def validate_report(path: Path, kinds: Sequence[str]) -> list[str]: + """Validate all requested coverage *kinds* in *path*.""" + + totals = load_totals(path) + return [validate_kind(totals, kind) for kind in kinds] + + +def build_parser() -> argparse.ArgumentParser: + """Create the command-line argument parser.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("report", type=Path) + parser.add_argument( + "--kind", + action="append", + choices=("lines", "branches"), + required=True, + dest="kinds", + ) + return parser + + +def main(arguments: Iterable[str] | None = None) -> int: + """Validate one LLVM coverage report.""" + + parser = build_parser() + namespace = parser.parse_args(list(arguments) if arguments is not None else None) + try: + messages = validate_report(namespace.report, namespace.kinds) + except (OSError, json.JSONDecodeError, ValueError) as error: + print(f"Coverage contract: FAIL: {error}", file=sys.stderr) + return 1 + for message in messages: + print(message) + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/scripts/check_docstrings.py b/scripts/check_docstrings.py new file mode 100644 index 000000000..ea11417cf --- /dev/null +++ b/scripts/check_docstrings.py @@ -0,0 +1,81 @@ +"""Enforce beginner-readable Rust documentation on TEPP public APIs.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Iterable, Sequence + +PUBLIC_ITEM_PATTERN = re.compile( + r"^\s*pub\s+" + r"(?:async\s+|const\s+|unsafe\s+|extern\s+)*" + r"(?:fn|struct|enum|trait|mod|type|const|static|use)\b" +) + + +def rust_sources(root: Path) -> list[Path]: + """Return production Rust source files in deterministic order.""" + + return sorted(root.glob("crates/*/src/**/*.rs")) + + +def validate_source(path: Path) -> list[str]: + """Return documentation violations in one Rust source file.""" + + lines = path.read_text(encoding="utf-8").splitlines() + errors: list[str] = [] + if not any(line.lstrip().startswith("//!") for line in lines): + errors.append(f"{path}: missing crate/module-level //! rustdoc") + + documented = False + for line_number, line in enumerate(lines, start=1): + stripped = line.strip() + if stripped.startswith("///") or stripped.startswith("#[doc"): + documented = True + continue + if stripped.startswith("#[") or not stripped: + continue + if PUBLIC_ITEM_PATTERN.match(line): + if not documented: + errors.append(f"{path}:{line_number}: public item lacks /// rustdoc") + documented = False + continue + documented = False + return errors + + +def validate_repository(root: Path) -> list[str]: + """Return all Rust documentation violations under *root*.""" + + sources = rust_sources(root) + if not sources: + return ["no production Rust source files were found"] + errors: list[str] = [] + for source_path in sources: + errors.extend(validate_source(source_path)) + return errors + + +def print_errors(errors: Sequence[str]) -> int: + """Print *errors* and return a conventional process exit code.""" + + if not errors: + print("Rust documentation contract: PASS") + return 0 + print("Rust documentation contract: FAIL", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + +def main(arguments: Iterable[str] | None = None) -> int: + """Validate the repository root supplied as the first argument.""" + + supplied = list(arguments if arguments is not None else sys.argv[1:]) + root = Path(supplied[0] if supplied else ".").resolve() + return print_errors(validate_repository(root)) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py new file mode 100644 index 000000000..c7b1ecf58 --- /dev/null +++ b/scripts/check_workspace_contract.py @@ -0,0 +1,245 @@ +"""Validate the TEPP Rust workspace and repository quality contracts. + +The checker deliberately uses only Python's standard library so it can run before +the Rust workspace or third-party quality tools are installed. +""" + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path +from typing import Any, Iterable, Mapping, Sequence + +EXPECTED_CRATES: tuple[str, ...] = ( + "evidence_core", + "temporal_core", + "event_core", + "relation_graph", + "membership_core", + "persistence_postgres", + "corpus_split", + "tepp_simulation", + "validation_core", + "tepp_api", +) + +REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( + "cargo fmt --all -- --check", + "cargo clippy --workspace --all-targets --all-features -- -D warnings", + "cargo nextest run --workspace --all-features", + "cargo test --doc --workspace --all-features", + "cargo doc --workspace --all-features --no-deps", + "cargo deny check", + "cargo llvm-cov --workspace --all-features", + "python3 scripts/check_docstrings.py", + "python3 scripts/check_coverage.py", + "Restore pinned Rust quality tools", + "Verify pinned Rust quality tool versions", + "Restore pinned cargo-llvm-cov", + "Verify pinned cargo-llvm-cov version", +) + +ACTION_PATTERN = re.compile(r"^\s*uses:\s*([^\s#]+)@([^\s#]+)", re.MULTILINE) +FULL_SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{40}$") +PLACEHOLDER_PATTERN = re.compile( + r"(?:\bpub\b[^\n]*(?:Placeholder|placeholder)|\b(?:todo|unimplemented)!\s*\()" +) + + +def load_toml(path: Path) -> Mapping[str, Any]: + """Load one UTF-8 TOML document from *path*.""" + + with path.open("rb") as stream: + return tomllib.load(stream) + + +def expected_member_paths() -> list[str]: + """Return the canonical ordered workspace member paths.""" + + return [f"crates/{crate_name}" for crate_name in EXPECTED_CRATES] + + +def validate_workspace(root: Path) -> list[str]: + """Return every workspace-contract violation below *root*. + + The result is deterministic and sorted by validation order so CI diagnostics + remain stable across operating systems. + """ + + errors: list[str] = [] + root_manifest_path = root / "Cargo.toml" + if not root_manifest_path.is_file(): + return ["Cargo.toml is missing"] + + root_manifest = load_toml(root_manifest_path) + workspace = _mapping(root_manifest.get("workspace")) + package_defaults = _mapping(root_manifest.get("workspace", {}).get("package")) + rust_lints = _mapping( + root_manifest.get("workspace", {}).get("lints", {}).get("rust") + ) + + expected_members = expected_member_paths() + if workspace.get("resolver") != "2": + errors.append("workspace resolver must be 2") + if workspace.get("members") != expected_members: + errors.append("workspace members must exactly match the approved crate list") + if workspace.get("default-members") != expected_members: + errors.append("workspace default-members must exactly match workspace members") + if package_defaults.get("edition") != "2024": + errors.append("workspace edition must be 2024") + if package_defaults.get("rust-version") != "1.97.1": + errors.append("workspace rust-version must be 1.97.1") + if package_defaults.get("license") != "Apache-2.0": + errors.append("workspace license must be Apache-2.0") + if rust_lints.get("unsafe_code") != "forbid": + errors.append("workspace must forbid unsafe_code") + if rust_lints.get("missing_docs") != "deny": + errors.append("workspace must deny missing_docs") + if rust_lints.get("warnings") != "deny": + errors.append("workspace must deny warnings") + + for crate_name in EXPECTED_CRATES: + errors.extend(_validate_crate(root, crate_name)) + + errors.extend(_validate_ci_contract(root)) + errors.extend(_validate_action_pins(root / ".github" / "workflows")) + return errors + + +def _mapping(value: Any) -> Mapping[str, Any]: + """Return *value* as a mapping, or an empty mapping for other values.""" + + return value if isinstance(value, Mapping) else {} + + +def _validate_crate(root: Path, crate_name: str) -> list[str]: + """Return contract violations for one workspace crate.""" + + errors: list[str] = [] + crate_root = root / "crates" / crate_name + manifest_path = crate_root / "Cargo.toml" + library_path = crate_root / "src" / "lib.rs" + test_path = crate_root / "tests" / "crate_contract.rs" + + if not manifest_path.is_file(): + return [f"{manifest_path.relative_to(root)} is missing"] + + manifest = load_toml(manifest_path) + package = _mapping(manifest.get("package")) + if package.get("name") != crate_name: + errors.append(f"{crate_name}: package.name must match its directory") + if package.get("publish") is not False: + errors.append(f"{crate_name}: publish must be false") + if _mapping(manifest.get("lints")).get("workspace") is not True: + errors.append(f"{crate_name}: lints.workspace must be true") + for inherited_field in ( + "version", + "edition", + "rust-version", + "license", + "authors", + "repository", + "homepage", + "readme", + "keywords", + "categories", + ): + if package.get(inherited_field, {}).get("workspace") is not True: + errors.append(f"{crate_name}: {inherited_field} must inherit from workspace") + + if not library_path.is_file(): + errors.append(f"{crate_name}: src/lib.rs is missing") + else: + library_text = library_path.read_text(encoding="utf-8") + if "//! " not in library_text: + errors.append(f"{crate_name}: crate-level rustdoc is missing") + if "#![forbid(unsafe_code)]" not in library_text: + errors.append(f"{crate_name}: unsafe_code is not explicitly forbidden") + if "#![deny(missing_docs)]" not in library_text: + errors.append(f"{crate_name}: missing_docs is not explicitly denied") + if _contains_placeholder_api(library_text): + errors.append(f"{crate_name}: placeholder production APIs are prohibited") + + if not test_path.is_file(): + errors.append(f"{crate_name}: package identity contract test is missing") + return errors + + +def _contains_placeholder_api(source: str) -> bool: + """Return whether *source* exposes or executes placeholder behavior.""" + + return bool(PLACEHOLDER_PATTERN.search(source)) + + +def _validate_ci_contract(root: Path) -> list[str]: + """Return violations in the Task 1 CI workflow and toolchain files.""" + + errors: list[str] = [] + ci_path = root / ".github" / "workflows" / "ci.yml" + toolchain_path = root / "rust-toolchain.toml" + deny_path = root / "deny.toml" + + if not ci_path.is_file(): + return [".github/workflows/ci.yml is missing"] + + ci_text = ci_path.read_text(encoding="utf-8") + for snippet in REQUIRED_CI_SNIPPETS: + if snippet not in ci_text: + errors.append(f"CI workflow is missing required command: {snippet}") + if "COPILOT_GITHUB_TOKEN" in ci_text: + errors.append("CI workflow must not reference COPILOT_GITHUB_TOKEN") + if "NVIDIA_NIM_API_KEY" in ci_text: + errors.append("Task 1 CI must not receive an LLM credential") + if "~/.cargo/registry" in ci_text or "~/.cargo/git" in ci_text: + errors.append("CI must not cache mutable Cargo registry or Git source trees") + if not toolchain_path.is_file(): + errors.append("rust-toolchain.toml is missing") + if not deny_path.is_file(): + errors.append("deny.toml is missing") + return errors + + +def _validate_action_pins(workflow_root: Path) -> list[str]: + """Return unpinned GitHub Action and reusable-workflow references.""" + + errors: list[str] = [] + if not workflow_root.is_dir(): + return [".github/workflows directory is missing"] + for workflow_path in sorted(workflow_root.glob("*.y*ml")): + workflow_text = workflow_path.read_text(encoding="utf-8") + if "COPILOT_GITHUB_TOKEN" in workflow_text: + errors.append( + f"{workflow_path.name}: COPILOT_GITHUB_TOKEN is prohibited" + ) + for action_name, action_ref in ACTION_PATTERN.findall(workflow_text): + if not FULL_SHA_PATTERN.fullmatch(action_ref): + errors.append( + f"{workflow_path.name}: {action_name} must use a full commit SHA" + ) + return errors + + +def print_errors(errors: Sequence[str]) -> int: + """Print *errors* and return a conventional process exit code.""" + + if not errors: + print("TEPP workspace contract: PASS") + return 0 + print("TEPP workspace contract: FAIL", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + +def main(arguments: Iterable[str] | None = None) -> int: + """Validate a repository root supplied as the first argument.""" + + supplied = list(arguments if arguments is not None else sys.argv[1:]) + root = Path(supplied[0] if supplied else ".").resolve() + return print_errors(validate_workspace(root)) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/tests/quality/__init__.py b/tests/quality/__init__.py new file mode 100644 index 000000000..3ab98ecb2 --- /dev/null +++ b/tests/quality/__init__.py @@ -0,0 +1 @@ +"""Quality-gate tests for TEPP repository tooling.""" diff --git a/tests/quality/test_check_coverage.py b/tests/quality/test_check_coverage.py new file mode 100644 index 000000000..623806aa9 --- /dev/null +++ b/tests/quality/test_check_coverage.py @@ -0,0 +1,176 @@ +"""Tests for exact LLVM coverage report enforcement.""" + +from __future__ import annotations + +import contextlib +import io +import json +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import check_coverage as coverage_contract + + +class CoverageContractTests(unittest.TestCase): + """Exercise valid, incomplete, and malformed LLVM coverage reports.""" + + @staticmethod + def write_report(directory: str, payload: object) -> Path: + """Write *payload* as JSON and return its path.""" + + path = Path(directory) / "coverage.json" + path.write_text(json.dumps(payload), encoding="utf-8") + return path + + @staticmethod + def payload( + *, + line_count: int = 2, + line_covered: int = 2, + branch_count: int = 1, + branch_covered: int = 1, + ) -> dict[str, object]: + """Return a minimal LLVM coverage payload.""" + + return { + "data": [ + { + "totals": { + "lines": {"count": line_count, "covered": line_covered}, + "branches": { + "count": branch_count, + "covered": branch_covered, + }, + } + } + ] + } + + def test_complete_and_zero_denominator_coverage(self) -> None: + """Exact coverage passes and empty foundation code is explicit.""" + + totals = self.payload()["data"][0]["totals"] # type: ignore[index] + self.assertEqual( + coverage_contract.validate_kind(totals, "lines"), # type: ignore[arg-type] + "lines coverage: PASS (2/2, 100%)", + ) + zero_totals = self.payload( + line_count=0, + line_covered=0, + branch_count=0, + branch_covered=0, + )["data"][0]["totals"] # type: ignore[index] + self.assertIn( + "0 executable units", + coverage_contract.validate_kind(zero_totals, "branches"), # type: ignore[arg-type] + ) + + def test_incomplete_and_malformed_summaries_fail(self) -> None: + """Missing, nonnumeric, impossible, and incomplete counts are rejected.""" + + with self.assertRaisesRegex(ValueError, "do not contain lines"): + coverage_contract.validate_kind({}, "lines") + with self.assertRaisesRegex(ValueError, "must be integers"): + coverage_contract.validate_kind( + {"lines": {"count": "one", "covered": 1}}, "lines" + ) + for count, covered in ((-1, 0), (1, -1), (1, 2)): + with self.subTest(count=count, covered=covered): + with self.assertRaisesRegex(ValueError, "counts are invalid"): + coverage_contract.validate_kind( + {"lines": {"count": count, "covered": covered}}, "lines" + ) + with self.assertRaisesRegex(ValueError, "incomplete"): + coverage_contract.validate_kind( + {"lines": {"count": 2, "covered": 1}}, "lines" + ) + with self.assertRaisesRegex(ValueError, "do not contain branches"): + coverage_contract.validate_kind({"branches": []}, "branches") + + def test_report_shape_validation(self) -> None: + """LLVM JSON must contain one data object with a totals mapping.""" + + with tempfile.TemporaryDirectory() as temporary: + for payload in ( + {}, + {"data": "wrong"}, + {"data": []}, + {"data": [{}, {}]}, + ): + with self.subTest(payload=payload): + path = self.write_report(temporary, payload) + with self.assertRaisesRegex(ValueError, "one data entry"): + coverage_contract.load_totals(path) + path = self.write_report(temporary, {"data": [{"totals": []}]}) + with self.assertRaisesRegex(ValueError, "contain totals"): + coverage_contract.load_totals(path) + + def test_validate_report_and_main(self) -> None: + """The CLI validates requested kinds and reports stable diagnostics.""" + + with tempfile.TemporaryDirectory() as temporary: + path = self.write_report(temporary, self.payload()) + self.assertEqual( + coverage_contract.validate_report(path, ["lines", "branches"]), + [ + "lines coverage: PASS (2/2, 100%)", + "branches coverage: PASS (1/1, 100%)", + ], + ) + standard_output = io.StringIO() + with contextlib.redirect_stdout(standard_output): + self.assertEqual( + coverage_contract.main( + [str(path), "--kind", "lines", "--kind", "branches"] + ), + 0, + ) + self.assertIn("branches coverage", standard_output.getvalue()) + + invalid_path = Path(temporary) / "invalid.json" + invalid_path.write_text("{", encoding="utf-8") + standard_error = io.StringIO() + with contextlib.redirect_stderr(standard_error): + self.assertEqual( + coverage_contract.main([str(invalid_path), "--kind", "lines"]), + 1, + ) + self.assertIn("FAIL", standard_error.getvalue()) + + missing_path = Path(temporary) / "missing.json" + with contextlib.redirect_stderr(io.StringIO()): + self.assertEqual( + coverage_contract.main([str(missing_path), "--kind", "lines"]), + 1, + ) + + incomplete_path = self.write_report( + temporary, self.payload(line_covered=1) + ) + with contextlib.redirect_stderr(io.StringIO()): + self.assertEqual( + coverage_contract.main([str(incomplete_path), "--kind", "lines"]), + 1, + ) + + def test_parser_and_default_argument_source(self) -> None: + """The parser contract and sys.argv execution path remain usable.""" + + parser = coverage_contract.build_parser() + with tempfile.TemporaryDirectory() as temporary: + path = self.write_report(temporary, self.payload()) + namespace = parser.parse_args([str(path), "--kind", "lines"]) + self.assertEqual(namespace.report, path) + self.assertEqual(namespace.kinds, ["lines"]) + with mock.patch.object( + sys, "argv", ["checker", str(path), "--kind", "lines"] + ): + with contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(coverage_contract.main(None), 0) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py new file mode 100644 index 000000000..2fc80b51a --- /dev/null +++ b/tests/quality/test_check_docstrings.py @@ -0,0 +1,89 @@ +"""Tests for Rust public-API documentation validation.""" + +from __future__ import annotations + +import contextlib +import io +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import check_docstrings as docstrings + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + + +class DocstringContractTests(unittest.TestCase): + """Exercise Rust documentation discovery and validation.""" + + def test_live_repository_is_documented(self) -> None: + """Every foundation crate contains crate-level rustdoc.""" + + sources = docstrings.rust_sources(REPOSITORY_ROOT) + self.assertEqual(len(sources), 10) + self.assertEqual(docstrings.validate_repository(REPOSITORY_ROOT), []) + + def test_missing_sources_fail_closed(self) -> None: + """A repository with no production Rust source cannot pass.""" + + with tempfile.TemporaryDirectory() as temporary: + self.assertEqual( + docstrings.validate_repository(Path(temporary)), + ["no production Rust source files were found"], + ) + + def test_documented_and_undocumented_items(self) -> None: + """Attributes and whitespace do not detach rustdoc from public items.""" + + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "lib.rs" + source.write_text( + "//! Module docs.\n" + "\n" + "/// A documented structure.\n" + "#[derive(Debug)]\n" + "pub struct Documented;\n" + "\n" + "pub fn undocumented() {}\n" + "\n" + "/// A documented constant.\n" + "#[doc = \"Additional documentation.\"]\n" + "pub const VALUE: usize = 1;\n", + encoding="utf-8", + ) + errors = docstrings.validate_source(source) + self.assertEqual(len(errors), 1) + self.assertIn("public item lacks", errors[0]) + + def test_missing_module_docs_are_reported(self) -> None: + """Crate or module documentation is mandatory.""" + + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "lib.rs" + source.write_text("fn private_item() {}\n", encoding="utf-8") + errors = docstrings.validate_source(source) + self.assertEqual(errors, [f"{source}: missing crate/module-level //! rustdoc"]) + + def test_print_and_main_exit_codes(self) -> None: + """Reporting succeeds for clean repositories and fails for empty ones.""" + + standard_output = io.StringIO() + with contextlib.redirect_stdout(standard_output): + self.assertEqual(docstrings.print_errors([]), 0) + self.assertIn("PASS", standard_output.getvalue()) + + standard_error = io.StringIO() + with contextlib.redirect_stderr(standard_error): + self.assertEqual(docstrings.print_errors(["problem"]), 1) + self.assertIn("problem", standard_error.getvalue()) + + self.assertEqual(docstrings.main([str(REPOSITORY_ROOT)]), 0) + with mock.patch.object(sys, "argv", ["checker", str(REPOSITORY_ROOT)]): + self.assertEqual(docstrings.main(None), 0) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/tests/quality/test_check_workspace_contract.py b/tests/quality/test_check_workspace_contract.py new file mode 100644 index 000000000..6f3d9c204 --- /dev/null +++ b/tests/quality/test_check_workspace_contract.py @@ -0,0 +1,209 @@ +"""Tests for the TEPP workspace contract checker.""" + +from __future__ import annotations + +import contextlib +import io +import shutil +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import check_workspace_contract as contract + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + + +class WorkspaceContractTests(unittest.TestCase): + """Exercise successful and fail-closed workspace validation.""" + + def copy_repository(self) -> tuple[tempfile.TemporaryDirectory[str], Path]: + """Copy the repository to a disposable test directory.""" + + temporary = tempfile.TemporaryDirectory() + destination = Path(temporary.name) / "repository" + shutil.copytree( + REPOSITORY_ROOT, + destination, + ignore=shutil.ignore_patterns( + "target", ".git", ".coverage", "__pycache__", "*.pyc" + ), + ) + return temporary, destination + + def test_live_repository_satisfies_contract(self) -> None: + """The committed workspace satisfies every repository contract.""" + + self.assertEqual(contract.validate_workspace(REPOSITORY_ROOT), []) + self.assertEqual( + contract.expected_member_paths(), + [f"crates/{name}" for name in contract.EXPECTED_CRATES], + ) + self.assertFalse(contract._contains_placeholder_api("//! documented\n")) + self.assertFalse( + contract._contains_placeholder_api("/// Real API.\npub struct EvidenceId;\n") + ) + self.assertTrue( + contract._contains_placeholder_api("pub struct Placeholder;\n") + ) + self.assertTrue( + contract._contains_placeholder_api("pub fn run() { todo!() }\n") + ) + self.assertTrue( + contract._contains_placeholder_api("fn private() { unimplemented!() }\n") + ) + self.assertEqual(contract._mapping({"key": "value"}), {"key": "value"}) + self.assertEqual(contract._mapping("not-a-table"), {}) + + def test_missing_root_manifest_fails_closed(self) -> None: + """A repository without a root manifest cannot pass.""" + + with tempfile.TemporaryDirectory() as temporary: + self.assertEqual( + contract.validate_workspace(Path(temporary)), + ["Cargo.toml is missing"], + ) + + def test_invalid_root_and_crate_contracts_are_reported(self) -> None: + """Root, package, source, and test drift is reported in one pass.""" + + temporary, repository = self.copy_repository() + self.addCleanup(temporary.cleanup) + + root_manifest = (repository / "Cargo.toml").read_text(encoding="utf-8") + replacements = { + 'resolver = "2"': 'resolver = "1"', + '"crates/tepp_api"': '"crates/unapproved_api"', + 'edition = "2024"': 'edition = "2021"', + 'rust-version = "1.97.1"': 'rust-version = "1.96.0"', + 'license = "Apache-2.0"': 'license = "MIT"', + 'unsafe_code = "forbid"': 'unsafe_code = "allow"', + 'missing_docs = "deny"': 'missing_docs = "warn"', + 'warnings = "deny"': 'warnings = "warn"', + } + for before, after in replacements.items(): + root_manifest = root_manifest.replace(before, after) + (repository / "Cargo.toml").write_text(root_manifest, encoding="utf-8") + + crate_root = repository / "crates" / "evidence_core" + manifest = (crate_root / "Cargo.toml").read_text(encoding="utf-8") + manifest = manifest.replace( + 'name = "evidence_core"', 'name = "wrong_package"' + ) + manifest = manifest.replace("publish = false", "publish = true") + manifest = manifest.replace("workspace = true", "workspace = false") + for inherited in ( + "version.workspace = true\n", + "edition.workspace = true\n", + "rust-version.workspace = true\n", + "license.workspace = true\n", + "authors.workspace = true\n", + "repository.workspace = true\n", + "homepage.workspace = true\n", + "readme.workspace = true\n", + "keywords.workspace = true\n", + "categories.workspace = true\n", + ): + manifest = manifest.replace(inherited, "") + (crate_root / "Cargo.toml").write_text(manifest, encoding="utf-8") + (crate_root / "src" / "lib.rs").write_text( + "pub struct Placeholder;\n", encoding="utf-8" + ) + (crate_root / "tests" / "crate_contract.rs").unlink() + shutil.rmtree(repository / "crates" / "temporal_core") + + errors = contract.validate_workspace(repository) + expected_fragments = ( + "workspace resolver", + "workspace members", + "workspace default-members", + "workspace edition", + "workspace rust-version", + "workspace license", + "forbid unsafe_code", + "deny missing_docs", + "deny warnings", + "package.name", + "publish must be false", + "lints.workspace", + "must inherit from workspace", + "crate-level rustdoc", + "unsafe_code is not explicitly forbidden", + "missing_docs is not explicitly denied", + "placeholder production APIs", + "package identity contract test", + "temporal_core/Cargo.toml is missing", + ) + for fragment in expected_fragments: + self.assertTrue( + any(fragment in error for error in errors), + f"missing diagnostic containing {fragment!r}: {errors}", + ) + + def test_missing_library_and_ci_assets_are_reported(self) -> None: + """Missing source, CI, toolchain, and policy files are rejected.""" + + temporary, repository = self.copy_repository() + self.addCleanup(temporary.cleanup) + (repository / "crates" / "event_core" / "src" / "lib.rs").unlink() + (repository / ".github" / "workflows" / "ci.yml").unlink() + + errors = contract.validate_workspace(repository) + self.assertIn("event_core: src/lib.rs is missing", errors) + self.assertIn(".github/workflows/ci.yml is missing", errors) + + ci_path = repository / ".github" / "workflows" / "ci.yml" + ci_path.write_text( + "uses: actions/checkout@v4\n" + "env:\n" + " COPILOT_GITHUB_TOKEN: forbidden\n" + " NVIDIA_NIM_API_KEY: forbidden\n" + " CACHE_PATH: ~/.cargo/registry\n", + encoding="utf-8", + ) + (repository / "rust-toolchain.toml").unlink() + (repository / "deny.toml").unlink() + errors = contract.validate_workspace(repository) + self.assertTrue(any("required command" in error for error in errors)) + self.assertTrue( + any("must not reference COPILOT_GITHUB_TOKEN" in error for error in errors) + ) + self.assertTrue( + any("must not receive an LLM credential" in error for error in errors) + ) + self.assertTrue(any("must not cache mutable Cargo" in error for error in errors)) + self.assertIn("rust-toolchain.toml is missing", errors) + self.assertIn("deny.toml is missing", errors) + self.assertTrue(any("full commit SHA" in error for error in errors)) + self.assertTrue(any("COPILOT_GITHUB_TOKEN is prohibited" in error for error in errors)) + + def test_action_pin_validator_handles_absent_directory(self) -> None: + """The action-pin validator fails closed when workflows are absent.""" + + with tempfile.TemporaryDirectory() as temporary: + errors = contract._validate_action_pins(Path(temporary) / "workflows") + self.assertEqual(errors, [".github/workflows directory is missing"]) + + def test_print_and_main_exit_codes(self) -> None: + """Human-readable reporting uses conventional exit codes.""" + + standard_output = io.StringIO() + with contextlib.redirect_stdout(standard_output): + self.assertEqual(contract.print_errors([]), 0) + self.assertIn("PASS", standard_output.getvalue()) + + standard_error = io.StringIO() + with contextlib.redirect_stderr(standard_error): + self.assertEqual(contract.print_errors(["problem"]), 1) + self.assertIn("problem", standard_error.getvalue()) + + self.assertEqual(contract.main([str(REPOSITORY_ROOT)]), 0) + with mock.patch.object(sys, "argv", ["checker", str(REPOSITORY_ROOT)]): + self.assertEqual(contract.main(None), 0) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/tests/quality/test_ci_coverage_diagnostics.py b/tests/quality/test_ci_coverage_diagnostics.py new file mode 100644 index 000000000..d24b08096 --- /dev/null +++ b/tests/quality/test_ci_coverage_diagnostics.py @@ -0,0 +1,33 @@ +"""Regression tests for exact Rust coverage diagnostics in CI.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +CI_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "ci.yml" + + +class CoverageDiagnosticsContractTests(unittest.TestCase): + """Keep failed 100% gates actionable without weakening them.""" + + def test_line_and_branch_failures_print_exact_missing_locations(self) -> None: + """Both LLVM coverage lanes retain same-run missing-location reports.""" + + workflow = CI_WORKFLOW.read_text(encoding="utf-8") + + self.assertIn("id: line-report", workflow) + self.assertIn("cargo llvm-cov report --text --show-missing-lines", workflow) + self.assertIn("steps.line-report.outcome == 'success'", workflow) + self.assertIn("id: branch-report", workflow) + self.assertIn( + "cargo +nightly-2026-08-01 llvm-cov report --branch --text --show-missing-lines", + workflow, + ) + self.assertIn("steps.branch-report.outcome == 'success'", workflow) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/tests/quality/test_hourly_maintenance_caller.py b/tests/quality/test_hourly_maintenance_caller.py new file mode 100644 index 000000000..d42c0a094 --- /dev/null +++ b/tests/quality/test_hourly_maintenance_caller.py @@ -0,0 +1,49 @@ +"""Contracts for the repository-local hourly maintenance caller.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +CALLER_WORKFLOW = REPOSITORY_ROOT / ".github" / "workflows" / "hourly-pr-maintenance.yml" +CENTRAL_SCHEDULER_REVISION = "f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae" + + +class HourlyMaintenanceCallerContractTests(unittest.TestCase): + """Keep the caller bounded, immutable, and credential-separated.""" + + def test_caller_runs_hourly_and_pins_the_verified_central_scheduler(self) -> None: + """The repository delegates policy instead of copying mutable scheduler code.""" + + workflow = CALLER_WORKFLOW.read_text(encoding="utf-8") + + self.assertIn('cron: "11 * * * *"', workflow) + self.assertIn( + "uses: ContextualWisdomLab/.github/.github/workflows/" + f"pr-review-merge-scheduler.yml@{CENTRAL_SCHEDULER_REVISION}", + workflow, + ) + self.assertNotIn("secrets: inherit", workflow) + self.assertNotIn("COPILOT_GITHUB_TOKEN", workflow) + self.assertNotIn("NVIDIA_NIM_API_KEY", workflow) + + def test_workflow_default_is_read_only_and_only_job_permissions_are_elevated(self) -> None: + """Review and merge authority stays scoped to the reusable-workflow job.""" + + workflow = CALLER_WORKFLOW.read_text(encoding="utf-8") + default_permissions = workflow.split("concurrency:", maxsplit=1)[0] + job_permissions = workflow.split("jobs:", maxsplit=1)[1] + + self.assertIn("permissions:\n contents: read", default_permissions) + self.assertIn("actions: write", job_permissions) + self.assertIn("checks: read", job_permissions) + self.assertIn("contents: write", job_permissions) + self.assertIn("id-token: write", job_permissions) + self.assertIn("pull-requests: write", job_permissions) + self.assertIn("cancel-in-progress: false", workflow) + + +if __name__ == "__main__": # pragma: no cover + unittest.main()