Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 11 additions & 60 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ language: "en-US"

reviews:
profile: "assertive"
# Auto-review disabled on purpose: reviews are triggered on demand so they land
# when the product owner wants them, within CodeRabbit's hourly review budget.
# Comment "@coderabbitai review" (incremental) or "@coderabbitai full review" on
# the PR to request one.
# Reviews run on request, "@coderabbitai review" (incremental) or "@coderabbitai full review" on the PR, so they land when the product owner wants them, within CodeRabbit's hourly budget.
auto_review:
enabled: false
drafts: false
Expand All @@ -17,60 +14,14 @@ reviews:
- "!**/build/**"
- "!test/doctest.h"

# The rules have one home each; CodeRabbit reads them from the files below rather than from a copy here.
instructions: |
This is a high-performance multi-platform C++20 system driving LEDs and DMX fixtures.
Primary target is ESP32 (ESP-IDF, no Arduino). Also runs on desktop/RPi/Teensy.

Review against these hard rules (from CLAUDE.md):
- Platform boundary: all #ifdef, platform #include, and hardware API calls must be in src/platform/ only. Flag any violation outside that directory.
- Hot path (render loop): no heap allocations (new, malloc, push_back, std::string), no blocking (delay, sleep, mutex.lock), prefer integer math over float.
- Warnings are errors: -Wall -Wextra -Werror. No suppressions.
- MoonModules are single-file .h only (implementation inline).
- Code style: #pragma once, constexpr over #define, namespace mm, no "using namespace" in headers.
- std::span over pointer+length where applicable.
- Buffers allocated as single contiguous blocks outside the hot path.

Design principles:
- Minimalism means simplicity. If a contributor can't understand it in 30 seconds, it's too complex.
- Data over objects. Design around data flow, not class hierarchies.
- Concrete first, abstract later. No framework before domain logic works.
- No unnecessary abstractions, helpers, or utilities for one-time operations.

path_instructions:
- path: "src/platform/**"
instructions: |
Platform abstraction layer. This is the ONLY place where #ifdef, platform-specific
#include, and hardware API calls are allowed. Verify implementations match the
API declared in platform.h. Desktop uses std::chrono, std::malloc, BSD sockets.

- path: "src/core/**"
instructions: |
Core infrastructure: MoonModule base, Controls, Scheduler, types, color math.
Must be platform-independent — no platform includes. Controls bind to class
variables by reference. Target <16 bytes per control descriptor on ESP32.
Color math must be constexpr, integer-only, no floats.

- path: "src/light/**"
instructions: |
Light domain: pipeline, buffers, layers, effects, layouts, drivers.
Buffers are raw uint8_t* with configurable channelsPerLight (not RGB structs).
Effects write to buffer using channel offsets. Speed controls use BPM (uint8_t).
All effects must be at least 2D. ArtNet packets need correct byte order
(OpCode/Universe LE, ProtVer/Length BE). Drivers must pace output (FPS limiting).
No heap allocations on the hot path (loop methods).

- path: "test/**"
instructions: |
Unit and integration tests using doctest. Verify tests cover edge cases
and match the specifications in docs/moonmodules/. Tests should not depend
on timing or network.

- path: "CMakeLists.txt"
instructions: |
Root CMake build. Must use C++20, -Wall -Wextra -Werror. Core library is
INTERFACE (header-only). Only platform .cpp files need compilation.

- path: "moondeck/**"
instructions: |
MoonDeck dev console and build/test/run scripts. Scripts must resolve
ROOT path correctly relative to their location in moondeck/subdir/.
Review against CLAUDE.md, docs/contributing/coding-standards.md and docs/contributing/documentation-standards.md, which hold every rule.

knowledge_base:
code_guidelines:
enabled: true
filePatterns:
- "CLAUDE.md"
- "docs/contributing/coding-standards.md"
- "docs/contributing/documentation-standards.md"
69 changes: 69 additions & 0 deletions .github/workflows/findings.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Findings

# The checks' findings in GitHub code scanning: each check writes its findings in one shape (moondeck/check/_findings.py), findings_sarif.py spells them as SARIF, and code scanning shows a pull request the findings its diff adds.
# Each check is a step that passes or fails as it does locally, so a pull request, a fork's included, shows which check a change broke. Its error-level findings are annotated in the files the pull request changes, through workflow commands, which need no token; the SARIF upload, which does, runs for our own branches.
# The checks here need no build: check_code reads a configured tree, so its two compiler rules (the render path and the stack frames, Clang 20 only) are left to the local run.

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
checks:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # the SARIF upload
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
fetch-depth: 0 # the pull request's base, for the files it changes
- uses: astral-sh/setup-uv@v3
- name: install Vale
run: |
curl -sSL https://github.com/errata-ai/vale/releases/download/v3.21.0/vale_3.21.0_Linux_64-bit.tar.gz | tar -xz -C "$RUNNER_TEMP" vale
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
# check_code reads a configured tree; check_docgen parses the headers, and one includes the build_info.h the build generates.
- name: configure the desktop build, and generate build_info.h
run: |
cmake -S . -B build/linux
uv run python moondeck/build/generate_build_info.py
- name: check_code
if: always()
run: uv run moondeck/check/check_code.py
- name: check_docgen
if: always()
run: uv run moondeck/check/check_docgen.py
# The pull request's own added lines, from its base; on main the whole-tree ratchet alone.
- name: check_prose
if: always()
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: uv run moondeck/check/check_prose.py ${BASE:+"$BASE...HEAD"}
- name: check_specs, check_taglines, check_devices, check_firmwares
if: always()
run: |
rc=0
for c in specs taglines devices firmwares; do uv run "moondeck/check/check_$c.py" || rc=1; done
exit $rc
- name: the findings in the files the pull request changes
if: always()
env:
BASE: ${{ github.event.pull_request.base.sha }}
run: |
if [ -n "$BASE" ]; then git diff --name-only "$BASE"...HEAD > "$RUNNER_TEMP/changed.txt"; fi
uv run moondeck/ci/github_annotations.py --findings --changed "$RUNNER_TEMP/changed.txt"
- name: the findings as SARIF
if: always()
run: uv run moondeck/ci/findings_sarif.py --out build/sarif
# A pull request from a fork gets a read-only token, which cannot upload; the annotations above carry its findings.
- if: always() && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository)
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: build/sarif
31 changes: 25 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ jobs:
# readable in the log instead of folded into a single count. Named here rather than split into
# its own step so the compiler still runs exactly once.
- name: every MoonLive script is valid C++
run: uv run --with pytest pytest test/python/test_scripts_are_cpp.py -v
run: uv run --with pytest pytest test/python/test_scripts_are_cpp.py -v --junitxml=results/junit-scripts.xml
# The rest, quietly. This directory is excluded because the step above already ran it: a bare
# `pytest test/python` here would compile all 34 scripts a second time.
- name: pytest
run: >
uv run --with pytest --with pyserial --with markdown --with wled --with pyyaml --with jsonschema
pytest test/python -q --ignore=test/python/test_scripts_are_cpp.py
pytest test/python -q --ignore=test/python/test_scripts_are_cpp.py --junitxml=results/junit-python.xml
# Each failing test annotated where it fails, and the counts in the job summary, through workflow commands: no token, so a fork's pull request gets them too.
- if: always()
run: uv run moondeck/ci/github_annotations.py --junit results/*.xml

js:
runs-on: ubuntu-latest
Expand All @@ -86,7 +89,14 @@ jobs:
# Node's built-in test runner — no npm install, no package.json. The glob
# form is required: a bare directory arg is treated as a module to execute.
- name: node --test
run: node --test "test/js/**/*.test.mjs"
run: |
mkdir -p results
node --test --test-reporter=spec --test-reporter-destination=stdout \
--test-reporter=junit --test-reporter-destination=results/junit-js.xml "test/js/**/*.test.mjs"
- uses: astral-sh/setup-uv@v3
# Each failing test annotated where it fails, and the counts in the job summary, through workflow commands: no token, so a fork's pull request gets them too.
- if: always()
run: uv run moondeck/ci/github_annotations.py --junit results/*.xml

# The Teensy 4.1 firmwares, built on every PR: a change to a platform seam the Teensy must implement fails here, not on the bench.
# The HEX images are kept as an artifact, so a PR's build can be flashed and tried before it merges.
Expand Down Expand Up @@ -177,7 +187,16 @@ jobs:
# third-party static init; keep the signal on what we're actually looking for.
#
# RTSan does NOT halt: the render path's known blocking calls are counted in
# docs/reference/metrics/code.md and backlogged as architecture work, so halting would
# check_code (docs/reference/metrics/code.json) and backlogged as architecture work, so halting would
# fail this lane on every run. It reports; the log is the signal.
ASAN_OPTIONS=detect_leaks=0 TSAN_OPTIONS=halt_on_error=1 \
RTSAN_OPTIONS=halt_on_error=0 ./build/san/test/mm_tests
# The address lane writes the unit results as JUnit for the annotations: one lane is enough.
results=""
if [ "${{ matrix.kind }}" = "address" ]; then
mkdir -p results
results="--reporters=junit --out=results/junit-unit.xml"
fi
# TSan reports every race of the run and still fails it (exit code 66): halting at the first hid the next one behind each fix.
ASAN_OPTIONS=detect_leaks=0 TSAN_OPTIONS=halt_on_error=0 \
RTSAN_OPTIONS=halt_on_error=0 ./build/san/test/mm_tests $results
- if: always() && matrix.kind == 'address'
run: uv run moondeck/ci/github_annotations.py --junit results/*.xml
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,6 @@ __pycache__/
# MkDocs — built docs site (rendered fresh in CI, never committed)
/site/

# Generated test inventory pages — built into the site by moondeck/docs/mkdocs_hooks.py
# (the CLI generate_test_docs.py can still write them locally, but they are not committed).
/docs/reference/tests/*.md


# Read-only hardware snapshots (flash/NVS dumps pulled off reverse-engineered boards).
# Root-anchored + dir-scoped, like /build/ above: a bare `.snapshots` would also swallow any
Expand Down
2 changes: 1 addition & 1 deletion .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ BasedOnStyles =
# suggestion becomes an error there, so a regression fails the gate instead of annotating a PR.
# Add a page here when `vale <page>` reports nothing; the sweep plan in docs/work/present/ is the
# list. When every page is here, the two prose scripts go and this section becomes the default.
[docs/how-to/installing-on-linux.md]
[docs/reference/installing-on-linux.md]
BasedOnStyles = MoonLight
MoonLight.SentenceLength = error
MoonLight.Weasel = error
Expand Down
9 changes: 9 additions & 0 deletions .vale/styles/MoonLight/LinkText.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# documentation-standards.md: "A link's text says what it reaches." "See here" does not tell the reader whether to follow
# it. The words below, alone as the text of a link, name nothing.
extends: existence
message: "A link reading '%s' does not say what it reaches: name the page or the thing."
level: warning
ignorecase: true
scope: raw
raw:
- '\[(?:here|this|link|this link|click here|more|read more|page|this page)\]\('
14 changes: 14 additions & 0 deletions .vale/styles/MoonLight/TonePadding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# documentation-standards.md: "No sentence whose job is tone." Politeness and throat-clearing carry no fact the reader needs.
extends: existence
message: "'%s' is tone, not a fact: cut it and state what is true or what to do."
level: warning
ignorecase: true
tokens:
- please
- feel free to
- note that
- it should be noted
- it is worth noting
- keep in mind that
- don't hesitate
- as you can see
14 changes: 14 additions & 0 deletions .vale/styles/MoonLight/VagueClose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# documentation-standards.md: say what the list holds. A closer that says there is more, without saying what, leaves the reader
# guessing what the rest is.
extends: existence
message: "'%s' says there is more without saying what: name it or end the list."
level: warning
ignorecase: true
nonword: true
# A closer ends its list: "and more" before punctuation or the line's end, so "and more robust" is a comparison; the others at a word's end. "etc" starts after a space or a bracket, so the path /etc/ is not a closer.
tokens:
- '(?:^|[\s(])etc\b\.?'
- 'and so on\b'
- 'and more(?:[.,;:)]|$)'
- 'and so forth\b'
- 'and the like\b'
Loading
Loading