Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

env:
BIN_NAME: {% endraw %}{{ package_name }}{% raw %}
CROSS_REVISION: v0.2.5
CROSS_REVISION: 88f49ff79e777bef6d3564531636ee4d3cc2f8d2

jobs:
build:
Expand Down Expand Up @@ -63,7 +63,7 @@ jobs:
if [ -x "$HOME/.cargo/bin/cross" ]; then
exit 0
fi
cargo install cross --git https://github.com/cross-rs/cross --tag "${CROSS_REVISION}"
cargo install cross --git https://github.com/cross-rs/cross --rev "${CROSS_REVISION}"
- name: Cache cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
with:
Expand All @@ -76,6 +76,10 @@ jobs:
- name: Build release binary
# Use +stable to override rust-toolchain.toml (which specifies nightly
# with Cranelift for development) and ensure release builds use stable.
env:
# Build release artifacts without repository-local linker flags,
# including mold rustflags from .cargo/config.toml.
RUSTFLAGS: ""
run: cross +stable build --release --target ${{ matrix.target }}
- name: Prepare artifact
run: |
Expand Down
60 changes: 58 additions & 2 deletions template/AGENTS.md → template/AGENTS.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,21 @@ project:
meaningfully named structs.
- Where a function is returning a large error, consider using `Arc` to reduce
the amount of data returned.
- Write unit and behavioural tests for new functionality. Run both before and
after making any change.
- Ensure that new features are validated with unit tests using `rstest` and
behavioural tests using `rstest-bdd` where applicable. Cover happy paths,
unhappy paths, and relevant edge cases.
- Add end-to-end tests where a change affects externally observable workflows,
integration contracts, persistence, command-line behaviour, network
boundaries, UI flows, or other system-level behaviour.
- Use property tests with `proptest` or a bounded model checker with `kani`
when a change introduces an invariant over a range of inputs, states,
orderings, or transitions. Use judgement to choose the appropriate level of
rigour.
- Use an exhaustive proof with `verus` for introduced lemmas or contractual
business logic. Proofs must be substantive, rigorous, and well-founded, not
merely a restatement of the assumed property.
- Run relevant unit, behavioural, property, model-checking, proof, and
end-to-end suites before and after making any change.
- Every module **must** begin with a module level (`//!`) comment explaining the
module's purpose and utility.
- Document public APIs using Rustdoc comments (`///`) so documentation can be
Expand Down Expand Up @@ -249,6 +262,30 @@ project:
- Consume fallible fixtures in `rstest` by **making the test return `Result`**
and applying `?` to the fixture.

### Observability

- Use `tracing` for logging and diagnostics. Prefer structured
`tracing::{trace, debug, info, warn, error}` events and spans over
`println!`, `eprintln!`, or direct `log` macros. Add fields for identifiers,
state, and error context so downstream subscribers can filter and correlate
events without parsing message text.
- Use `#[tracing::instrument]` or explicit spans around request handling,
command execution, retries, background jobs, and other meaningful units of
work. Do not hold `Span::enter()` guards across `.await`; use
`Instrument::instrument` or scoped synchronous spans instead.
- Use the `metrics` crate for metric emission where usage, uptake, failure,
or mitigation metrics are required. Prefer `counter!` for cumulative events,
`gauge!` for values that rise and fall, and `histogram!` for distributions
such as latency or payload size.
- Describe emitted metrics with `describe_counter!`, `describe_gauge!`, or
`describe_histogram!` where the unit or purpose is not obvious from the
metric name. Keep metric names stable and labels low-cardinality; do not put
user input, request IDs, paths with unbounded parameters, or raw error
strings into labels.
- Libraries may emit `metrics` and `tracing` instrumentation, but must not
install global recorders or subscribers. Applications should initialise
exporters/subscribers once, as early as practical in startup.

## Markdown Guidance

- Validate Markdown files using `make markdownlint`.
Expand All @@ -262,6 +299,25 @@ project:
- Use GitHub-flavoured Markdown footnotes (`[^1]`) for references and
footnotes.

## Project Documentation

Record design decisions in the design document. Where a decision is
substantive, record it in an ADR document following the documentation style
guide, then reference that ADR from the design document.

{% if flavour == 'app' -%}
Update `docs/users-guide.md` for any change to application behaviour or user
interface that a user should know about. Document internally facing interfaces
or practices in the relevant component architecture document. Document
internally facing conventions or practices in `docs/developers-guide.md`.
{% else -%}
Update `docs/users-guide.md` for any change to behaviour or public API that a
consumer of the library should know about. Document internally facing
interfaces or practices in the relevant component architecture document.
Document internally facing conventions or practices in
`docs/developers-guide.md`.
{% endif %}

## Additional tooling

The following tooling is available in this environment:
Expand Down
6 changes: 4 additions & 2 deletions template/Cargo.toml.jinja
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{% set package_keyword_values = package_keywords.split(',') | map('trim') | reject('equalto', '') | list -%}
{% set package_category_values = package_categories.split(',') | map('trim') | reject('equalto', '') | list -%}
[package]
name = "{{ package_name }}"
version = "0.1.0"
Expand All @@ -7,8 +9,8 @@ license = "ISC"
repository = "{{ repository_url }}"
homepage = "{{ homepage_url }}"
readme = "README.md"
keywords = [{% for keyword in package_keywords.split(',') %}"{{ keyword.strip() }}"{% if not loop.last %}, {% endif %}{% endfor %}]
categories = [{% for category in package_categories.split(',') %}"{{ category.strip() }}"{% if not loop.last %}, {% endif %}{% endfor %}]
keywords = [{% for keyword in package_keyword_values %}"{{ keyword }}"{% if not loop.last %}, {% endif %}{% endfor %}]
categories = [{% for category in package_category_values %}"{{ category }}"{% if not loop.last %}, {% endif %}{% endfor %}]

[dependencies]

Expand Down
20 changes: 4 additions & 16 deletions template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,8 @@

This is a generated project using [Copier](https://copier.readthedocs.io/).

## Build Linkers
## Documentation

Development builds use `mold` on Linux through `.cargo/config.toml` so local
debug builds link quickly. Coverage generation uses `lld` instead because the
LLVM coverage tools expect LLVM-compatible linker behaviour.

Install both linkers before running the full workflow locally:

```sh
sudo apt-get install clang lld mold
```

Run coverage with:

```sh
make coverage
```
- [Documentation contents](docs/contents.md)
- [User guide](docs/users-guide.md)
- [Developer guide](docs/developers-guide.md)
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ Core Principles of Calculation:
Cognitive Complexity is incremented based on three main rules[^8]:

1. **Breaks in Linear Flow:** Each time the code breaks the normal linear
reading flow (e.g., loops, conditionals like
`if`/`else`/`switch`, `try-catch` blocks, jumps to labels, and sequences of
logical operators like `&&` and `||`), a penalty is applied.
reading flow (e.g., loops, conditionals like `if`/`else`/`switch`,
`try-catch` blocks, jumps to labels, and sequences of logical operators like
`&&` and `||`), a penalty is applied.

2. **Nesting:** Each level of nesting of these flow-breaking structures adds an
additional penalty. This is because deeper nesting makes it harder to keep
Expand Down
35 changes: 35 additions & 0 deletions template/docs/contents.md.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Documentation contents

[Documentation contents](contents.md) is the index for {{ project_name }}'s
documentation set.

## Project guides

- [User guide](users-guide.md) explains how to use the generated project and
its public build and test commands.
- [Developer guide](developers-guide.md) explains the local workflow and
implementation tooling for contributors.
- [Repository layout](repository-layout.md) explains the generated project's
top-level files, directories, and ownership boundaries.
- [Documentation style guide](documentation-style-guide.md) defines the
spelling, structure, Markdown, Architecture Decision Record (ADR), Request
for Comments (RFC), and roadmap conventions used by this documentation set.

## Rust reference material

- [Reliable testing in Rust via dependency injection](reliable-testing-in-rust-via-dependency-injection.md)
explains how to keep tests deterministic by injecting environment, clock,
filesystem, and other external dependencies.
- [Rust doctest Don't Repeat Yourself guide](rust-doctest-dry-guide.md)
explains how to write maintainable, executable Rust documentation examples.
- [Rust testing with `rstest` fixtures](rust-testing-with-rstest-fixtures.md)
explains fixture-based, parameterized, and asynchronous testing with `rstest`.

## Engineering practice

- [Complexity antipatterns and refactoring strategies](complexity-antipatterns-and-refactoring-strategies.md)
explains cognitive complexity, the bumpy-road antipattern, and refactoring
approaches for maintainable code.
- [Scripting standards](scripting-standards.md) explains the preferred Python
scripting stack, command execution patterns, and test expectations for helper
scripts.
21 changes: 21 additions & 0 deletions template/docs/developers-guide.md.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Developer Guide

This guide explains the contributor workflow for the generated
{{ project_name }} project.

## Local Workflow

Use `make all` as the public entrypoint for formatting, linting, and tests.
`make lint` runs rustdoc, Clippy, and Whitaker. `make test` prefers
`cargo nextest run` and falls back to `cargo test` when cargo-nextest is not
available. `make coverage` uses `cargo llvm-cov` with `lld`.

## Tooling

Development builds use Cranelift for debug code generation. On Linux targets,
`.cargo/config.toml` configures clang to link with `mold` so debug builds link
quickly. Coverage generation uses `lld` because LLVM coverage tooling expects
LLVM-compatible linker behaviour.

Install `clang`, `lld`, and `mold` before running the full generated workflow
locally on Linux.
35 changes: 21 additions & 14 deletions template/docs/documentation-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ material, the user's guide explains how to use the project, the developer's
guide explains how to work on the project, the design document explains why the
system is shaped the way it is, and the repository layout document explains
where important things live. For discoverability, use canonical filenames
unless a stronger repository-specific constraint applies: `docs/contents.md`,
`docs/users-guide.md`, `docs/developers-guide.md`, `docs/repository-layout.md`,
and a primary design document with an explicit product or topic name such as
unless a stronger repository-specific constraint applies. A minimal canonical
set looks like
`docs/{contents,users-guide,developers-guide,repository-layout}.md` plus a
primary design document under `docs/*-design.md`, for example
`docs/theoremc-design.md` or `docs/query-planner-design.md`.

### Contents file
Expand Down Expand Up @@ -135,8 +136,9 @@ the existing system, not as the place for the project's primary design document.

- Open with one short paragraph that states the audience and the operational
scope of the guide.
- Link early to the design document, accepted decision records, and other
normative references that explain architecture or rationale in depth.
- Link early to the design document, repository layout document, accepted
decision records, and other normative references that explain architecture or
rationale in depth.
- Put maintainer-facing implementation guidance here, for example build, test,
lint, release, debugging, extension, and contribution workflows.
- Use numbered sections for long-form technical documents to improve
Expand All @@ -148,6 +150,9 @@ the existing system, not as the place for the project's primary design document.
- Keep subsystem descriptions focused on current responsibilities,
integration points, and operational expectations. Put design rationale, major
trade-offs, and proposed architecture in design documents instead.
- Do not embed repository-layout guidance here. The canonical location for
file-tree and path-responsibility documentation is
`docs/repository-layout.md`.
- Keep the document synchronized with decision records, roadmap items, and the
codebase. A stale developer's guide is worse than a shorter one.

Expand Down Expand Up @@ -175,10 +180,10 @@ decision, and an RFC to propose a change.
### Design document

Use a dedicated design document, conventionally named
`docs/<product-or-topic>-design.md`, to explain the architecture,
constraints, rationale, and intended evolution of a system or subsystem. This
document is the correct location for design intent; that material must not be
buried in the user's guide or developer's guide.
`docs/<product-or-topic>-design.md`, to explain the architecture, constraints,
rationale, and intended evolution of a system or subsystem. This document is
the correct location for design intent; that material must not be buried in the
user's guide or developer's guide.

- Start with a concise front matter section that states status, scope, primary
audience, and the decision records or other documents that take precedence.
Expand Down Expand Up @@ -408,7 +413,7 @@ Include these sections as appropriate to the decision's complexity:

### ADR template

```markdown
```plaintext
# Architectural decision record (ADR) NNN: <title>

## Status
Expand Down Expand Up @@ -488,9 +493,9 @@ implementation is required.>
### Repository layout document

Use a repository layout document, canonically `docs/repository-layout.md`, to
explain the shape of the tree and the responsibilities of its major paths. This
may be a standalone document or a clearly labelled section within the
developer's guide, provided readers can find it easily from the contents file.
explain the shape of the tree and the responsibilities of its major paths. Use
this standalone document as the canonical location for repository-layout
guidance rather than embedding that material in the developer's guide.

- Document the top-level directories and any critical subdirectories that a new
contributor must understand quickly.
Expand All @@ -504,6 +509,8 @@ developer's guide, provided readers can find it easily from the contents file.
long-lived reference documents belong.
- Call out any directories with unusual constraints, such as generated output,
fixtures, snapshots, or capability-restricted paths.
- Ensure the contents file links directly to `docs/repository-layout.md` so
readers can find it without scanning the developer's guide.
- Update the layout document when the repository structure changes enough that
a contributor could otherwise follow outdated guidance.

Expand Down Expand Up @@ -634,7 +641,7 @@ This hierarchy should align with the GIST framework:

### Roadmap example

```markdown
```plaintext
## 1. Core infrastructure

### 1.1. Logging subsystem
Expand Down
27 changes: 14 additions & 13 deletions template/docs/reliable-testing-in-rust-via-dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Writing robust, reliable, and parallelizable tests requires an intentional
approach to handling external dependencies such as environment variables, the
filesystem, or the system clock. Functions that directly call `std::env::var`
or `SystemTime::now()` are difficult to test because they depend on global,
filesystem, or the system clock. Functions that directly call `std::env::var` or
`SystemTime::now()` are difficult to test because they depend on global,
non-deterministic state.

This leads to several problems:
Expand All @@ -18,9 +18,10 @@ This leads to several problems:

The solution is a classic software design pattern: **Dependency Injection
(DI)**. Instead of a function reaching out to the global state, its
dependencies are provided as arguments. The `mockable` crate offers a
convenient set of traits (`Env`, `Clock`, etc.) to implement this pattern for
common system interactions in Rust.
dependencies are provided as arguments. The
[mockable](https://docs.rs/mockable/latest/mockable/) crate offers a convenient
set of traits (`Env`, `Clock`, etc.) to implement this pattern for common
system interactions in Rust.

______________________________________________________________________

Expand All @@ -32,13 +33,12 @@ First, add the crate to development dependencies in `Cargo.toml`.

```toml
[dev-dependencies]
mockable = { version = "0.1.4", default-features = false, features = ["clock", "mock"] }
mockable = "0.3"
```

### 2. The untestable code (before)

Directly calling `std::env` makes it difficult to test all logic paths
exhaustively.
Directly calling `std::env` makes it hard to test all logic paths.

```rust,no_run
pub fn get_api_key() -> Option<String> {
Expand Down Expand Up @@ -127,8 +127,8 @@ ______________________________________________________________________
## 🔩 Handling other non-deterministic dependencies

This dependency injection pattern also applies to other non-deterministic
dependencies, such as the system clock. The `mockable` crate provides a `Clock`
trait for this purpose.
dependencies, such as the system clock. `mockable` provides a `Clock` trait for
this purpose.

### Untestable code

Expand Down Expand Up @@ -193,7 +193,7 @@ ______________________________________________________________________
## 📌 Key takeaways

- **The Problem is Non-Determinism:** Directly accessing global state like
`std::env` or `SystemTime::now` makes code difficult to test exhaustively.
`std::env` or `SystemTime::now` makes code hard to test.
- **The Solution is Dependency Injection:** Pass dependencies into functions as
arguments.
- **Use** `mockable` **Traits:** Abstract dependencies behind traits such as
Expand All @@ -204,5 +204,6 @@ ______________________________________________________________________
to interact with the actual system.
- **`RealEnv` is NOT a Scope Guard:** `RealEnv` directly mutates the global
process environment without automatic cleanup. For integration tests that
require modifying the live environment, consider a crate such as `temp_env`.
For unit tests, `MockEnv` is preferable.
require modifying the live environment, consider a crate such as
[temp_env](https://crates.io/crates/temp-env). For unit tests, `MockEnv` is
preferable.
Loading
Loading