Skip to content

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.

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.

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).

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.

Write down what is not obvious and what must always hold. Do not narrate milestones or process: git log is the history source.

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.

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.

  • Develop on main. The one exception is concurrency: when another agent is working in the same checkout, use a git worktree, because two builds sharing a tree contaminate each other’s artifacts and status.
  • Commit per feature, not per file, in type(scope): subject form 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.

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).

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.