Skip to content
Open
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
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fix, keeping main clean) — creating one silently moves work somewhere the PO i

1. **Pick.** One module/effect/driver/capability — the product owner picks what to build next.
2. **Spec.** Specs before code: the module spec and the UI spec sufficient to implement from (a draft may sit in the backlog until it ships); when in doubt, ask.
3. **Plan.** Plan mode before every feature; save the approved plan to `docs/history/plans/` as `Plan-YYYYMMDD - <title>.md` — a temporary document: it ends up as the PR description and the file is deleted once the plan is realized; the merged PR is the design record. For a restructure ("make it simpler/cleaner"): enumerate 2–4 end states, name what each gains and loses, pick the leanest that solves the actual problem; propose as a question, implement only what's picked; surface follow-ups before starting so it's one coherent refactor.
3. **Plan.** Plan mode before every feature; save the approved plan to `docs/history/plans/` as `Plan-YYYYMMDD - <title>.md` — a temporary document: it ends up as the PR description and the file is deleted once the plan is realized; the merged PR is the design record. **Deleting a plan is the product owner's call — never the agent's.** "The code is written" is not "the plan is realized": a plan is realized when its *verification* is done too, including the judgement steps (thresholds tuned, results read together, the bench check). Ask; do not infer it from a green build. For a restructure ("make it simpler/cleaner"): enumerate 2–4 end states, name what each gains and loses, pick the leanest that solves the actual problem; propose as a question, implement only what's picked; surface follow-ups before starting so it's one coherent refactor.

### Build

Expand Down Expand Up @@ -74,6 +74,8 @@ Commit message: title ≤ 72 characters, imperative. Then a 1–3 sentence end-u

**Reviewer at commit-time:** run the Reviewer on the staged diff when the commit is large (roughly ten files or more across areas) or on PO request — start it first so the other checks run in parallel; findings fixed or accepted-with-reason before "commit now".

**Handling review findings** — from the Reviewer, CodeRabbit, or a human: *verify each finding against current code; fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.* A reviewer reads a snapshot and can be wrong or already out of date, so a finding is a claim to check, not an instruction to apply. Work through **every** finding, lowest severity first — a nit is a one-line fix while attention is cheap, and leaving the small ones for later means they are never done. Rising to the serious findings last also means the cheap context is already loaded.

### Merge

The PO pushes the branch; external review runs on the PR; findings are processed on the branch. On "run pre-merge": `uv run moondeck/event/premerge.py`, which re-runs the mechanical checks over the whole branch diff and lists the judgment gates it cannot decide.
Expand Down
13 changes: 13 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ Abstractions are added when a concrete implementation needs them, not pre-design

**Platform boundary (hard rule).** All `#ifdef`, `#if defined`, platform-specific `#include`s, and hardware API calls live exclusively in `src/platform/`. Everything outside `src/platform/` compiles on every target without modification. Compile-time platform branching uses `if constexpr` on `platform_config.h` flags, never a preprocessor `#ifdef`. The boundary is enforced by [`moondeck/check/check_platform_boundary.py`](../moondeck/check/check_platform_boundary.py), a commit gate (see [CLAUDE.md § The Process](../CLAUDE.md#the-process)).

**The desktop build runs everything (hard rule).** Every module, effect and driver in the repo
links and runs on the host — the platform layer simply has no silicon behind the call. Where a
peripheral is absent the host *emulates* it rather than declaring itself incapable: the parallel
WS2812 buses are backed by heap buffers, `lcdLanes` / `parlioLanes` / `rmtTxChannels` report a
real chip's counts, and `hasLcdCam` is true. Code excluded from the host binary is code that
cannot be unit-tested, cannot be seen by any AST-based check, and only ever runs where it is
hardest to debug — which is exactly what the LED drivers were until they were linked here.

A capability flag therefore answers *"can this build exercise the path?"*, not *"is this real
hardware?"*. Where a flag must mean the latter (`hasLcdCam` gating the pin expander), that is a
deliberate, commented exception. Timing, wire protocol and pin state are NOT emulated: they need
silicon, and faking them would let a self-test report on hardware it never touched.

## Firmware vs deviceModel vs board

Three distinct things, kept distinct in the vocabulary:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,70 @@ lizard covers 94% of it already.

## Verification

1. `uv run moondeck/check/check_clang_query.py --rule comments` — table appears, split by scope.
2. **Control check that must fire** (a zero is indistinguishable from a tool that read nothing):
the report must contain `MoonLedDriver.h` at ~9731 chars / 129 lines and `HttpServerModule.h`
at ~8085 / 101. If the longest known comments are absent, the run is broken, not the tree.
3. Cross-check against the source-text count in this plan: **619 blocks** (class 114 / method 400
/ field 63 / other 42). A materially lower AST count means the matcher is missing a scope —
the `varDecl`-without-`fieldDecl` mistake that silently dropped every class member from the
array rule.
4. Run it through the **MoonDeck card**, not the terminal, so the PO's 📄 log is the real number.
5. `cmake --build build` + `ctest` unaffected (no `src/` change); `check_specs.py` clean.
6. Then read the output together and set the per-scope thresholds.
1. ✅ The rule runs and prints a per-scope table.
2. ✅ **Control check fired.** `HttpServerModule.h` at 7968/101 and `MoonI80Peripheral` at
9334/129 are both in the report. Four parsing bugs were caught by exactly this step: every
NAME read as `col`; the largest comment in the tree dropped by a fixed 80-line lookahead;
~45 one-line `///` blocks missed because a same-line span prints `<col:23, col:71>` with no
line number; and 365 of 474 rows mis-attributed until the matcher bound the declaration.
3. ✅ Cross-checked. **629 found** (class 124 / method 400 / field 105) against 642 in source —
the remainder is 4 blocks in `ImprovFrame.h` plus enum/typedef scopes outside the matcher.
4. ✅ Run through the MoonDeck card; the log reads 629.
5. ✅ 876 unit tests, 19 scenarios, specs clean, three ESP32 firmwares byte-identical.
6. ◻ **OPEN — the reason this plan is not yet realized.** Read the output together and set the
per-scope thresholds. The report deliberately ships with none.

### Where the thresholds landed

The report went through three shapes on PO direction, each discarded for a measured reason:

1. **Per comment, by characters** — the biggest single comments. Covered only `///`, so it saw
24% of the tree's comment lines, and could not tell "one huge comment" from "a file of many".
2. **Per file, by line ratio** — hid the outlier inside an average (a 16-line comment in a 27-line
header is fine), and the PO's "2× the line count" budget could not apply: `/// lines ÷ total
lines` is capped at 1.0 by construction, so 2.0 would flag nothing.
3. **Per declaration, by words** — what shipped. A line is a formatting accident; words are what a
reader absorbs. Measured: a comment line carries a median of **13 words** in both kinds, so the
PO's line yardstick (class 10, method/attribute 3) converts to **130 / 40 / 40 words**.

`DOC DEVIATION` is the signed % against those ideals. `DEV WORDS` is a raw count with no ideal,
because zero IS the ideal there — measuring deviation from "one line" made the best case (no
developer note at all) read as -100%, i.e. worst. Rows sort by |deviation| so both failure modes —
bloated and missing — surface together.

A `VIS` column separates public from private: doxygen publishes only public members, so an
undocumented public method is an API gap while a private one is a maintenance note. Both are
reported; a bloated comment is bloat either way.

**No cutoff is enforced.** The ideals are a ruler — `MoonI80Peripheral`'s header may be right at
ten times the ideal, because it IS the driver's spec.

## Grew out of this plan: the host runs every driver

Not in the original scope, added on PO direction while implementing. The plan predicted a
permanent coverage limit — 184 `///` blocks in `src/light/drivers/` that the desktop AST could
never see, because the drivers were `#if defined(CONFIG_SOC_*)`-gated in `main.cpp`. The PO's
question ("shouldn't we instead run all existing drivers on the desktop?") turned out to be
right, and the premise wrong: those headers have zero direct ESP includes, already route
everything through `platform.h`, and already had desktop stubs. Only the *constants* said "not
my chip".

So the host now **emulates** the peripherals rather than declaring itself incapable:
`lcdLanes`/`parlioLanes` 16, `rmtTxChannels` 4, `hasLcdCam` true, and the platform seams back the
buses with heap memory instead of returning `false`/`nullptr`. `ParallelLedDriver` runs on macOS
against all three real backends, switchable live.

Recorded as a hard rule in [architecture.md § Platform abstraction](../../architecture.md). Its
limit is deliberate: timing, wire protocol and pin state are NOT emulated, because faking them
would let a self-test report on hardware it never touched.

Coverage side effects, all from the same change: doc comments 454 → 629, RAM-costing arrays
362 → 390, heap allocation sites 63 → 80. Three ESP32 firmwares stayed byte-identical.

**Follow-up the PO raised, not yet planned:** a host LED-strip emulator that decodes the encoded
buffer back to RGB — `busLoopback` is already the "read back what the wire carried" seam, with a
fully specified `RmtLoopbackResult`. That would catch encode bugs `PreviewDriver` structurally
cannot, since it shows the *layer* buffer rather than the wire.

## Deliberately not in this plan

Expand Down
46 changes: 23 additions & 23 deletions docs/metrics/repo-health.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"commit": "4a2effa2",
"commit": "1af1be61",
"flash": {
"esp32": 1684240,
"esp32p4-eth": 1497264,
"esp32": 1678368,
"esp32p4-eth": 1497168,
"esp32p4-eth-wifi": 1793760,
"esp32s3-n16r8": 1666592,
"esp32s3-n8r8": 1666592,
"esp32s31": 1919136,
"desktop": 878680
"desktop": 942552
},
"perf": {
"desktop": {
"tick_us": 127,
"fps": 7874
"tick_us": 2741,
"fps": 364
},
"esp32": {
"tick_us": 4164,
Expand All @@ -21,52 +21,52 @@
},
"loc": {
"core": 14827,
"light": 19515,
"platform": 11914,
"light": 19540,
"platform": 12054,
"ui": 5811,
"test": 34447,
"moondeck": 18318
"test": 34483,
"moondeck": 18766
},
"comments": {
"core": {
"lines": 5549,
"ratio": 0.408
},
"light": {
"lines": 7457,
"ratio": 0.422
"lines": 7482,
"ratio": 0.423
},
"platform": {
"lines": 3937,
"ratio": 0.366
"lines": 3994,
"ratio": 0.367
},
"ui": {
"lines": 1518,
"ratio": 0.278
},
"test": {
"lines": 5898,
"lines": 5912,
"ratio": 0.198
},
"moondeck": {
"lines": 2781,
"ratio": 0.174
"lines": 2938,
"ratio": 0.18
}
},
"tests": {
"cases": 970,
"cases": 974,
"scenarios": 22
},
"docs": {
"md_files": 169,
"md_lines": 22610,
"md_lines": 22534,
"plans_files": 89,
"backlog_lines": 3114,
"lessons_lines": 378,
"claude_md_lines": 126
"backlog_lines": 3113,
"lessons_lines": 379,
"claude_md_lines": 135
},
"complexity": {
"functions": 2129,
"functions": 2135,
"over_threshold": 136,
"worst_ccn": 93
}
Expand Down
28 changes: 14 additions & 14 deletions docs/metrics/repo-health.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# Repo health

Measured at `4a2effa2`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run — **do not edit by hand**.
Measured at `1af1be61`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run — **do not edit by hand**.

Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human.

## Firmware size

| Target | Flash |
|---|---:|
| desktop | 858 KB (+0 KB) ⚠ |
| esp32 | 1,645 KB |
| desktop | 920 KB |
| esp32 | 1,639 KB |
| esp32p4-eth | 1,462 KB |
| esp32p4-eth-wifi | 1,752 KB |
| esp32s3-n16r8 | 1,628 KB |
Expand All @@ -20,32 +20,32 @@ Current state only; the trend is this file's git history (`git log -p docs/metri

| Target | Tick | FPS |
|---|---:|---:|
| desktop | 127 µs | 7,874 |
| desktop | 2,741 µs (+2,544 µs) ⚠ | 364 (−4,712) ⚠ |
| esp32 | 4,164 µs | 240 |

## Code

| Area | Lines | Comments | Comment share |
|---|---:|---:|---:|
| core | 14,827 | 5,549 | 40.8 % |
| light | 19,515 | 7,457 | 42.2 % |
| platform | 11,914 (+24) ⚠ | 3,937 | 36.6 % |
| light | 19,540 (+1) ⚠ | 7,482 | 42.3 % |
| platform | 12,054 (−1) ✓ | 3,994 | 36.7 % |
| ui | 5,811 | 1,518 | 27.8 % |
| test | 34,447 | 5,898 | 19.8 % |
| moondeck | 18,318 | 2,781 | 17.4 % |
| test | 34,483 (−82) ✓ | 5,912 | 19.8 % (−0.1 %) ✓ |
| moondeck | 18,766 (+30) ⚠ | 2,938 | 18.0 % (+0.1 %) ⚠ |

## Tests

| Kind | Count |
|---|---:|
| unit cases | 970 |
| unit cases | 974 |
| scenarios | 22 |

## Complexity

| Metric | Value |
|---|---:|
| functions | 2,129 (+1) ✓ |
| functions | 2,135 |
| over threshold | 136 |
| worst CCN | 93 |

Expand All @@ -54,9 +54,9 @@ Current state only; the trend is this file's git history (`git log -p docs/metri
| Metric | Value |
|---|---:|
| markdown files | 169 |
| markdown lines | 22,610 (+18) ⚠ |
| markdown lines | 22,534 (+2) ⚠ |
| plan files | 89 |
| backlog lines | 3,114 |
| lessons lines | 378 |
| CLAUDE.md lines | 126 |
| backlog lines | 3,113 |
| lessons lines | 379 |
| CLAUDE.md lines | 135 |

Loading
Loading