Writing design docs
The design/ directory records why the Noir language, compiler, and tooling work the
way they do. It is a decision record aimed at a contributor, not user-facing documentation
(that lives in docs/docs).
Beyond the explicit decisions, describe how the feature is expected to behave in enough detail that an external party — someone who has not read the code — could compare the doc against the implementation and find where they disagree. The doc is the specification the implementation is checked against; prose so vague that nothing could contradict it does not serve that purpose. Cover the normal behavior, the edge cases, and how the feature interacts with the parts of the system around it.
Where a behavior can be enforced by a test, prefer a short summary plus a link to that test over an exhaustive prose description. A test is an executable specification that cannot silently drift from the code, whereas prose can — so state the behavior in a sentence and link the test that demonstrates it, rather than enumerating every case at length. Link the test file and name the test (not a line number, per the linking rule below). Reserve prose for what a test cannot capture: the rationale, the invariants, and how the parts fit together.
When the intended behavior is meant to match an existing system — for example Rust's
#[allow] / #[deny] lint levels — say so explicitly and link to that system's
documentation. That tells a reader where the reference behavior is defined so they can
check the match, and flags what should change here if that system changes.
Follow these rules when writing or editing a design/*.md file.
Describe the present, not the history
A design doc states the design as it stands today and the rationale that holds today. Do not narrate how the code got here.
No historical narrative. Drop "historically…", "this used to be…", "previously we did…", "originally…", "before this change…". Version control already records how things changed; the design doc records the decision that is in force now. Rewrite the rationale in the present tense as a property the design guarantees. Keep that property at the design's altitude — the behavior or invariant it ensures — not the mechanism that implements it (that belongs next to the code). E.g.
- ❌ "Historically any lint name was accepted, so a typo silently did nothing; that was later fixed."
- ✅ "Only recognized lint names are accepted, so a misspelled name is reported to the author rather than silently ignored."
Only keep history that is a live constraint. The single exception is when a past decision still binds the present — a backwards-compatibility guarantee, a migration that must remain supported, an on-disk or wire format that cannot change. There the "history" is actually a present-day constraint, so state it as one ("The vN artifact format must stay readable, so …") rather than as a story about the past.
No PR or task progress. A design doc is not a changelog. Drop "this is the first slice", "added in PR #123", "TODO in a follow-up", "the next step will…". Describe the intended end-state and the rule for getting there, and link the tracking issue for the roadmap. Progress belongs in the PR description and the issue, not in
design/.
The cherry-pick test (mirrors the code-comment rule in CLAUDE.md): imagine the file
dropped into a fresh repo with no git history and no memory of the change that introduced
it. Every sentence must still make sense and be verifiable against the current code. If a
sentence only lands for someone who remembers the previous state or the PR that changed it,
cut or rewrite it.
Keep it accurate and in sync
- Point at the code that implements the decision — with a relative link (see below) and the relevant type or function named — so the doc can be checked against reality.
- When a change alters or invalidates a recorded decision, update the corresponding
design/file in the same PR. A stale design doc is worse than none. - Record a new decision here when a change introduces a non-obvious, cross-cutting design choice that isn't captured anywhere else.
Sketch the intended future state when the feature is partial
When a design doc describes a feature that isn't fully built yet, close the file with a short section (e.g. "Intended direction") describing the desired end state, so a reader knows the current design is a stepping stone rather than the whole picture. This is not the PR/task progress banned above: it describes where the design is headed, not which PR added what. Keep it lean:
- Summarise the end state in a few sentences and link to a tracking issue for the full plan whenever one exists.
- If the section starts to grow, that is the signal that its content belongs in a tracking issue: move it there and leave only a short pointer here. A design doc records the decision in force plus a lean sketch of the direction — it is not a roadmap.
Reference the code with a relative link
When a design doc discusses code, link to the file it describes so a reader can open it and compare the prose against the implementation. Prefer a link over a bare path in prose:
- Use a relative link from the doc's location, e.g.
[the lint registry](../compiler/noirc_frontend/src/lint.rs), not an absolute or GitHub URL. Relative links keep working in a clone or fork, and they are what the link checker validates. - Never link to a line number. Line numbers drift as the file changes, so a
line-anchored link silently starts pointing at the wrong place. Link to the file and name
the type or function in the prose instead; use a
#anchoronly if the file itself defines one. just check-design-links(script:scripts/check_design_links.sh) verifies that every relative link indesign/resolves to a file that exists, so a moved or renamed target fails loudly in CI instead of rotting silently. It ignores external URLs and pure#anchorlinks. Run it after editing links.