Developer Guide — Standards and Guidelines¶
This document defines how we write code, review contributions, and use AI tooling in projectMM. Architecture and design rationale live in architecture.md; build, deploy, and toolchain instructions are in deploy.md.
Coding Standards¶
C/C++ (backend)¶
- Target: C++17 where available on all platforms; avoid features that break ESP-IDF's toolchain.
- Naming:
CamelCasefor classes,camelCasefor methods and variables,UPPER_SNAKEfor constants and macros. - Headers: one public class per header where reasonable. Keep headers free of heavy includes — forward declare when possible.
- Memory: no raw
new/deletein Module code. Acquire insetup(), release inteardown(). A Module that leaks on teardown is a bug. - Hot path discipline: no allocations, no blocking calls, no logging at info level or above in
loop()on the hot path. Measure, don't guess. - Platform-specific code: goes behind the platform abstraction layer. Modules do not
#ifdef ESP32— if they need to, the abstraction is missing something. - Comments: document classes, non-trivial functions, and any variable whose purpose is not obvious from its name. Comments explain why, not what.
JavaScript (frontend)¶
- No framework for now (see architecture.md — Frontend Technology). Plain HTML/CSS/JS.
- No build step unless a Module genuinely requires one.
- Module boundaries: one file per logical unit. Global state is explicit, not implicit.
- Naming: matches the backend convention where they meet (e.g. state keys, JSON schema fields).
JSON schemas¶
- Schema-first. Every Module's JSON UI description is part of its definition of done, not an afterthought.
- Stable keys. Renaming a control is a breaking change and requires a migration note.
Human and AI Readability¶
Human readability comes first. Classes, variables, and functions are documented inline. A reader should be able to understand a file without jumping across the repository.
AI readability follows from that. We do not write separate AI-targeted comments or mirror documentation. If an AI agent cannot contribute, a human probably cannot either — the fix is better human-readable code, not AI-specific scaffolding.
Reading the code is not the documentation. If a contributor needs to read source to understand what a Module does, the Module is missing its doc page. If they need to read source to understand why the architecture is shaped this way, a design document is missing.
Anti-Debt Checklist¶
Every PR is reviewed against these rules. They are also the design principles in architecture.md — Anti-Debt:
- [ ] No implicit conventions — anything a contributor needs to know is in a document or in a comment on the thing itself.
- [ ] No dead code paths — features that "might be useful later" are not added until needed.
- [ ] No new dependency without a measured footprint and an exit strategy.
- [ ] No monolithic internals — each component is replaceable in isolation.
- [ ] Hot path is clean — no allocations, no blocking, no surprise work.
- [ ] Teardown is complete — running
setup→loop→teardownleaves no residue. - [ ] Tests cover the Module's actual behavior, not just compilation. See Testing.
Testing¶
The goal is non-trivial tests. A test that only verifies "the code compiles and returns 0" has negative value — it creates the illusion of coverage without catching regressions. Every Module should have tests that exercise its real behavior: state transitions, output correctness, boundary conditions.
Platform abstraction and testability. The platform abstraction layer is also a testing seam. Code that calls the abstraction can be tested on PC with a test implementation of the abstraction, independent of ESP32/rPi hardware.
Test levels:
- Unit tests — pure logic, run on PC natively. No hardware, no network, no filesystem unless the test provides them.
- Integration tests — multiple Modules, the scheduler, the hot path, the HTTP/WebSocket server. Run on PC as a subprocess.
- Platform tests — build and smoke-test on each target platform.
- Hardware-in-the-loop — deploy to a real ESP32/rPi, capture serial output, assert health metrics.
Framework: doctest — single-header C++, runs on PC, rPi, and ESP32.
Test complexity¶
Every test case carries a complexity badge that signals how much real behavior it verifies. The badge appears in test-results.md and deploy-summary.md.
| Badge | Meaning | Example |
|---|---|---|
smoke |
Verifies only that the code does not crash. Lifecycle tests (setup/loop/teardown without a crash, zero-residue after teardown) belong here. | ArtNetOutModule - lifecycle without layer (should not crash) |
format |
Checks a string or schema format: field names present, healthReport structure, getSchema shape. No behavior verified. These are the "trivial" tests — necessary but not sufficient. | ArtNetInModule - healthReport format |
behavioral |
Verifies actual module behavior: output values, state transitions, field round-trips, boundary conditions, counts. The majority of tests should be here. | ArtNetOutModule - multi-universe: 170 pixels use 1 universe, 171 use 2 |
integration |
Wires multiple modules together or exercises a cross-module path: pipelines, protocol correctness, HTTP/WebSocket end-to-end, loopback. | ArtNetOut loopback to ArtNetIn on 127.0.0.1 |
The same four levels apply to live system tests (deploy/live_suite.py). Classification is assigned when the test is registered:
runner.register("test1", test1_ripples_pipeline, level="integration")
runner.register("test3", test3_resize_grid, level="behavioral")
runner.register("test0", test0_infrastructure, level="format")
Aim: the breakdown in test-results.md should have few smoke and format tests relative to behavioral and integration. A codebase where most tests are smoke or format is under-tested.
Definition of Done for a Module¶
A Module is not merged until all of these are in place:
- [ ] Tests — non-trivial doctest cases covering the Module's real behavior. A compile-only test does not count.
- [ ] Doc page — a short page under
docs/modules/describing what the Module does, its controls, and any platform constraints. - [ ] JSON schema — control definitions and state fields, versioned.
- [ ] Footprint — the Module's contribution to flash and heap is reported by CI and does not push the ESP32 classic build over budget.
Footprint Budgeting¶
The ESP32 classic flash/heap budget is enforced by an automated CI report on every PR. The report is written to the GitHub Actions step summary and shows the delta versus the last main build.
Budget caps (ESP32-D0WDQ5-V3, 1.25 MiB flash / 320 KiB static RAM)¶
| Metric | Total | Budget cap | Yellow (near limit) | Red (over budget) |
|---|---|---|---|---|
| Flash | 1 310 720 B | 1 048 576 B (1 MiB, 80%) | > 943 718 B (90% of cap) | > 1 048 576 B |
| Static RAM | 327 680 B | 65 536 B (64 KiB, 20%) | > 58 982 B (90% of cap) | > 65 536 B |
Thresholds are constants in scripts/esp32_footprint.py. Update there first, then update this table.
Rule of thumb: if a change to one Module makes another Module too large to fit, the change is the problem, not the victim.
AI-Specific Writing Rules¶
AI agents tend to produce recognisable writing patterns that make text feel machine-generated. Avoid them in all documentation, comments, and commit messages:
- No em dashes (
—). Use a comma, colon, or parentheses instead. Em dashes are a strong AI-style marker. - No excessive hedging. Phrases like "it is worth noting that", "it is important to mention", "please note" add no information.
- No trailing summaries. Do not end a response or section with "In summary, …" or "To recap, …" — the reader can see what was written.
- No filler openers. Do not start a sentence with "Certainly!", "Absolutely!", "Great question!", or similar.
- Concrete over vague. "212/212 unit tests pass" beats "the tests are all passing successfully".
These rules are checked during PR review. If a doc page reads like it was written by a language model, it needs a rewrite pass.
AI in the Workflow¶
projectMM uses AI agents as first-class contributors. We stay tool-agnostic: currently Claude Code for generation and CodeRabbit for PR review.
Rules:
- AI-generated code is held to the same review bar as human code.
- AI-generated commits are labeled as such (commit trailer or PR description).
- An AI agent must be able to run the test suite locally or in CI before the PR is considered ready.
AI attribution¶
Commits and PRs generated with AI assistance carry a Co-Authored-By trailer:
| Tool | Trailer |
|---|---|
| Claude Code (Sonnet 4) | Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> |
| Claude Code (Opus 4) | Co-Authored-By: Claude Opus 4 <noreply@anthropic.com> |
The PR description should note the agent and sprint context (e.g. "Generated with Claude Code, Sprint 8").
CodeRabbit¶
CodeRabbit performs automated PR review. Configuration lives in .coderabbit.yaml at the repo root.
Key review rules encoded in .coderabbit.yaml:
src/**/*.h— no allocations or blocking calls inloop(), teardown completeness,healthReport()required.tests/**/*.cpp— behavioral tests required, not just smoke/format.docs/**/*.md— no em dashes, no trailing summaries, module doc page structure.src/pal/Pal.h— three-way platform guard pattern.
To install CodeRabbit, add the CodeRabbit GitHub App to the repository. The .coderabbit.yaml file is picked up automatically on the first PR review.
Contribution Workflow¶
- Pick or open an issue. No work lands without a tracked rationale.
- Branch from
main. Short, descriptive branch names. - Write the change with tests. Tests first when practical.
- Open a PR. Fill in the template: what changed, why, how tested, footprint impact on ESP32 classic.
- Automated review. CI builds all platforms; CodeRabbit (or equivalent) comments.
- Human review. At least one maintainer approval required.
- Merge. Squash-merge by default; keep
mainlinear.
Lowering the barrier to a first contribution is an explicit goal. A new contributor — human or AI — should be able to make a meaningful change on day one. If the workflow blocks that, the workflow is wrong.