Command-line tool for accessing Y-Statement-style ADRs in code comments.
See also Helsing's announcement blog post.
ADRs, or Architecture Decision Records are justifications for software design choices that address a functional or non-functional requirement that is architecturally significant. These take many forms, with Markdown ADRs being particularly common. However, those are removed from the code that they document, which makes them harder to remember to look for and harder to keep up to date.
There exists a shorter version of an ADR, known as a Y-statement. Y-statements take the form
In the context of {use case/user story u}, facing {concern c} we decided for {option o} and neglected {other options}, to achieve {system qualities/desired consequences}, accepting {downside d/undesired consequences}, because {additional rationale}.
and aim to very concisely capture just the essential bits of a taken decision. It's beneficial to maintain a changelog at the bottom of the Y-statement to keep a historical reference of the decision change and track major changes in the decision.
These are short enough that they can be placed directly in comments in source code near where a given decision is actually effected. By placing the decision record in the code, we make it more likely that they are seen, read, and updated when someone is in the related block of code.
The downside to placing these records in source code is that they are harder to spot from the outside. Sure, you'll see them when browsing that part of the code, but what if you want to survey the various architectural decisions made in a project overall?
Enter this tool.
This tool expects you to write Y-Statement ADRs in comments in the following format:
/*
* YADR: YYYY-MM-DD some title
*
* In the context of {use case/user story u}, we faced {concern c}.
*
* We decided for {option o}, and neglected {other options}.
*
* We did this to achieve {system qualities/desired consequences},
* accepting {downside/undesired consequences}.
*
* We think this is the right trade-off because {additional rationale}.
*
* {Optional changelog}
* YYYY-MM-DD: <description of change>
*/(note that this is a modified Y-statement that's easier to digest!)
With the Y-Statements written this way, yadr can survey a whole source
tree for you, print any one of them in full, and check in CI that the
ones you have still follow the format.
For example, here is one taken from the dson crate:
/*
* YADR: 2024-06-18 Array elements without a position defined
*
* In the context of dealing with array elements which don't have a position defined,
* we faced a decision of how to expose these elements through the public OrArray API.
*
* We decided for assigning these elements an arbitrary but deterministic position, as
* a function of their uid, and neglected to attempt to map them to the start or end of
* the array, or provide a separate API for access to position-less elements, when their
* uid is not yet known.
*
* We did this to achieve minimal impact to the user-facing API, to avoid increasing the
* cognitive burden of using this crate, and to ensure that every node has a consistent
* view of the array when they share the same state, accepting that users may be
* surprised to find that a non-move operation (like a delete) can result in an element
* being assigned a different position.
*
* We think this is the right trade-off because this is a rare edge case, and placing
* the burden of handling it on users (by providing a separate access interface)
* would've been unreasonable. Additionally, non-deterministic views of the array would
* have violated a core assumption that nodes in sync with each other have the same view
* of the state.
*/$ cargo install yadrOr, from a checkout of this repository:
$ cargo install --path .yadr walks a directory tree, parses every file whose extension it
recognises, and reports on the Y-Statements it finds. It understands
Rust (.rs), Python (.py), and Nix (.nix), and it honours
.gitignore.
Every subcommand takes an optional path to search — the current
directory by default — and any number of --exclude globs.
$ yadr ls src/
==> src/crdts.rs
-> 2024-05-06 Removal of `.alive` tracking
==> src/causal_context.rs
-> 2024-04-19 Allocation of identifier bitsA statement that carries a changelog also reports when it was last revisited, which is often the first thing you want to know about a decision:
-> 2024-01-15 Store timestamps as UTC (last changed: 2024-02-01)Pass -L to print only the names of files that contain Y-Statements.
Give it any prefix of the title, which in practice means the date:
$ yadr show 2024-05-06The statement is printed with each paragraph reflowed onto a single line, and prefixed with the file and line it came from, so you can go and read the code it belongs to.
$ yadr checkPrints nothing and exits zero if every Y-Statement in the tree follows the format. Otherwise it exits non-zero and points at what is wrong:
Error: × in src/storage.rs
├─▶ in comment on line 7
├─▶ in Y-Statement starting on line 8
├─▶ in paragraph 2 of that statement
╰─▶ Y-Statement did not match expected format
╭─[5:1]
4 │
5 │ We chose one of the options available to us, and forgot the rest of the format.
· ──────────────────────────────────────┬─────────────────────────────────────────
· ╰── expected "We decided for <..>, and neglected <..>."
╰────
help: Note that punctuation is significant; you have to match every , and .The format is strict on purpose. A Y-Statement is only worth writing if it makes you answer all of the questions, and it is only worth reading if you can trust that it did.
The crate that backs the binary is published too, if you want to do
something with Y-Statements that the CLI doesn't do — render them to a
web page, say, or pull them out of a file format yadr can't read.
use yadr::{find_all, Language, YAdr};
let source = std::fs::read_to_string("src/lib.rs")?;
find_all(&source, Language::Rust, |line, yadr| {
println!("{}:{line} {}", "src/lib.rs", yadr.title);
Ok(true)
})?;Use YAdr::parse if you already have a single statement in hand rather
than a whole file. See the API documentation for the rest.
1. How should the date be formatted on the first line of a YADR, and what is the recommended method for documenting changes?
The date on a YADR should typically reflect the "as of" date—indicating when the decision documented in the YADR is applicable. This date should be updated whenever the YADR is significantly changed.
Here are the recommended methods for documenting changes:
-
Minor Edits: If you make small fixes to the text that don't alter the content or decision's validity, simply make the necessary corrections without updating the date or maintaining a changelog.
-
Content Clarifications or Additions: If you change or add reasoning, include missing information, or rewrite sections for clarity, add an entry to the changelog noting the modification.
-
Decision Changes: If the change involves updating the decision itself, consider rewriting the YADR, including the title and the date, to clearly reflect the new decision. In this case, you must keep a single line in the changelog indicating that a significant decision change has occurred, including the old title (and date) of the previous YADR.
These practices ensure that YADRs provide a clear historical record of the decision-making process while maintaining their relevance and accuracy over time.
yadr does what it says on the tin, and is stable in that. It finds and
verifies Y-Statements in .rs, .py, and .nix files.
Things we would like to exist, and would welcome help with:
- Support for more file formats (
.toml,.yaml,.json,.md,.ts, and so on). - Following the source links in comments, which
miettealready supports. yadr lsreporting every error it finds, rather than stopping at the first.
$ git clone https://github.com/helsing-ai/yadr && cd yadr
$ cargo testThe test suite is in three parts: unit tests in src/lib.rs for the
parser, snapshot tests in tests/instas.rs covering what each kind of
input parses to and what each kind of malformed input reports, and
end-to-end tests in tests/cli.rs that run the built binary over the
fixture trees in tests/fixtures/.
The snapshot tests use insta. If you change what
the parser produces, run cargo insta test --review and read every diff
before accepting it — a snapshot that quietly absorbs a bug is worse
than no snapshot at all.
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.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.