Conventions
Updated
These come from AGENTS.md, docs/CONTRIBUTING.md and README.md in the source
repository. Most have a gate behind them, so they are not house style: breaking one fails
a build.
Evidence, not memory
Section titled “Evidence, not memory”Conclusions come from reading the current code, tests, generated contracts and logs. A document that disagrees with the code is a defect, and the code is right.
Design for the product, not for the diff
Section titled “Design for the product, not for the diff”The project is pre-launch, so “small change”, “low risk” and “backward compatible” are not arguments on their own. Proposals are judged as if designing from scratch. Two things must be stated honestly when a change is proposed: what verification has to be re-run, and what real functionality is lost.
No abstraction without a second implementation
Section titled “No abstraction without a second implementation”Before adding a trait, a generic, an indirection layer or a configuration option, name the second implementation or call site that exists today. “Another backend might want this later” does not qualify (ADR-0064).
Deleting is cheaper than keeping
Section titled “Deleting is cheaper than keeping”Code with no callers, interfaces kept for the future, and half-connected paths all get removed; git recovers them if they are needed. The stated reason is operational rather than aesthetic: a path nobody drives is more dangerous than one that does not exist, because it looks like a working feature.
Do not let a development shortcut become architecture
Section titled “Do not let a development shortcut become architecture”Authentication bypasses, a pre-created bridge assumed to exist, a default base image, placeholder onboarding values. When one is found it is named out loud, not quietly built upon.
Comments carry invariants only
Section titled “Comments carry invariants only”Write down what is not obvious and what must always hold. Do not narrate milestones or
process: git log is the history source.
Never ship a control the product ignores
Section titled “Never ship a control the product ignores”A commit must leave the product honest. A field, toggle or endpoint that the API accepts and then does nothing with is worse than the missing feature, because the operator discovers it only after they have configured it and trusted it. Land the pipeline first, keep it unexposed, and open the surface in the commit where it takes effect.
Debug through the product first
Section titled “Debug through the product first”The logs, diagnosis findings and diagnostic report exist for exactly this moment. Reach
for journalctl, /sys and ps on the host only after the product cannot answer, and
then ask why it could not: either the missing observation becomes a finding or collector,
or the reason it stays a one-off is written down. “I can just look myself this time” is
how a diagnosis surface never grows.
If a customer could hit the same thing, it is documented: method goes in
docs/runbooks/, current implementation in docs/DESIGN*.md, behaviour change in
CHANGELOG.md.
Where a change lands
Section titled “Where a change lands”- Develop on
main. The one exception is concurrency: when another agent is working in the same checkout, use agit worktree, because two builds sharing a tree contaminate each other’s artifacts and status. - Commit per feature, not per file, in
type(scope): subjectform with a body. - Every commit compiles and passes preflight.
- Push after each commit. Local commits that live only on one machine do not exist to anyone else, and the pre-push hook re-runs the fast hygiene checks as you go.
- A new dependency needs its justification in the commit message: why, what it replaces, how mature it is.
- If an implementation conflicts with
docs/DESIGN.md, change the design document first.
Migrations only roll forward
Section titled “Migrations only roll forward”The schema moves by expand-contract. A destructive change (drop a column, rename it,
change its type) never ships in the same release as the code that stops using the old
shape: it is split into expand, backfill and contract, with the contract step a release
or two later, so an emergency rollback lands on a binary that still tolerates the schema
it finds. New state prefers evolving the payload JSON over a new typed column
(ADR-0003).
What the gates enforce
Section titled “What the gates enforce”| Rule | Gate |
|---|---|
| No empty or stray tracked files | tools/check-stray-sources.sh |
No source file past 1500 lines (*.rs, *.ts, *.tsx, *.css) |
tools/check-file-sizes.sh; the ratchet table it keeps is a ceiling that may only shrink |
Chinese never reaches an operator: UI copy, client-visible messages, log lines, host-installed files, and SQL that .schema prints back |
tools/check-no-chinese.sh, scanning Rust string literals, the shipped systemd units and the CREATE statements in the migrations; source comments are exempt |
The frontend is gated like the Rust side, and UI regression tests go in the mocked suite under web/e2e/*-mock/ |
pnpm lint, pnpm format:check and pnpm build in CI and preflight; pnpm test:e2e:mocked is the local release gate |
| Retired names and dead decision links are gone | tools/check-current-contract.sh |
| Dependency advisories, sources and licences | tools/check-supply-chain.sh with cargo-deny |
| No secret ever entered history | tools/check-secret-history.sh, and a pinned full-history scan |
| The image bake keeps its shape | tools/test-host-image-build-contract.sh |
Naming: the product in prose, the binary on disk
Section titled “Naming: the product in prose, the binary on disk”The product is Virtainer wherever an operator reads text: the wordmark, page titles,
toasts and log messages. The installed identity is plain virtainer: the crate, the
binary /usr/bin/virtainer, the unit virtainer.service, and the data paths around them.
The line between the two is literal correctness. A command an operator must type, and a path that appears in a log field, are facts. Restyling them to match the brand would hand someone a command that fails or a path that does not exist.