Rubric is cohesion tooling for software development in Rust.
Status: Rubric is currently in pre-release. Expect significant breaking changes between minor versions. More feedback from real projects using Rubric is needed for progress to a v1.0.0 stable release.
Install is handled through Cargo and crates.io
cargo install cargo-rubric
Rubric allows you to define requirements for your project, track which source code meets a requirement, and register verification evidence against the two:
# examples/gizmo/rubric.toml
[req.gizmo.spins] # pathed requirement label
kind = "functional" # a function implements this
statement = "The gizmo spins for 100ms" # what this is supposed to do / not do
reconcile = true # require vouching for changes// examples/gizmo/src/main.rs
use rubric_trace_macros::satisfies; // Optional annotation macros
// Write some code that satisfies the requirement. Only the label has to match:
pub struct Gizmo {}
impl Gizmo {
#[satisfies(req.gizmo.spins)] // Annotate code to match it to a requirement
pub fn spin(&self, times: usize) -> usize { // Can also use bare comment or cfg_attr forms:
for _ in 0..times { // `// satisfies: req.gizmo.spins` or
println!("spinnin...") // `#[cfg_attr(any, satisfies(req.gizmo.spins))]`
}
times
}
}
// Verify the satisfier. It can be an ordinary test, external evidence, or something
// else. Verifiers don't have to be in the same file, so you can bring in your
// favorite static analysis tools (like clippy).
pub mod tests {
use crate::Gizmo;
use rubric_trace_macros::verifies;
#[test]
#[verifies(req.gizmo.spins)] // Match the verifier edge to the req/sat
fn does_it_spin() { // Rubric will now seal the three
let gizmo = Gizmo{};
let times = gizmo.spin(10);
assert!(times == 10,
"expected to return 10"
);
}
}Rubric collects the three into a triplet and uses content-hashing to record the
state of a triplet in its lockfile (rubric.lock). Rubric calls these hashes
seals because they enclose content against an address, kind of like an actual
wax seal. If the content included in the satisfier or verifier
ever changes, the hash will change. The triplet of requirement, satisfier,
verifier, can be referred to as a requirement triplet, where its content-hash
is called a seal.
Rubric ships a Cargo tool called cargo-rubric. cargo rubric init bootstraps
an empty rubric rubric.toml in the current working directory. You can then
write whatever requirement, satisfiers, and verifiers you want. Rubric takes
care of the tracking.
Once you have at least one requirement - and a matching satisfier and verifier
-cargo-rubric should report as much:
user@host:~/gizmo> cargo rubric check
gizmo.spins pending statement not yet accepted
gizmo.spins pending crate::Gizmo::spin not yet accepted
gizmo.spins pending crate::tests::does_it_spin not yet accepted
gizmo.spins attest not attested; vouch for changes with `cargo rubric attest`
You can then accept your changes, and optionally vouch for them by running
cargo rubric attest. Attestation performs additional tracking apart from
cargo rubric accept, presenting an additional audit surface for items.
Accepting will generate a lockfile, rubric.lock:
user@host~/gizmo> cargo rubric accept
+ gizmo.spins (statement)
+ gizmo.spins crate::Gizmo::spin
+ gizmo.spins crate::tests::does_it_spin
rubric: 3 added, 0 re-sealed, 0 removed
This is the simplest case, but Rubric also supports:
- Tracking the history of the lock over time
- Generating requirements documentation
- Adding requirement triplets to existing docs.
- Alternative sealing behavior (signature, signature + body, full file, etc.)
When the requirement triplet is sealed, Rubric's default behavior will seal the
body of whatever satisfier or verifier entities are annotated. However,
Rubric supports other sealing behaviors:
stmt: The requirement statement alonesig: An item's signature up to its body blockfull: An item's signature and its bodyfile: Raw bytes of some file (e.g., external evidence)attest: A requirement's attestation root, hashed over its current leg/edge seals
The variety here makes Rubric deceptively flexible. In particular, file based sealing enables Rubric to support citing outputs from external tooling without requiring an explicit parsing step. Rubric's concern is recording the data and providing visibility for changes scoped to the requirement. External evidence support means Rubric can stay light and focused, letting other tools perform analysis where Rubric tracks that analysis against the code and stated requirements.
Returning to the gizmo example, you can examine the lockfile after accept
and observe the seal content:
user@host:~/gizmo> cat rubric.lock
# rubric.lock — managed by `cargo rubric accept`. Do not edit by hand.
# Format: <requirement_label>\t<item_path>\t<origin>\t<seal>
# <origin> is `annotation` or `declared`.
# <seal> is `<scheme>:<hex>` (e.g. `body:a3f2b1c8`, `stmt:...`) or `off`.
gizmo.spins <statement> declared stmt:8a90d01158c425db
gizmo.spins crate::Gizmo::spin annotation body:697348f1bad4658e
gizmo.spins crate::tests::does_it_spin annotation body:c3a1471434b59809
Here we can see the locked form of each item in the requirement triplet. Earlier
on, the attest subcommand and sealing mode were mentioned
If you remember from the Gizmo example's rubric.toml we set reconcile = true.
Running cargo rubric check, you might notice that the attestation root for
the gizmo.spins requirement isn't present in the lockfile:
user@host:~/gizmo> cargo rubric check
gizmo.spins attest not attested; vouch for changes with `cargo rubric attest`
1 delta
This displays because accept will generate any needed seals for an item but
not an attestation root.
The attestation root itself is a hash of the requirement triplet's constituent
leg seals. This hash-of-hashes is important because it is sensitive to change
in the membership set of any legs of the requirement triplet - or more plainly,
it enables Rubric to track sets of satisfiers or verifiers tied to one
requirement. Since Rubric automatically supports multiple satisfier
annotations per requirement, changes to the set of satisfiers will change
its respective content-hash in rubric.lock.
If you then run cargo rubric attest on Gizmo, a new line will appear over the
existing triplet:
gizmo.spins <attest> declared attest:1dd4df25f7aa79a4
One way to use attestation is as a continuous integration step. To support
this, cargo-rubric includes the log and audit subcommands. log will
utilize a repository's existing git history to specifically highlight changes
to the lockfile and requirements. audit specifically checks commits that carry an
attestation root which doesn't match its requirements triplet components. An
example where this may become useful would be a crate with a sensitive API
surface. Once requirements, satisfiers, and verifiers are written as desired,
log and audit become tools for monitoring changes to any of the components.
Someone running cargo rubric attest will update an attestation root in the
lockfile. This would be a CI signal to key certain review steps on and cargo rubric audit locates this plainly.
Rubric supports sealing of external evidence to enable integration with other
analysis tools, like (but not limited to) rustdoc or clippy. Suppose you are
working on a project that requires tight control over its public API surface.
You can point Rubric at that external evidence:
# widget/rubric.toml
[req.api.surface]
kind = "invariant"
statement = "The crate's public API surface is exactly the reviewed set"
verified_by = ["external:docs/public-api.txt"]
reconcile = trueIn this example, you might use rustdoc to audit the public API surface and
generate a report:
cargo +nightly rustdoc --lib -- -Z unstable-options --output-format json
jq -r '.paths | to_entries[]
| select(.value.crate_id == 0)
| "\(.value.kind) \(.value.path | join("::"))"' \
target/doc/widget.json | sort > docs/public-api.txtuser@host:~/widget> ./gen-surface.sh
user@host:~/widget> cat docs/public-api.txt
function widget::connect
function widget::disconnect
module widget
You might be concerned about the report becoming stale, however. But don't worry, because you have options. For example, you could add a second verifier to the existing requirement:
verified_by = ["external:docs/public-api.txt", "crate::tests::public_api_is_current"]
Then, the other verifier could generate the evidence each time the test is run.
#[cfg(test)]
mod tests {
use std::process::Command;
#[test]
fn public_api_is_current() {
let status = Command::new("bash").arg("gen-surface.sh").status().expect("gen-surface");
assert!(status.success(), "surface generation failed");
let current = std::fs::read_to_string("docs/public-api.txt").unwrap();
let reviewed = include_str!("../docs/public-api.txt.reviewed");
assert_eq!(current, reviewed, "public API surface drifted; review and re-accept");
}
}If you want a stronger guarantee that the analysis report stays up-to-date, you
could also put it in build.rs:
verified_by = ["external:docs/public-api.txt", "external:build.rs"]
Each approach has pros and cons - it's up to you to decide what's best for your project.
Rubric supports requirement metadata in rubric.toml through the meta blocks. With meta
blocks, you can define your own optional keys and values to support items like expanded prose
descriptions, rationale notes, change orders, issue IDs, or other data required by your own
project's requirements tracing policy. For the gizmo example:
# examples/gizmo/rubric.toml
<SNIP> # existing requirements
[req.gizmo.spins.meta]
rationale = "The downstream sensor must have 100ms of spin to get a stable indication."
change_order = "CO-1234"
description = '''
spin(times) turns the rotor for the given number of cycles and returns
that count. Thus a caller can make sure that all the given cycles occurred.'''If you accept, attest, and review the lock, you can then see what was tracked:
gizmo.spins <attest> declared attest:644ecd6570b765f5
gizmo.spins <meta:change_order> declared meta:de0c95056e2fe4fa
gizmo.spins <meta:description> declared meta:6645a123421b5ba9
gizmo.spins <meta:rationale> declared meta:787df6885c40d8f4
gizmo.spins <statement> declared stmt:8a90d01158c425db
gizmo.spins crate::Gizmo::spin annotation body:697348f1bad4658e
gizmo.spins crate::tests::does_it_spin annotation body:c3a1471434b59809
The project is early-stage and all contributions are welcome.
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.