Skip to content

Compiler-checked hot-path discipline and tier-zero warnings - #56

Closed
ewowi wants to merge 4 commits into
mainfrom
next
Closed

Compiler-checked hot-path discipline and tier-zero warnings#56
ewowi wants to merge 4 commits into
mainfrom
next

Conversation

@ewowi

@ewowi ewowi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What this lands

Hot-path discipline, checked by the compiler. MoonModule::tick/tick20ms/tick1s carry
MM_NONBLOCKING, and Clang verifies under -Wfunction-effects that nothing the render path
reaches can block or allocate — transitively, through the whole call graph. That is the half
check_hotpath.py's regex cannot see: it reads the text of a tick body and is blind to what its
callees do.

The attribute is inherited by overrides, so three annotations cover ~90 modules. It also sits in
tickChildren's member-pointer type — without that, the indirect dispatch is a hole every
module's tick escapes through. Passing an unannotated method is now a compile error.

MM_NONBLOCKING is empty on GCC: the ESP32 toolchain has neither the attribute nor the warning
and builds with -Werror, so a bare attribute there is a build break. Same compiler-gated shape
as MM_PRINTF_FORMAT.

A MoonDeck card, "clang-hotpath", reports the 181 findings this surfaced, split by tier —
70 on tick() (every frame), 6 on tick20ms(), 95 on tick1s(), 10 unresolved — because
the same blocking call costs orders of magnitude more in one than another. The sharp ones are UDP
sendTo/recvFrom inside AudioService::tick: socket I/O every frame.

Five tier-zero warnings join -Wall -Wextra -Werror: -Wshadow -Wnon-virtual-dtor -Wdouble-promotion -Wimplicit-fallthrough -Wnull-dereference. The ESP32 build is clean under
them too. Three real defects fixed:

  • Rings241Layout: 1.1 was a double literal promoting a float — a silent softfloat call on a
    chip with no FPU.
  • RubiksCubeEffect: a parameter shadowed the effect's cubeSize control field.
  • unit_PreviewDriver: a test double with virtual functions and a public non-virtual destructor.

Correction recorded

Earlier text in this repo claimed check_hotpath.py covers src/platform/esp32/. It does
not.
Measured: it scans 67 tick methods, all in src/core/ and src/light/, zero in the
platform layer — which has no tick methods, being free functions the tick path calls into. All 60
of its files compile on desktop, so the compiler check covers the identical set. It reports 0
findings where the compiler reports 181.

Not done here

The 181 findings are backlogged, along with deleting check_hotpath.py (170 lines). The blocker
is gate order, not coverage: the script fails pre-commit today while -Wfunction-effects
carries -Wno-error, so deleting it first would leave hot-path discipline with nothing
enforcing. Sequence: triage → drop -Wno-error → delete and swap the gate.

Performance

tick:4231us(FPS:236) on esp32s3-n16r8, measured on MM-testbench-S3. Desktop tick:129us.
The annotation is compile-time only: ESP32 flash is byte-identical at 1,667,104 B.

Summary by CodeRabbit

  • New Features

    • Added a “Last-run log” button to script cards, allowing the most recent script output to be replayed without rerunning the script.
    • Script output is now streamed live and saved for later viewing, with automatic log-size limits.
  • Bug Fixes

    • Improved desktop and ESP32 connection-state handling for more reliable network status reporting.
    • Corrected several rendering edge cases involving empty or degenerate grids.
  • Documentation

    • Updated static-analysis, hot-path, and repository health documentation and metrics.

MoonModule's three tick methods now carry MM_NONBLOCKING, so Clang verifies
transitively that nothing the render path reaches can block or allocate — the
half a regex over the tick body could never see. A new MoonDeck card reports
the 181 findings that surfaced, split by tick tier. Five more compiler warnings
join -Wall -Wextra -Werror, catching three real defects.

KPI: 16384lights | Desktop:856KB | ESP32:1499KB | tick:4231us(FPS:236) | heap:8294KB | lizard:136w

**Core**
- MM_NONBLOCKING (platform.h): `noexcept [[clang::nonblocking]]` on Clang 20+,
  bare `noexcept` on GCC. The ESP32 toolchain has neither the attribute nor the
  warning and builds with -Werror, so a bare attribute there is a build break —
  same compiler-gated shape as MM_PRINTF_FORMAT.
- The attribute is inherited by overrides, so three annotations cover ~90
  modules. It also sits in `tickChildren`'s member-POINTER TYPE: without that,
  the indirect dispatch is a hole every module's tick escapes through. Passing
  an unannotated method is now a compile error.
- Five platform reads (millis, micros, ethLinkUp, ethConnected,
  wifiStaConnected) annotated: they are register/variable reads, and marking
  them took the finding count from 209 to 10. Most of the 209 were unannotated
  helpers, not violations.
- Rings241Layout: `1.1` → `1.1f`. The double literal promoted a float on a chip
  with no FPU — a silent softfloat call in a layout path.
- RubiksCubeEffect: `init(uint8_t cubeSize)` shadowed the effect's cubeSize
  control field; renamed to `order`.

**Scripts/MoonDeck**
- check_nonblocking.py + the "clang-hotpath" card. Reports unique SITES: a
  header included by N translation units warns N times, so a raw build prints
  ~1350 lines for 181 findings.
- Split by tier — 70 on tick() (every frame), 6 on tick20ms(), 95 on tick1s(),
  10 unresolved — because the same blocking call costs orders of magnitude more
  in one than another. Clang names the call and the callee but NOT the
  enclosing function, so the tier is read back from source.
- Columns: CALLS / IN / WHY IT BLOCKS / FILE:LINE, aligned with the other
  reports. WHY comes from clang's own `note:` line.
- clang-query card renamed from "AST Rules" so every tools card names its tool.

**Tests**
- unit_PreviewDriver: CaptureBroadcaster had virtual functions and a public
  non-virtual destructor. It cannot copy the base's protected-destructor trick
  (that forbids the stack construction the tests rely on), so a pragma scoped
  to the one struct carries the reason.

**Docs/CI**
- Plan-20260727: steps 1 and 3 recorded, net -68 lines. Three sections deleted
  as superseded by shipped code — the nonblocking probe, the clang-query probe,
  and the clang-tidy configuration walkthrough, whose reasons live in
  .clang-tidy itself.
- CORRECTION: earlier text claimed check_hotpath.py covers src/platform/esp32/.
  It does not. Measured: it scans 67 tick METHODS, all in src/core/ and
  src/light/, zero in the platform layer — which has no tick methods, being
  free functions the tick path calls into. All 60 of its files compile on
  desktop, so the compiler check covers the identical set. It reports 0 where
  the compiler reports 181.
- Backlogged: triage the 181, then delete check_hotpath.py (170 lines). The
  blocker is gate ORDER, not coverage — the script fails pre-commit today while
  -Wfunction-effects carries -Wno-error, so deleting it first leaves nothing
  enforcing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bca7209f-95a6-4d35-a727-0929d0724123

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/platform/desktop/platform_desktop.cpp (1)

108-129: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the Clang-only diagnostic pragmas.

platform_desktop.cpp is built on desktop GCC/Clang with -Wall -Wextra -Werror; #pragma clang diagnostic only belongs inside a Clang check so GCC doesn’t emit a warning in a warning-as-error build.

Wrap these pragmas in #if defined(__clang__) / #endif.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/desktop/platform_desktop.cpp` around lines 108 - 129, Guard the
clang diagnostic push, ignored -Wfunction-effects, and pop directives
surrounding millis() and micros() with `#if` defined(__clang__) and a matching
`#endif`, leaving the clock implementations unchanged for GCC and Clang builds.
src/light/layers/Layer.h (1)

148-192: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Defer applyState() out of the annotated render tick.

Line 192 can recursively call prepare()/release(), resize ScratchBuffers, and touch module peripherals. A beat-driven modifier can therefore trigger allocation or blocking work directly inside MM_NONBLOCKING tick(). Queue the rebuild request and execute it through the scheduler’s non-render prepare path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/layers/Layer.h` around lines 148 - 192, Defer modifier-triggered
rebuilds out of Layer::tick() instead of calling applyState() from the
MM_NONBLOCKING render path. Preserve the existing coalescing of
consumeNeedsRebuild() requests, but queue the rebuild for the scheduler’s
non-render prepare path so applyState(), including prepare/release,
ScratchBuffer resizing, and peripheral access, runs outside tick().

Source: Path instructions

src/core/HttpServerModule.cpp (1)

67-107: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep synchronous transport work out of both annotated callbacks.

  • src/core/HttpServerModule.cpp#L67-L107: replace synchronous handleConnection() processing with resumable/nonblocking work; the outer 100 ms budget cannot bound a single call.
  • src/core/HttpServerModule.cpp#L111-L112: route WLED state frames through the resumable sender or a transport task instead of inline TcpConnection::write calls.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/HttpServerModule.cpp` around lines 67 - 107, Keep synchronous
transport work out of both annotated callbacks: in src/core/HttpServerModule.cpp
lines 67-107, replace inline handleConnection processing in
HttpServerModule::tick20ms with resumable/nonblocking connection handling so the
outer 100 ms budget can actually bound work; in src/core/HttpServerModule.cpp
lines 111-112, route WLED state frames through the existing resumable sender or
a transport task instead of performing inline TcpConnection::write calls.

Source: Path instructions

src/light/effects/RipplesEffect.h (1)

37-70: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Remove per-light floating-point math from this tick.

sqrt, sin, and floor execute inside the x/z render loop on every frame, so cost scales with the grid size and violates the hot-path rule. Use fixed-point/integer lookup tables or precompute bounded wave data outside tick(); keep the inner loop integer-only.

As per path instructions, render-loop callees must avoid per-light floating-point math.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/effects/RipplesEffect.h` around lines 37 - 70, Replace the
per-light sqrt, sin, and floor calculations in RipplesEffect::tick with
fixed-point or integer lookup data prepared outside the x/z render loop, while
preserving the current ripple geometry, timing, and y-coordinate output. Ensure
the inner loop uses only integer arithmetic and existing bounded dimensions,
with any required wave-table or distance precomputation performed once during
setup or otherwise outside tick().

Source: Path instructions

src/core/ImprovProvisioningModule.h (1)

177-197: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep APPLY_OP execution out of the nonblocking tick.

Line 186 invokes HttpServerModule::applyOp(), which can add, replace, or clear modules and trigger allocation/release and prepareTree(). The failure path also calls std::printf at Line 189. Execute the queued operation in a cold/control task and publish only its result/status from tick().

As per path instructions, render-loop callees must avoid blocking operations and heap work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/ImprovProvisioningModule.h` around lines 177 - 197, Move the queued
APPLY_OP execution out of ImprovProvisioningModule::tick(): have a cold/control
task call HttpServerModule::applyOp(), including its logging and status updates,
then publish only the operation result/status and completion state for tick() to
consume. Keep tick() limited to nonblocking state handling and preserve the
existing Ok/AlreadyExists success behavior and failure reporting.

Source: Path instructions

src/light/drivers/HueDriver.h (1)

108-126: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Move Hue network work out of the annotated tick callbacks.

tick() is marked MM_NONBLOCKING, but pushOneChangedLight() reaches synchronous platform::httpRequest() at Line 655, with a timeout of up to 200 ms. tick1s() similarly performs HTTP requests and heap allocation through pollPairing(), fetchLights(), fetchGroups(), and reportBridge(). These callbacks can stall the render loop for 200–400 ms despite the annotation.

Queue this work on a worker or make the platform API genuinely nonblocking; keep the callbacks limited to bounded state updates.

As per coding guidelines, render-loop callbacks and their callees must avoid blocking operations and heap allocations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/HueDriver.h` around lines 108 - 126, Move all Hue network
and allocation work out of the MM_NONBLOCKING callbacks tick() and tick1s().
Replace direct calls to pushOneChangedLight(), pollPairing(), fetchLights(),
fetchGroups(), and reportBridge() with bounded state updates that queue or
signal equivalent work to a worker context, preserving the existing timing and
ordering behavior; ensure the worker performs the synchronous HTTP requests and
heap allocations.

Source: Coding guidelines

test/unit/light/unit_Drivers_rendersplit.cpp (1)

53-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a rationale for the deliberate MM_NONBLOCKING violation in SlowDriver::tick.

test_Drivers_rendersplit.cpp:53 is annotated nonblocking despite using a mutex and condition_variable::wait_for(5s). It also generates a fixed check_nonblocking.py Tick tick() finding, so add an in-file reason explaining the test harness intent and keep the diagnostic scoped if the toolchain accepts a pragma.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/light/unit_Drivers_rendersplit.cpp` around lines 53 - 65, Add an
in-file comment at SlowDriver::tick explaining that its mutex and timed wait are
intentional to hold the test’s use-after-free window open, despite the
MM_NONBLOCKING annotation. If supported by the toolchain, narrowly suppress the
check_nonblocking.py Tick tick() diagnostic around this method only, without
changing the test synchronization behavior.
src/core/FilesystemModule.cpp (1)

50-55: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Move persistence flushing off tick1s().

FilesystemModule::tick1s() is marked MM_NONBLOCKING, but after the debounce it calls flush() → g rows a heap JsonSink and performs LittleFS write/fsync/rename via platform::fsWriteAtomic(). Keep this callback to status polling and route the actual save through a worker / non-hot persistence mechanism.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/FilesystemModule.cpp` around lines 50 - 55, Remove the persistence
call from the nonblocking FilesystemModule::tick1s() callback, retaining only
mount, scheduler, timestamp, dirty-state, and debounce checks for status
polling. Route the debounced save operation through an existing worker or other
non-hot persistence mechanism, ensuring flush() and its filesystem work execute
outside tick1s().

Source: Coding guidelines

src/light/effects/DemoReelEffect.h (1)

56-64: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Defer effect replacement outside the render callback.

advance() calls swapTo() from within tick(), and swapTo() creates/deletes effect children and runs setup/applyState(). That puts allocation and release work on the nonblocking render path; queue the switch and run the replacement in a deferred lifecycle path while making tick() render only the current child.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/effects/DemoReelEffect.h` around lines 56 - 64, Update
DemoReelEffect::tick so it only renders the current child and records a pending
switch when the interval elapses; do not call advance() or swapTo() from the
render callback. Add or use a deferred lifecycle/update path to process the
pending switch outside tick, where effect creation, deletion, setup, and
applyState() can safely run.

Source: Coding guidelines

src/light/drivers/ParallelLedDriver.h (1)

552-578: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep ParallelLedDriver::tick() out of MM_NONBLOCKING while it waits on bus transfers.

tickSync() and tickRing() both call busWaitIfBusy(), which forwards a void busWait(uint8_t, uint32_t) wait to the peripheral. On a wedged transfer this callback can block the render thread for the wait budget before tick() returns, so the MM_NONBLOCKING override should not be advertised until those waits are moved outside this hot path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/ParallelLedDriver.h` around lines 552 - 578, Remove the
MM_NONBLOCKING annotation from ParallelLedDriver::tick() because its tickSync()
and tickRing() paths can block through busWaitIfBusy(); leave the dispatch and
transfer behavior unchanged until those waits are moved out of the render path.

Source: Coding guidelines

src/light/drivers/Drivers.h (1)

385-396: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The new nonblocking contract is violated by synchronous work in three callbacks.

  • src/light/drivers/Drivers.h#L385-L396: remove worker joining/stopping from tick(); use asynchronous recovery.
  • src/light/drivers/NetworkSendDriver.h#L208-L208: enqueue network transmission instead of sending the frame inline.
  • src/light/drivers/RmtLedDriver.h#L234-L295: poll or offload hardware completion and reset timing instead of waiting in tick().

As per coding guidelines, render-loop callbacks and their callees must avoid blocking operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/Drivers.h` around lines 385 - 396, Remove synchronous
worker joining/stopping from Drivers::tick and replace the timed-out recovery
with asynchronous handling; in src/light/drivers/Drivers.h:385-396, preserve
safe buffer ownership without blocking the render callback. In
src/light/drivers/NetworkSendDriver.h:208, update NetworkSendDriver transmission
to enqueue frames rather than send inline. In
src/light/drivers/RmtLedDriver.h:234-295, update RmtLedDriver hardware
completion and reset handling to poll or offload the work instead of waiting in
tick.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md:
- Around line 78-80: Update the collective-noun sentence in the plan text to
read “The bulk of the findings were unannotated helpers,” preserving the
surrounding analysis and meaning.

In `@moondeck/check/check_nonblocking.py`:
- Around line 77-82: Update build_output to check the subprocess result from
cmake; when the build exits with a non-zero return code, surface an error
instead of returning output that is interpreted as no findings. Preserve the
existing combined stdout and stderr behavior for successful builds.
- Around line 127-141: Update the imports and build-directory lookup in the
nonblocking check to use a direct check_clang_tidy import, or an existing public
API, instead of accessing check_clang_query.check_clang_tidy._host_build_dir().
Preserve the current build validation and subsequent filtering behavior.
- Around line 168-172: Update the row-printing loop around subset and clip so
the em-dash default is assigned to a variable before the f-string, then pass
that variable to clip instead of using the Unicode escape inside the f-string
expression. Preserve the existing fallback output and formatting while keeping
the script compatible with Python versions before 3.12.

In `@moondeck/MoonDeck.md`:
- Around line 346-347: Update the tier-frequency statement near “tick()”,
“tick20ms()”, and “tick1s()” to accurately describe the difference between 50
calls per second and 1 call per second; replace “three orders of magnitude” with
“roughly two orders of magnitude” or a similarly non-exaggerated phrase.

In `@src/core/FileManagerModule.cpp`:
- Around line 44-46: Update FileManagerModule::tick1s() so it does not call
platform::filesystemUsed() from the MM_NONBLOCKING path. Cache filesystem usage
from a non-hot-path task and have tick1s() read the cached value, or remove
MM_NONBLOCKING until the usage call is guaranteed nonblocking.

In `@src/core/HttpServerModule.h`:
- Around line 176-177: The nonblocking annotations cover callbacks that still
perform blocking or allocating work. In src/core/HttpServerModule.h lines
176-177, move blocking HTTP response handling out of tick20ms() or use
exclusively nonblocking writes; in src/core/MqttModule.cpp line 414, remove
synchronous DNS and discovery-buffer allocation from tick1s(); and in
src/core/NetworkModule.h line 380, move WiFi/AP lifecycle transitions and tree
rebuilds off the annotated callback. Keep -Wno-error=function-effects explicitly
temporary until all affected paths are asynchronous, bounded, or
allocation-free.

In `@src/core/Scheduler.cpp`:
- Line 66: Remove the MM_NONBLOCKING contract from Scheduler::tick() unless its
dispatch path is made fully nonblocking. Ensure tick20ms(), tick1s(), and their
callees do not perform synchronous transport work such as
HttpServerModule::handleConnection(); otherwise move transport handling outside
the annotated render callback.

In `@src/core/SystemModule.h`:
- Line 232: Update the P4 implementation of tick1s() so its coprocessorWifi()
path does not directly invoke the synchronous
esp_hosted_get_coprocessor_fwversion() query; instead, maintain the firmware
version via a non-render task or nonblocking cached snapshot and have tick1s()
read that cached value while preserving its MM_NONBLOCKING contract.

In `@src/light/drivers/PreviewDriver.h`:
- Line 132: Remove the MM_NONBLOCKING contract from PreviewDriver::tick() until
its reachable work is safe, or refactor sendFrame()’s synchronous socket path
and buildAndSendCoordTable()’s keptIdx_ allocation off the render path; ensure
every operation reachable from tick() avoids blocking I/O and heap allocation
before restoring the annotation.

In `@src/light/effects/BouncingBallsEffect.h`:
- Line 49: The nonblocking render callbacks perform per-element floating-point
work; update BouncingBallsEffect::tick in
src/light/effects/BouncingBallsEffect.h:49-49 to use fixed-point/integer math or
precompute trajectory, damping, square-root, and rounding values off the render
path, and update SphereMoveEffect::tick in
src/light/effects/SphereMoveEffect.h:36-36 to replace per-voxel trigonometric
and distance calculations with integer math or precomputed values.

In `@src/platform/esp32/platform_esp32.cpp`:
- Around line 888-894: Make the cross-task status fields ethLinkUp_,
ethConnected_, and wifiStaConnected_ atomic booleans, and update their
event-handler writes and getter reads to use relaxed stores and loads. Preserve
the existing ethLinkUp() and ethConnected() return behavior while ensuring all
accesses to these flags use the atomic operations.

In `@src/platform/platform.h`:
- Around line 13-25: Update the fallback comment above MM_NONBLOCKING to state
that GCC omits the clang attribute but retains noexcept, accurately describing
the macro’s exception behavior.

---

Outside diff comments:
In `@src/core/FilesystemModule.cpp`:
- Around line 50-55: Remove the persistence call from the nonblocking
FilesystemModule::tick1s() callback, retaining only mount, scheduler, timestamp,
dirty-state, and debounce checks for status polling. Route the debounced save
operation through an existing worker or other non-hot persistence mechanism,
ensuring flush() and its filesystem work execute outside tick1s().

In `@src/core/HttpServerModule.cpp`:
- Around line 67-107: Keep synchronous transport work out of both annotated
callbacks: in src/core/HttpServerModule.cpp lines 67-107, replace inline
handleConnection processing in HttpServerModule::tick20ms with
resumable/nonblocking connection handling so the outer 100 ms budget can
actually bound work; in src/core/HttpServerModule.cpp lines 111-112, route WLED
state frames through the existing resumable sender or a transport task instead
of performing inline TcpConnection::write calls.

In `@src/core/ImprovProvisioningModule.h`:
- Around line 177-197: Move the queued APPLY_OP execution out of
ImprovProvisioningModule::tick(): have a cold/control task call
HttpServerModule::applyOp(), including its logging and status updates, then
publish only the operation result/status and completion state for tick() to
consume. Keep tick() limited to nonblocking state handling and preserve the
existing Ok/AlreadyExists success behavior and failure reporting.

In `@src/light/drivers/Drivers.h`:
- Around line 385-396: Remove synchronous worker joining/stopping from
Drivers::tick and replace the timed-out recovery with asynchronous handling; in
src/light/drivers/Drivers.h:385-396, preserve safe buffer ownership without
blocking the render callback. In src/light/drivers/NetworkSendDriver.h:208,
update NetworkSendDriver transmission to enqueue frames rather than send inline.
In src/light/drivers/RmtLedDriver.h:234-295, update RmtLedDriver hardware
completion and reset handling to poll or offload the work instead of waiting in
tick.

In `@src/light/drivers/HueDriver.h`:
- Around line 108-126: Move all Hue network and allocation work out of the
MM_NONBLOCKING callbacks tick() and tick1s(). Replace direct calls to
pushOneChangedLight(), pollPairing(), fetchLights(), fetchGroups(), and
reportBridge() with bounded state updates that queue or signal equivalent work
to a worker context, preserving the existing timing and ordering behavior;
ensure the worker performs the synchronous HTTP requests and heap allocations.

In `@src/light/drivers/ParallelLedDriver.h`:
- Around line 552-578: Remove the MM_NONBLOCKING annotation from
ParallelLedDriver::tick() because its tickSync() and tickRing() paths can block
through busWaitIfBusy(); leave the dispatch and transfer behavior unchanged
until those waits are moved out of the render path.

In `@src/light/effects/DemoReelEffect.h`:
- Around line 56-64: Update DemoReelEffect::tick so it only renders the current
child and records a pending switch when the interval elapses; do not call
advance() or swapTo() from the render callback. Add or use a deferred
lifecycle/update path to process the pending switch outside tick, where effect
creation, deletion, setup, and applyState() can safely run.

In `@src/light/effects/RipplesEffect.h`:
- Around line 37-70: Replace the per-light sqrt, sin, and floor calculations in
RipplesEffect::tick with fixed-point or integer lookup data prepared outside the
x/z render loop, while preserving the current ripple geometry, timing, and
y-coordinate output. Ensure the inner loop uses only integer arithmetic and
existing bounded dimensions, with any required wave-table or distance
precomputation performed once during setup or otherwise outside tick().

In `@src/light/layers/Layer.h`:
- Around line 148-192: Defer modifier-triggered rebuilds out of Layer::tick()
instead of calling applyState() from the MM_NONBLOCKING render path. Preserve
the existing coalescing of consumeNeedsRebuild() requests, but queue the rebuild
for the scheduler’s non-render prepare path so applyState(), including
prepare/release, ScratchBuffer resizing, and peripheral access, runs outside
tick().

In `@src/platform/desktop/platform_desktop.cpp`:
- Around line 108-129: Guard the clang diagnostic push, ignored
-Wfunction-effects, and pop directives surrounding millis() and micros() with
`#if` defined(__clang__) and a matching `#endif`, leaving the clock implementations
unchanged for GCC and Clang builds.

In `@test/unit/light/unit_Drivers_rendersplit.cpp`:
- Around line 53-65: Add an in-file comment at SlowDriver::tick explaining that
its mutex and timed wait are intentional to hold the test’s use-after-free
window open, despite the MM_NONBLOCKING annotation. If supported by the
toolchain, narrowly suppress the check_nonblocking.py Tick tick() diagnostic
around this method only, without changing the test synchronization behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ecd018d-7914-481c-8cfe-0d03bf8e5533

📥 Commits

Reviewing files that changed from the base of the PR and between da23a92 and bc02fe4.

📒 Files selected for processing (98)
  • CMakeLists.txt
  • docs/backlog/backlog-core.md
  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • moondeck/MoonDeck.md
  • moondeck/check/check_nonblocking.py
  • moondeck/moondeck_config.json
  • src/core/AudioService.h
  • src/core/DevicesModule.h
  • src/core/FileManagerModule.cpp
  • src/core/FileManagerModule.h
  • src/core/FilesystemModule.cpp
  • src/core/FilesystemModule.h
  • src/core/FirmwareUpdateModule.h
  • src/core/HttpServerModule.cpp
  • src/core/HttpServerModule.h
  • src/core/ImprovProvisioningModule.h
  • src/core/IrService.h
  • src/core/MoonModule.h
  • src/core/MqttModule.cpp
  • src/core/MqttModule.h
  • src/core/NetworkModule.h
  • src/core/PinsModule.h
  • src/core/Scheduler.cpp
  • src/core/Scheduler.h
  • src/core/SystemModule.h
  • src/core/TasksModule.h
  • src/light/drivers/DriverBase.h
  • src/light/drivers/Drivers.h
  • src/light/drivers/HueDriver.h
  • src/light/drivers/LightPresetsModule.h
  • src/light/drivers/NetworkSendDriver.h
  • src/light/drivers/ParallelLedDriver.h
  • src/light/drivers/PreviewDriver.h
  • src/light/drivers/RmtLedDriver.h
  • src/light/effects/AudioSpectrumEffect.h
  • src/light/effects/AudioVolumeEffect.h
  • src/light/effects/BlurzEffect.h
  • src/light/effects/BouncingBallsEffect.h
  • src/light/effects/DemoReelEffect.h
  • src/light/effects/DistortionWavesEffect.h
  • src/light/effects/EffectBase.h
  • src/light/effects/FireEffect.h
  • src/light/effects/FixedRectangleEffect.h
  • src/light/effects/FreqMatrixEffect.h
  • src/light/effects/FreqSawsEffect.h
  • src/light/effects/GEQ3DEffect.h
  • src/light/effects/GEQEffect.h
  • src/light/effects/GameOfLifeEffect.h
  • src/light/effects/LavaLampEffect.h
  • src/light/effects/LinesEffect.h
  • src/light/effects/LissajousEffect.h
  • src/light/effects/MetaballsEffect.h
  • src/light/effects/NetworkReceiveEffect.h
  • src/light/effects/Noise2DEffect.h
  • src/light/effects/NoiseEffect.h
  • src/light/effects/NoiseMeterEffect.h
  • src/light/effects/PaintBrushEffect.h
  • src/light/effects/ParticlesEffect.h
  • src/light/effects/PlasmaEffect.h
  • src/light/effects/PraxisEffect.h
  • src/light/effects/RainbowEffect.h
  • src/light/effects/RandomEffect.h
  • src/light/effects/RingsEffect.h
  • src/light/effects/RipplesEffect.h
  • src/light/effects/RubiksCubeEffect.h
  • src/light/effects/SineEffect.h
  • src/light/effects/SolidEffect.h
  • src/light/effects/SphereMoveEffect.h
  • src/light/effects/SpiralEffect.h
  • src/light/effects/StarFieldEffect.h
  • src/light/effects/StarSkyEffect.h
  • src/light/effects/TetrixEffect.h
  • src/light/effects/TextEffect.h
  • src/light/effects/WaveEffect.h
  • src/light/layers/Layer.h
  • src/light/layers/Layers.h
  • src/light/layouts/LayoutBase.h
  • src/light/layouts/Rings241Layout.h
  • src/light/modifiers/ModifierBase.h
  • src/light/modifiers/RandomMapModifier.h
  • src/light/modifiers/RotateModifier.h
  • src/light/moonlive/MoonLiveEffect.h
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/platform_esp32.cpp
  • src/platform/platform.h
  • test/unit/core/unit_ModuleFactory.cpp
  • test/unit/core/unit_MoonModule_lifecycle.cpp
  • test/unit/core/unit_Services.cpp
  • test/unit/core/unit_SystemModule.cpp
  • test/unit/light/unit_Drivers_container.cpp
  • test/unit/light/unit_Drivers_rendersplit.cpp
  • test/unit/light/unit_Layer_extrude.cpp
  • test/unit/light/unit_Layer_live_modifier.cpp
  • test/unit/light/unit_Layer_persistence.cpp
  • test/unit/light/unit_Layouts_toggle_cycle.cpp
  • test/unit/light/unit_PreviewDriver.cpp

Comment thread moondeck/check/check_nonblocking.py Outdated
Comment thread moondeck/check/check_nonblocking.py Outdated
Comment thread moondeck/check/check_nonblocking.py
Comment thread moondeck/MoonDeck.md Outdated
Comment thread src/core/SystemModule.h
}

void tick1s() override {
void tick1s() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C3 'coprocessorWifi|esp_hosted_get_coprocessor_fwversion' src/core src/platform

Repository: MoonModules/projectMM

Length of output: 6815


🌐 Web query:

ESP-IDF esp_hosted_get_coprocessor_fwversion source esp_hosted.c

💡 Result:

The function esp_hosted_get_coprocessor_fwversion is a part of the ESP-Hosted component in the ESP-IDF ecosystem, used to retrieve the current firmware version of the connected slave coprocessor [1][2]. API Signature The function is defined as: esp_err_t esp_hosted_get_coprocessor_fwversion(esp_hosted_coprocessor_fwver_t *version); [1][2] It populates a structure containing the major, minor, and patch version numbers [1][2]: typedef struct { uint32_t major1; // Major version uint32_t minor1; // Minor version uint32_t patch1; // Patch version } esp_hosted_coprocessor_fwver_t; [1][2] Implementation Details The function typically communicates with the slave device using Remote Procedure Calls (RPC) to fetch the version information synchronously [3]. It is commonly used in OTA (Over-The-Air) update workflows, allowing the host to verify the current slave version before initiating an update [1][2]. Historically, this function was found in files such as esp_hosted_api.c [3], though as the ESP-Hosted codebase has evolved (including significant refactors and code reorganizations), its location may vary depending on the specific version of the ESP-Hosted component being used [3][4]. Users should ensure they are using the latest version of the esp_hosted component for their specific ESP-IDF project [4][5].

Citations:


🌐 Web query:

ESP-IDF esp_wifi_remote SDIO esp_hosted_get_coprocessor_fwversion nonblocking

💡 Result:

The esp_hosted_get_coprocessor_fwversion function, provided by the esp_hosted component, is used to retrieve the firmware version of a connected ESP co-processor [1][2]. It is typically utilized within the context of OTA (Over-the-Air) update workflows to check if an update is necessary by comparing the slave's current version against an available firmware image [1][2]. Based on official documentation and common usage patterns, here are key points regarding your query: Functionality and Blocking Behavior The function esp_hosted_get_coprocessor_fwversion is a synchronous, blocking call [1][2]. It initiates a Remote Procedure Call (RPC) request across the transport bus (such as SDIO) to the co-processor and waits for the response [3][4]. Because it must perform a round-trip communication over the transport interface, it will block the calling task until the version information is returned or a timeout occurs [3][1]. Implementation Context The esp_wifi_remote component relies on the esp_hosted component as its primary backend for transport (e.g., via SDIO or SPI) [3][4][5]. While esp_wifi_remote provides a transparent API for Wi-Fi, the specific function for querying co-processor firmware version belongs to the underlying esp_hosted transport layer [1][2]. Considerations for Non-blocking Needs If you require non-blocking behavior to prevent the calling task from stalling, you should handle the version query in a dedicated task or thread [6][7]. Since esp_hosted is designed for host-slave communication, perform this query after the hosted transport has successfully initialized (esp_hosted_init) [7][2]. Calling it immediately upon boot before the transport is ready may result in failures or exceptions [7]. Summary of Key Dependencies - Transport: The esp_hosted component handles the communication (SDIO/SPI) required for the RPC [8][9]. - API Layer: The esp_wifi_remote component utilizes these underlying transport mechanisms to translate Wi-Fi API calls [3][4]. - Versioning: Ensure the slave firmware uses modern semantic versioning (X.Y.Z), as older versions based on git hashes can cause issues in comparison logic during OTA updates [2][10].

Citations:


🌐 Web query:

ESP-IDF esp_hosted_get_coprocessor_fwversion implementation source

💡 Result:

The function esp_hosted_get_coprocessor_fwversion is part of the ESP-Hosted component in the ESP-IDF ecosystem [1][2]. It is designed to query the firmware version of the connected co-processor [3][4]. Implementation Details: The function is declared to return an esp_err_t and accepts a pointer to an esp_hosted_coprocessor_fwver_t structure, which stores the major, minor, and patch version numbers [3][4]. In the source code, this function typically acts as a wrapper that calls an underlying RPC (Remote Procedure Call) mechanism to communicate with the co-processor [5]. For example, in many versions of the ESP-Hosted codebase, the implementation in the host porting layer is: esp_err_t esp_hosted_get_coprocessor_fwversion(esp_hosted_coprocessor_fwver_t *ver_info) { return rpc_get_coprocessor_fwversion(ver_info); } The rpc_get_coprocessor_fwversion function subsequently handles the synchronous communication required to retrieve the version information from the slave device [5]. Usage: To use this function, you populate a local instance of the version structure and pass its address to the function [6][7]: esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { ESP_LOGI("TAG", "Coprocessor version: %u.%u.%u", ver_info.major1, ver_info.minor1, ver_info.patch1); } You can find the exact source files within the espressif/esp-hosted component repository, typically under the host driver or API source directories, such as host/api/src/esp_hosted_api.c [5].

Citations:


🌐 Web query:

site:github.com/espressif/esp-hosted-mcu "rpc_get_coprocessor_fwversion" "esp_hosted_get_coprocessor_fwversion"

💡 Result:

In the ESP-Hosted framework (specifically esp-hosted-mcu), esp_hosted_get_coprocessor_fwversion and rpc_get_coprocessor_fwversion are related functions used to retrieve firmware version information from the co-processor [1][2]. esp_hosted_get_coprocessor_fwversion serves as a public-facing API function intended for application use [2]. It acts as a wrapper that internally calls rpc_get_coprocessor_fwversion to perform the actual Remote Procedure Call (RPC) to the slave co-processor [1][3]. When called, the function populates an esp_hosted_coprocessor_fwver_t structure with version details such as major, minor, and patch numbers, as well as additional information like revision, build, and chip target details [3]. As of recent updates, the RPC implementation has been extended to include identifiers like chip_id and target name to provide more comprehensive version and hardware identification [3].

Citations:


🌐 Web query:

github esp-hosted-mcu platform_port/esp32 platform_port/esp32/rpc.c rpc_get_coprocessor_fwversion function source

💡 Result:

In the esp-hosted-mcu repository, the rpc_get_coprocessor_fwversion function is primarily defined and declared within the RPC wrapper layer, rather than a specific platform_port/esp32/rpc.c file [1]. Based on the repository structure, the function is declared in host/drivers/rpc/wrap/rpc_wrap.h [2][1]: esp_err_t rpc_get_coprocessor_fwversion(esp_hosted_coprocessor_fwver_t *ver_info); The implementation is located in host/drivers/rpc/wrap/rpc_wrap.c [1][3], which handles the synchronous execution of the RPC call [1]. This function facilitates communication with the co-processor to retrieve firmware version information, identified in the protobuf definition as Req_GetCoprocessorFwVersion (ID 350) [4][5]. Historically, the project underwent code reorganization [1], and while there are platform-specific ports (e.g., host/port/), the core RPC logic resides in the host/drivers/rpc/ directory [6]. You can track the implementation details and recent changes in host/drivers/rpc/wrap/rpc_wrap.c [3].

Citations:


🌐 Web query:

raw file host/drivers/rpc/wrap/rpc_wrap.c esp_hosted_get_coprocessor_fwversion rpc_call

💡 Result:

The function esp_hosted_get_coprocessor_fwversion is a component of the ESP-Hosted framework [1][2]. It is used to retrieve the currently running firmware version of the coprocessor (slave) device [2][3]. In the ESP-Hosted architecture, this function typically acts as a wrapper that calls an underlying Remote Procedure Call (RPC) function, such as rpc_get_coprocessor_fwversion, to communicate with the coprocessor [4]. The retrieved version information is stored in an esp_hosted_coprocessor_fwver_t structure, which contains the following fields: typedef struct { uint32_t major1; // Major version uint32_t minor1; // Minor version uint32_t patch1; // Patch version } esp_hosted_coprocessor_fwver_t; This function is commonly used in Over-the-Air (OTA) update workflows to check the current slave firmware version and determine whether an update is necessary [1][3]. If the versions match, the host can skip the OTA process [1][2]. While the file host/drivers/rpc/wrap/rpc_wrap.c is a central file in the ESP-Hosted repository where various RPC wrappers are defined and managed [5][4], the specific function signature and implementation details are provided by the ESP-Hosted component libraries [2][3].

Citations:


🌐 Web query:

"GetCoprocessorFwVersion" "#define RPC_ID" "sync" "esp_hosted"

💡 Result:

In the context of the ESP-Hosted framework, GetCoprocessorFwVersion is a defined remote procedure call (RPC) used by the host to query the firmware version of the slave co-processor [1][2][3]. Key details: - RPC ID: The RPC identifier for Req_GetCoprocessorFwVersion is 350 (0x15e) [1][2]. - Functionality: The request allows the host to retrieve the slave's firmware version, which is useful for tasks such as OTA (Over-the-Air) update validation to avoid redundant updates [3]. - Recent Updates: As of version 3.0.5, the RPC was extended to include additional information, specifically the co-processor name and chip ID [4]. - Implementation: This RPC is defined in the project's protocol buffer files (esp_hosted_rpc.proto) [1] and implemented within the ESP-Hosted C/C++ driver component as esp_hosted_get_coprocessor_fwversion [3]. The framework uses this RPC to ensure the host and slave are synchronized regarding firmware status, enabling efficient system management and update procedures [3][5][4].

Citations:


🌐 Web query:

"function esp_hosted_call_rpc" esp-hosted source esp_hosted_call_rpc

💡 Result:

In the ESP-Hosted framework, there is no public API function explicitly named esp_hosted_call_rpc. The framework relies on a protobuf-based Remote Procedure Call (RPC) mechanism to allow a Host system (Linux or MCU) to offload Wi-Fi and Bluetooth tasks to an ESP co-processor [1][2]. The ESP-Hosted architecture implements RPCs using the following structure: 1. Protocol Definition: RPC messages, request types, and event IDs are defined in common/proto/esp_hosted_rpc.proto [3]. 2. Request Handling: Host-side applications typically use high-level library functions (e.g., wifi_connect_ap, wifi_get_mac) provided by the ESP-Hosted control library rather than calling a raw "call_rpc" function [4][1][5]. 3. Underlying Mechanism: These high-level functions internally handle the serialization of requests into protobuf messages, which are then transmitted over the communication bus (SPI, SDIO, or UART) to the co-processor [1]. 4. Custom RPCs: For user-defined functionality, developers can utilize the Custom RPC feature (specifically Req_CustomRpc with ID 388) [3][6]. On the host side, custom behavior is registered using functions such as esp_hosted_register_custom_callback [7]. If you are encountering a reference to esp_hosted_call_rpc in your source code, it is likely a custom wrapper, a deprecated function, or a misunderstanding of the internal API naming, as the official ESP-Hosted implementation manages RPC communication via its control library abstraction layers [4][1].

Citations:


🌐 Web query:

"glibc.h" "esp_hosted_get_coprocessor_fwversion" "esp_hosted_api.c"

💡 Result:

The function esp_hosted_get_coprocessor_fwversion is an API provided by the ESP-Hosted framework [1][2]. It is used to query the firmware version currently running on the ESP-based co-processor [1][3]. Technical details regarding your query components: 1. esp_hosted_get_coprocessor_fwversion: This function retrieves the semantic version (major, minor, and patch) of the co-processor firmware [1][2]. It is typically used in OTA (Over-the-Air) update workflows to compare the running version with a target version to determine if an update is required [1][2]. It is defined in the ESP-Hosted API headers and implemented within the project's source files [1][4]. 2. esp_hosted_api.c: This is a core source file in the ESP-Hosted framework that implements various host-side API functions, including those for communication, transport configuration, and firmware management [4][5]. It serves as part of the interface between the host application and the ESP co-processor [6][7]. 3. glibc.h: There is no direct functional link between glibc.h and the ESP-Hosted API functions in the context of standard usage. While Espressif maintains a repository for the GNU C Library (glibc) [8], it is independent of the ESP-Hosted framework's control plane APIs. If you encountered a reference to glibc.h in your specific development environment, it is likely an incidental header included by the build system or the host operating system's standard library, rather than a component of the ESP-Hosted solution itself [4][9]. In summary, esp_hosted_get_coprocessor_fwversion is a standard function found in the ESP-Hosted API (often implemented in or linked with esp_hosted_api.c) used for system diagnostics and firmware lifecycle management [1][10][2].

Citations:


Make coprocessorWifi() nonblocking or cache it from another task.

tick1s() is MM_NONBLOCKING, but the P4 implementation calls esp_hosted_get_coprocessor_fwversion() once per second, which is a synchronous RPC/transport query. Cache the version in a non-render task or provide a nonblocking snapshot so this hot-path override does not become a timed RPC wait.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/SystemModule.h` at line 232, Update the P4 implementation of
tick1s() so its coprocessorWifi() path does not directly invoke the synchronous
esp_hosted_get_coprocessor_fwversion() query; instead, maintain the firmware
version via a non-render task or nonblocking cached snapshot and have tick1s()
read that cached value while preserving its MM_NONBLOCKING contract.

/// spatial resolution via adaptive downscale), so a large grid never stalls the
/// loop or tears — it always delivers a complete frame.
void tick() override {
void tick() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Do not declare PreviewDriver::tick() nonblocking while blocking I/O and allocation remain reachable.

tick() can call sendFrame() through the documented synchronous begin/push/end socket path, and buildAndSendCoordTable() can allocate keptIdx_. Move those operations off the render path or make every reachable path nonblocking before applying this contract.

As per coding guidelines, render-loop callees must avoid blocking operations and heap allocation; as per path instructions, light-driver loop methods must not allocate on the hot path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/PreviewDriver.h` at line 132, Remove the MM_NONBLOCKING
contract from PreviewDriver::tick() until its reachable work is safe, or
refactor sendFrame()’s synchronous socket path and buildAndSendCoordTable()’s
keptIdx_ allocation off the render path; ensure every operation reachable from
tick() avoids blocking I/O and heap allocation before restoring the annotation.

Sources: Coding guidelines, Path instructions

}

void tick() override {
void tick() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Remove per-element floating-point work from these nonblocking render callbacks.

  • src/light/effects/BouncingBallsEffect.h#L49-L49: replace per-ball trajectory, damping, square-root, and rounding calculations with fixed-point/integer math or move them off the render path.
  • src/light/effects/SphereMoveEffect.h#L36-L36: replace per-voxel trigonometric/distance calculations with integer math or move them off the render path.

As per coding guidelines, render-loop callees must avoid per-light floating-point math.

📍 Affects 2 files
  • src/light/effects/BouncingBallsEffect.h#L49-L49 (this comment)
  • src/light/effects/SphereMoveEffect.h#L36-L36
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/effects/BouncingBallsEffect.h` at line 49, The nonblocking render
callbacks perform per-element floating-point work; update
BouncingBallsEffect::tick in src/light/effects/BouncingBallsEffect.h:49-49 to
use fixed-point/integer math or precompute trajectory, damping, square-root, and
rounding values off the render path, and update SphereMoveEffect::tick in
src/light/effects/SphereMoveEffect.h:36-36 to replace per-voxel trigonometric
and distance calculations with integer math or precomputed values.

Source: Coding guidelines

Comment thread src/platform/esp32/platform_esp32.cpp
Comment thread src/platform/platform.h Outdated
…otations

Unblocks CI: the clang-only pragma added with the hot-path check failed the
GCC sanitizer lanes. Also makes three cross-task connection flags atomic — a
real data race, independent of the hot-path work — and removes two
MM_NONBLOCKING annotations that were asserting something false. MoonDeck cards
gain a 📄 button that replays the script's last run.

KPI: 16384lights | Desktop:856KB | ESP32:1499KB | tick:4215us(FPS:237) | heap:8294KB | lizard:136w

**Core**
- platform_esp32: ethLinkUp_, ethConnected_ and wifiStaConnected_ are written
  by the IDF event loop and read from the render task every tick. Plain bools
  there are a data race — benign in practice on this target, but UB the
  compiler may fold or reorder, and TSan reports it. Now std::atomic<bool> with
  relaxed ordering: each is an independent state signal, nothing else is
  published through them.
- platform_desktop: `#pragma clang diagnostic` is an UNKNOWN PRAGMA to GCC, and
  the CI sanitizer lanes build with GCC + -Werror. Guarded with #ifdef
  __clang__. Verified against the real ESP32 GCC: guarded compiles clean,
  unguarded reproduces the CI error exactly.

**Light domain**
- ParallelLedDriver::tick and PreviewDriver::tick keep MM_NONBLOCKING (it is
  inherited from the base and cannot be dropped without breaking the override)
  but suppress -Wfunction-effects at the site, with the reason. Both genuinely
  block: busWaitIfBusy() spins for the DMA peripheral; sendFrame() writes a
  socket and buildAndSendCoordTable() resizes keptIdx_. Suppressing where the
  code blocks is honest; letting the annotation claim otherwise was not.
  tick() findings: 68 → 56.
- LedPeripheral::lanesAvailable / busIsRing annotated through their overrides
  and test doubles — const accessors, no allocation.

**Scripts/MoonDeck**
- Every run is teed to build/moondeck-logs/<id>.log AS IT STREAMS, not buffered
  to the end, so a run stopped mid-way still leaves what it printed. A 📄
  button per card replays it. Own button rather than a click on the status dot:
  a dot is a status indicator, and making it secretly clickable gave a new
  reader no way to guess the feature existed.
- check_nonblocking: a FAILED build now errors instead of returning partial
  output that would read as "nearly clean" — the silent-zero trap again.
  check_clang_tidy imported directly rather than through another module's
  attribute. NO_CAUSE named, since a backslash escape inside an f-string
  expression is a syntax error before Python 3.12.

**Tests**
- unit_Drivers_rendersplit: SlowDriver::tick's mutex and timed wait ARE the
  test — they hold the use-after-free window open. Documented so the
  clang-hotpath report on those lines reads as expected, not as a defect.

**Docs/CI**
- Backlogged: move blocking work off the render callbacks. 17 sites named with
  what needs moving where — synchronous HTTP in HueDriver, inline transport in
  HttpServerModule, applyState() from Layer::tick, effect lifecycle from
  DemoReelEffect::tick. Each is a design change, not a lint fix.
- Explicitly NOT backlogged: the float-math findings. Review suggested
  fixed-point, but the float trajectory IS the ported MoonLight behaviour and
  fidelity is deliberate; if the FPU-less Xtensa cost is real that is a
  profiling question first.
- "three orders of magnitude" → "roughly two" in three places: 50/s vs 1/s.

**Reviews**
- 🐇 CodeRabbit (PR #56): pragma guard, build-failure guard, direct import,
  f-string escape, plan wording, magnitude overstatement, test-double comment
  → all done. Atomics and the two annotation removals → done. The architecture
  findings → backlogged with their specifics. Float-math findings → declined
  with the fidelity reason above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
moondeck/moondeck.py (1)

1794-1812: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent concurrent runs from corrupting the “last run” log.

A fast rerun can leave the previous stream handler finishing after _running[script_id] has been replaced. Both handlers open the same script_id.log with "w" and then write to it, so the stored result can be truncated, interleaved, or overwritten by the older run. Use a per-run generation/file and publish only the current run atomically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/moondeck.py` around lines 1794 - 1812, Update the stream-handling
logic around the per-script log creation and writing loop to use a unique
generation-specific log file for each run instead of opening the shared
script_id.log with "w". Track the active run generation associated with
_running[script_id], and only atomically publish that run’s completed log as the
current script_id.log when it is still current; older handlers must never
overwrite or truncate the current result.
docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md (2)

68-80: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use American English spelling in the plan.

Replace analyser with analyzer in this changed documentation block.

As per coding guidelines, **/*.md uses American English spelling throughout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md around lines 68 - 80, Update the changed documentation heading and
prose around “lizard” to use the American English spelling “analyzer” instead of
“analyser,” leaving the rest of the plan unchanged.

Source: Coding guidelines


136-138: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the GCC description of MM_NONBLOCKING.

The macro expands to noexcept on non-Clang builds; only the Clang attribute and function-effects warning are unavailable.

  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md#L136-L138: replace “macro (empty on GCC)” with wording that states the noexcept expansion.
  • moondeck/MoonDeck.md#L359-L361: replace “MM_NONBLOCKING is empty on GCC” with the same accurate contract.

As per coding guidelines, documentation pages remain synchronized with the current code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md around lines 136 - 138, The documentation incorrectly describes
MM_NONBLOCKING as empty on GCC. Update docs/history/plans/Plan-20260727 - Static
analysis and repo health tooling.md lines 136-138 and moondeck/MoonDeck.md lines
359-361 to state that MM_NONBLOCKING expands to noexcept on non-Clang builds,
while only the Clang attribute and function-effects warning are unavailable;
keep both documentation pages synchronized.

Source: Coding guidelines

docs/backlog/backlog-core.md (1)

393-415: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep the hot-path inventory canonical across documentation.

The same 181-finding counts and hotspot narrative are copied into three documents, so future triage will require synchronized edits.

  • docs/backlog/backlog-core.md#L393-L415: keep this as the canonical triage inventory.
  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md#L127-L161: retain historical rationale but link to the canonical inventory.
  • moondeck/MoonDeck.md#L347-L366: document checker semantics and link to the current inventory instead of embedding counts.

As per path instructions, docs/**/*.md should avoid duplicating facts and keep each fact in one canonical location.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/backlog/backlog-core.md` around lines 393 - 415, Keep
docs/backlog/backlog-core.md lines 393-415 as the canonical hot-path triage
inventory. In docs/history/plans/Plan-20260727 - Static analysis and repo health
tooling.md lines 127-161, retain the historical rationale but replace duplicated
counts and narrative with a link to the canonical inventory. In
moondeck/MoonDeck.md lines 347-366, describe checker semantics and link to the
current inventory instead of embedding counts; do not duplicate these facts
across documents.

Source: Path instructions

src/platform/esp32/platform_esp32.cpp (1)

404-426: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Publish the static-IP snapshot atomically, not just the mode flag.

ethStaticIp_, ethStaticGw_, and the other byte arrays are still plain shared storage. The render task writes them while the Ethernet event task reads them during reconnect; ethStatic_.load(acquire) only orders the transition to true, not later edits while the flag remains true. A concurrent live address change can therefore apply a torn or mixed configuration. Use a coherent atomic snapshot, lock, or versioned double-buffer, and apply the same fix to the STA arrays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/esp32/platform_esp32.cpp` around lines 404 - 426, Make the
Ethernet static-address configuration in ethStaticIp_, ethStaticGw_,
ethStaticMask_, and ethStaticDns_ a coherent cross-thread snapshot instead of
relying only on ethStatic_ atomic ordering. Protect concurrent writes from
netSetStaticIPv4/syncAddressingLive and reads during the Ethernet reconnect
handler with an appropriate lock or versioned double-buffer, and apply the same
synchronization approach to the corresponding STA address arrays.
src/light/drivers/ParallelLedDriver.h (1)

704-710: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Close the nonblocking path on ParallelLedDriver::tick1s().

MM_NONBLOCKING is a compiler-enforced annotation, but the peripheral KPI call chain from ParallelLedDriver::tick1s() is not: LedPeripheral::busLastTransmitUs() and LedPeripheral::refreshBusKpi() still lack MM_NONBLOCKING, and the MoonI80 override of refreshBusKpi() reads platform::moonI80Ws2812RingStats(...) before updating the diagnostic string. Annotate and enforce the full nonblocking chain, or drop the claim from this override.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/ParallelLedDriver.h` around lines 704 - 710, Make the
entire ParallelLedDriver::tick1s() KPI call chain nonblocking: annotate
LedPeripheral::busLastTransmitUs(), LedPeripheral::refreshBusKpi(), and the
MoonI80 refreshBusKpi() override with MM_NONBLOCKING, and ensure the
platform::moonI80Ws2812RingStats() path satisfies that constraint. If these
calls cannot be made nonblocking, remove MM_NONBLOCKING from
ParallelLedDriver::tick1s() instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@moondeck/check/check_nonblocking.py`:
- Around line 90-98: Update the subprocess.run call in the build-check flow to
pass check=False, since the code intentionally handles proc.returncode. Rename
the errs comprehension variable l to error_line and update its use in the
subsequent print loop.
- Around line 82-100: Update build_output() to fail closed after a successful
build unless its diagnostics contain [-Wfunction-effects], or validate through
the existing build configuration that the selected compiler satisfies the Clang
20+ requirement. Return None and report an incomplete/unsupported diagnostics
configuration when the contract is not met; preserve existing failed-build
handling and successful output behavior when the warning is present.

In `@moondeck/moondeck_ui/app.js`:
- Around line 493-512: Update the .log-btn click handler to clear logEl before
initiating fetch, then serialize log loads using a per-request token or
AbortController so stale responses cannot overwrite the currently selected
script’s log. Ensure failed requests append errors to the cleared log rather
than prior content, and preserve the existing no-stored-run and successful log
behavior.
- Line 380: Add aria-label="Show this script's last run" to the log button while
preserving the existing title attribute and button behavior.

In `@moondeck/moondeck.py`:
- Line 2053: Add the missing type annotations to _serve_log: annotate script_id
as str and declare the method return type as None, preserving its existing
behavior.
- Around line 1816-1829: Update the SSE process cleanup around proc.wait(), the
BrokenPipeError/ConnectionResetError handler, and the finally block so a client
disconnect still terminates or otherwise drains the child, waits for it, records
its exit code, and finalizes the log before closing it and removing the process
from _running.
- Around line 1791-1799: Bound persisted script logs and endpoint reads: update
the streaming log handling around LOG_DIR, log_path, and log so the file is
capped or rolled over at a fixed byte budget, and update the serving code at
moondeck/moondeck.py lines 2063-2072 to replace unbounded read_bytes() with a
bounded tail read. Apply the required change at both sites in
moondeck/moondeck.py; the endpoint must return only the configured maximum log
bytes.
- Around line 1794-1799: Make log persistence best-effort in the SSE handler:
wrap LOG_DIR.mkdir, log_path.open/write, and subsequent flush operations in
OSError handling. On any filesystem failure, disable or discard the log and
continue streaming process output without propagating the exception.
- Line 1799: Update the timestamp formatting in the log-writing code around
script_id to use a timezone-aware datetime, such as the local time from
datetime.now().astimezone() serialized with isoformat(), or UTC with its offset
included. Preserve the existing log entry structure while ensuring the timestamp
contains explicit timezone information.

In `@src/light/drivers/ParallelLedDriver.h`:
- Around line 552-561: Remove the MM_NONBLOCKING annotation from
ParallelLedDriver::tick(), along with the surrounding clang diagnostic
suppression for -Wfunction-effects. Preserve the existing tickSync()/tickRing()
behavior and deliberate busWaitIfBusy() handling; do not claim nonblocking
behavior unless the wait is moved off the render path or replaced with an
immediate busy return.

In `@src/platform/esp32/platform_esp32.cpp`:
- Line 1370: Make wifiStaAssociated_ an atomic flag and update its accesses,
including the read in the WiFi event handler near wifiStaConnected_. Use relaxed
memory-order loads and stores consistently so the event handler and
netSetStaticIPv4() can safely exchange the association state.

In `@test/unit/light/unit_Drivers_rendersplit.cpp`:
- Around line 53-58: Scope the intentional blocking SlowDriver::tick() fixture
out of the nonblocking error gate by adding a narrow Clang function-effects
diagnostic suppression around its override, preserving the existing
justification comments and keeping the suppression limited to this test double.

---

Outside diff comments:
In `@docs/backlog/backlog-core.md`:
- Around line 393-415: Keep docs/backlog/backlog-core.md lines 393-415 as the
canonical hot-path triage inventory. In docs/history/plans/Plan-20260727 -
Static analysis and repo health tooling.md lines 127-161, retain the historical
rationale but replace duplicated counts and narrative with a link to the
canonical inventory. In moondeck/MoonDeck.md lines 347-366, describe checker
semantics and link to the current inventory instead of embedding counts; do not
duplicate these facts across documents.

In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md:
- Around line 68-80: Update the changed documentation heading and prose around
“lizard” to use the American English spelling “analyzer” instead of “analyser,”
leaving the rest of the plan unchanged.
- Around line 136-138: The documentation incorrectly describes MM_NONBLOCKING as
empty on GCC. Update docs/history/plans/Plan-20260727 - Static analysis and repo
health tooling.md lines 136-138 and moondeck/MoonDeck.md lines 359-361 to state
that MM_NONBLOCKING expands to noexcept on non-Clang builds, while only the
Clang attribute and function-effects warning are unavailable; keep both
documentation pages synchronized.

In `@moondeck/moondeck.py`:
- Around line 1794-1812: Update the stream-handling logic around the per-script
log creation and writing loop to use a unique generation-specific log file for
each run instead of opening the shared script_id.log with "w". Track the active
run generation associated with _running[script_id], and only atomically publish
that run’s completed log as the current script_id.log when it is still current;
older handlers must never overwrite or truncate the current result.

In `@src/light/drivers/ParallelLedDriver.h`:
- Around line 704-710: Make the entire ParallelLedDriver::tick1s() KPI call
chain nonblocking: annotate LedPeripheral::busLastTransmitUs(),
LedPeripheral::refreshBusKpi(), and the MoonI80 refreshBusKpi() override with
MM_NONBLOCKING, and ensure the platform::moonI80Ws2812RingStats() path satisfies
that constraint. If these calls cannot be made nonblocking, remove
MM_NONBLOCKING from ParallelLedDriver::tick1s() instead.

In `@src/platform/esp32/platform_esp32.cpp`:
- Around line 404-426: Make the Ethernet static-address configuration in
ethStaticIp_, ethStaticGw_, ethStaticMask_, and ethStaticDns_ a coherent
cross-thread snapshot instead of relying only on ethStatic_ atomic ordering.
Protect concurrent writes from netSetStaticIPv4/syncAddressingLive and reads
during the Ethernet reconnect handler with an appropriate lock or versioned
double-buffer, and apply the same synchronization approach to the corresponding
STA address arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9dc4efb0-2e05-45bd-bdd6-b1d38590c446

📥 Commits

Reviewing files that changed from the base of the PR and between bc02fe4 and 5f2116d.

📒 Files selected for processing (22)
  • docs/backlog/backlog-core.md
  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • moondeck/MoonDeck.md
  • moondeck/check/check_nonblocking.py
  • moondeck/moondeck.py
  • moondeck/moondeck_ui/app.js
  • moondeck/moondeck_ui/style.css
  • src/light/drivers/LedPeripheral.h
  • src/light/drivers/MoonLedDriver.h
  • src/light/drivers/MultiPinLedDriver.h
  • src/light/drivers/ParallelLedDriver.h
  • src/light/drivers/ParlioLedDriver.h
  • src/light/drivers/PreviewDriver.h
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/platform_esp32.cpp
  • test/unit/light/unit_Drivers_rendersplit.cpp
  • test/unit/light/unit_ParallelLedDriver_doublebuffer.cpp
  • test/unit/light/unit_ParallelLedDriver_pinexpander.cpp
  • test/unit/light/unit_ParallelLedDriver_ring.cpp
  • test/unit/light/unit_ParallelLedDriver_swap.cpp

Comment thread moondeck/check/check_nonblocking.py Outdated
Comment thread moondeck/check/check_nonblocking.py Outdated
Comment thread moondeck/moondeck_ui/app.js Outdated
Comment thread moondeck/moondeck_ui/app.js
Comment thread moondeck/moondeck.py Outdated
Comment thread moondeck/moondeck.py
Comment on lines 1816 to +1829
proc.wait()
exit_msg = f"[exit code: {proc.returncode}]"
if log:
log.write(exit_msg + "\n")
self.wfile.write(f"data: {json.dumps(exit_msg)}\n\n".encode())
done_data = json.dumps({"exitCode": proc.returncode})
self.wfile.write(f"event: done\ndata: {done_data}\n\n".encode())
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
pass
finally:
if log:
with suppress(OSError):
log.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Reap the child and finalize the log after client disconnects.

When the SSE client disconnects, the exception path skips proc.wait() and the exit-code record, then removes the process from _running. The child can continue without a Stop handle, and the log remains incomplete. Continue draining/logging after disconnect, or terminate and reap the child before closing the log and unregistering it.

🧰 Tools
🪛 ast-grep (0.44.1)

[info] 1819-1819: use jsonify instead of json.dumps for JSON output
Context: json.dumps(exit_msg)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1820-1820: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"exitCode": proc.returncode})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/moondeck.py` around lines 1816 - 1829, Update the SSE process
cleanup around proc.wait(), the BrokenPipeError/ConnectionResetError handler,
and the finally block so a client disconnect still terminates or otherwise
drains the child, waits for it, records its exit code, and finalizes the log
before closing it and removing the process from _running.

Comment thread moondeck/moondeck.py Outdated
Comment thread src/light/drivers/ParallelLedDriver.h Outdated
Comment on lines +552 to +561
// NOT nonblocking, and the annotation would assert otherwise: tickSync()/tickRing() reach
// busWaitIfBusy(), which spins until the DMA peripheral is free. That wait is deliberate —
// the driver owns the bus for the frame — but it IS blocking, so the check is suppressed
// here rather than the code claiming a property it does not have. Moving the wait off the
// render path is backlogged (backlog-core: hot path).
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wfunction-effects"
#endif
void tick() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)ParallelLedDriver\.h$|ParallelLedDriver\.(h|hpp|cpp)$|platform_config\.h|function-effects|function-effects|NONBLOCKING' || true

echo "== target lines =="
cat -n src/light/drivers/ParallelLedDriver.h | sed -n '520,575p'

echo "== nearby blocking/wait definitions/usages =="
rg -n "busWaitIfBusy|MM_NONBLOCKING|function-effects|tickSync|tickRing|tick1s" src/light src/platform || true

echo "== helper definitions outline =="
rg -n "class .*Driver|struct .*Driver|ParallelLedDriver|busWaitIfBusy|tickSync|tickRing" src/light/drivers -g '*.{h,hpp,cpp,cc,h}' || true

Repository: MoonModules/projectMM

Length of output: 28253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target lines =="
cat -n src/light/drivers/ParallelLedDriver.h | sed -n '520,575p'

echo "== nearby blocking/wait definitions/usages =="
rg -n "busWaitIfBusy|MM_NONBLOCKING|function-effects|tickSync|tickRing|tick1s" src/light src/platform || true

Repository: MoonModules/projectMM

Length of output: 19885


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ParallelLedDriver tick/render wait sections =="
cat -n src/light/drivers/ParallelLedDriver.h | sed -n '586,755p'
cat -n src/light/drivers/ParallelLedDriver.h | sed -n '755,850p'

echo "== all MM_NONBLOCKING callers/annotations around wait helpers =="
python3 - <<'PY'
import re, pathlib
p = pathlib.Path('src/light/drivers/ParallelLedDriver.h')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
    if re.search(r'busWaitIfBusy|tickSync|tickRing|MM_NONBLOCKING|pragma clang diagnostic ignored "-Wfunction-effects"', line):
        print(f"{i}: {line}")
PY

Repository: MoonModules/projectMM

Length of output: 23278


Remove MM_NONBLOCKING from ParallelLedDriver::tick() or make the wait nonblocking.

tick() is annotated MM_NONBLOCKING, but both per-frame paths reach busWaitIfBusy(), which waits on the render thread for up to a 20–100 ms bus timeout (notably in tickSync()). This violates the hot-path contract and the clang nonblocking static checking; move the wait off the render loop or return when the DMA is busy instead of suppressing -Wfunction-effects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/ParallelLedDriver.h` around lines 552 - 561, Remove the
MM_NONBLOCKING annotation from ParallelLedDriver::tick(), along with the
surrounding clang diagnostic suppression for -Wfunction-effects. Preserve the
existing tickSync()/tickRing() behavior and deliberate busWaitIfBusy() handling;
do not claim nonblocking behavior unless the wait is moved off the render path
or replaced with an immediate busy return.

Sources: Coding guidelines, Path instructions

Comment thread src/platform/esp32/platform_esp32.cpp Outdated
Comment on lines +53 to +58
// DELIBERATELY blocking, despite MM_NONBLOCKING: the mutex and timed wait ARE the test.
// They hold the use-after-free window open on demand, which is the only way to observe
// the race this file pins. The annotation is inherited from MoonModule::tick and cannot
// be dropped without breaking the override, so clang-hotpath will report these lines —
// that report is correct and expected here.
void tick() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Scope the intentional blocking fixture out of the nonblocking error gate.

The comment explains why SlowDriver::tick() blocks, but it does not suppress -Wfunction-effects. When the planned -Wno-error=function-effects exemption is removed, this deliberate test double will become a hard build failure. Add a narrow Clang diagnostic suppression with the same justification, or explicitly exclude this fixture from the hot-path gate.

🧰 Tools
🪛 Cppcheck (2.21.0)

[style] 53-53: The function 'atan2_8' is never used.

(unusedFunction)


[style] 56-56: The function 'userEditable' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/light/unit_Drivers_rendersplit.cpp` around lines 53 - 58, Scope the
intentional blocking SlowDriver::tick() fixture out of the nonblocking error
gate by adding a narrow Clang function-effects diagnostic suppression around its
override, preserving the existing justification comments and keeping the
suppression limited to this test double.

MM_NONBLOCKING now has a frozen baseline, so the report says what is NEW rather
than re-listing 107 known blocking calls. check_hotpath.py (170 lines of regex
that could not see past a tick body) is deleted and its gate replaced by the
compiler check running incrementally in 0.7s. RealtimeSanitizer joins the
sanitizer matrix as the runtime half of the same attribute.

KPI: 16384lights | Desktop:856KB | ESP32:1499KB | tick:4164us(FPS:240) | heap:8294KB | lizard:136w

**The reports report; consumers decide.**
`-Wno-error=function-effects` stays and the gate never fails the event. Failing
on ~50 known findings would block every build until the architecture work lands,
and a gate nobody can satisfy gets disabled rather than obeyed. A NEW blocking
call may also be entirely legitimate — a driver that must wait for hardware — so
forcing a failure pushes people to suppress it under time pressure, which is
what this exists to prevent. The report states the finding and marks it NEW.

**Core**
- platform_esp32: wifiStaAssociated_ made atomic. Same cross-task pattern as the
  three flags in the previous commit — written by the IDF event handler, read by
  netSetStaticIPv4 — and missed there.
- AudioService::kSilence is constexpr, not a function-local static: a static
  needs a guard variable and a one-time lock on first use, and this is reached
  from eight audio-reactive effects' tick(). One fix, eight findings.
- platform.h: MM_NONBLOCKING's comment corrected — on GCC it expands to
  `noexcept`, not to nothing. Only the clang attribute and the warning are absent.

**Light domain**
- ParallelLedDriver::tick and PreviewDriver::tick no longer suppress their
  findings. They genuinely block (a DMA wait; a socket write plus a resize) and
  the report exists to show that. Hiding the two worst offenders made the number
  look better and the report worthless.
- LavaLamp/Metaballs constant tables hoisted to class scope: -Wfunction-effects
  flags ANY static local in a nonblocking function, including a constexpr that
  needs no guard.
- nsToTicks, rmtWs2812Resolution, ethGetIPv4 annotated — stored-value reads and
  integer math.

**Scripts/MoonDeck**
- docs/metrics/hotpath-baseline.txt: 107 (file, callee) pairs frozen. Keyed on
  file+callee, not line, so an entry survives edits above it.
- check_nonblocking --incremental (~0.7s) replaces check_hotpath.py in the
  pre-commit gate: it rebuilds only what the commit touched, answering "did this
  add a blocking call" rather than re-listing the baseline. Verified by adding a
  static local to RainbowEffect::tick — reported 1 NEW — then reverting.
- FAIL CLOSED on a missing warning: -Wfunction-effects exists only on Clang 20+
  and CMake omits it silently otherwise, so "0 findings" would be
  indistinguishable from a clean tree. The script now says so instead.
- Log persistence hardened: capped at 1 MB (an ESP32 build prints megabytes),
  best-effort writes so a full disk cannot kill the stream, timezone-aware
  stamps, bounded reads. The 📄 button shows only on cards that have actually
  run, so its absence marks what nobody has used yet.
- .clang-tidy disables clang-diagnostic-function-effects: with MM_NONBLOCKING in
  the source it double-reported all 165 hot-path findings, burying clang-tidy's
  own 47. One rule, one owner — the clang-hotpath card owns that rule.

**Docs/CI**
- test.yml: `realtime` added to the sanitizer matrix. The lane pins clang (GCC
  rejects -fsanitize=realtime and ubuntu-latest defaults to GCC) and sets
  RTSAN_OPTIONS=halt_on_error=0 for the same report-don't-gate reason.
- Plan steps 3 and 4 marked done, with what the estimates got wrong.
- coding-standards: the hot-path lint entry replaced by the compiler check.
- MoonDeck.md § Tools: "a report shows the real situation" written down — a
  finding is fixed, or shown with its reason, never suppressed to tidy output.

**Reviews**
- 🐇 CodeRabbit (PR #56, second round): wifiStaAssociated_ atomic, log size
  bound, best-effort writes, tz-aware timestamps, bounded reads, type
  annotations, check=False, aria-label, serialized log loads, fail-closed
  contract, GCC-expansion doc fix, analyser→analyzer → all done. Declined with
  reasons: removing MM_NONBLOCKING from the two blocking ticks and suppressing
  the SlowDriver fixture (both contradict "reports show the real situation");
  per-run generation log files (over-engineering for a last-run record).
  Deferred: SSE child reaping, Ethernet static-IP snapshot locking, doc
  de-duplication.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/platform/esp32/platform_esp32.cpp (1)

1115-1135: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset wifiStaAssociated_ during wifiStaStop().

Line 1133 clears only wifiStaConnected_. If the queued STA_DISCONNECTED event is processed after handlers are unregistered, wifiStaAssociated_ can remain true. A subsequent static STA setup then passes the Line 1372 association check and publishes connected state before L2 association. Store false during teardown, alongside the connected flag.

As per path instructions, platform state shared between event and scheduler tasks must remain synchronized across lifecycle transitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/esp32/platform_esp32.cpp` around lines 1115 - 1135, Update
wifiStaStop() to also atomically store false in wifiStaAssociated_ during
teardown, alongside wifiStaConnected_. Ensure both association and connection
state are reset before a later wifiStaInit() or static STA setup.

Source: Path instructions

src/platform/platform.h (1)

341-351: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Make the IP getters genuinely nonblocking across platforms.

The new contract is inconsistent: the WiFi fallback remains outside MM_NONBLOCKING, while the desktop Ethernet implementation performs synchronous route/socket work. A tick caller can therefore either escape compiler checking or block inside a function explicitly promised nonblocking.

  • src/platform/platform.h#L341-L351: apply one consistent contract to both IPv4 getters, backed by cached/lock-free state.
  • src/platform/desktop/platform_desktop.cpp#L658-L677: remove hostIp() route resolution from the getter and return cached octets.

As per path instructions, platform implementations must match the nonblocking API contract declared in platform.h.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/platform.h` around lines 341 - 351, Make both IPv4 getters
genuinely nonblocking: in src/platform/platform.h lines 341-351, declare
wifiStaGetIPv4 with the same MM_NONBLOCKING contract as ethGetIPv4; in
src/platform/desktop/platform_desktop.cpp lines 658-677, update the getter
implementation to return cached octets and remove synchronous hostIp() route
resolution, ensuring the cache is maintained through the existing platform
connection/update flow.

Source: Path instructions

moondeck/MoonDeck.md (1)

356-361: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the ESP32 hot-path coverage claim.

src/platform/esp32/ builds are excluded from the desktop Clang build, so -Wfunction-effects only sees tick call edges and MM_NONBLOCKING declarations, not the ESP32 function bodies. Document this coverage gap and link the backlog item, or require separate ESP32 Clang/LLVM analysis before claiming full coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/MoonDeck.md` around lines 356 - 361, Update the ESP32 hot-path
coverage statement in the documentation to acknowledge that desktop Clang
analysis does not inspect src/platform/esp32 function bodies. Either link the
relevant backlog item documenting this gap or state that separate ESP32
Clang/LLVM analysis is required before claiming full coverage.

Source: Path instructions

src/light/effects/MetaballsEffect.h (1)

37-42: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard degenerate grids before pointer arithmetic.

MetaballsEffect::tick() enters the outer loop when w == 0 && h > 0 and computes buf + y * w * cpl, which still performs pointer arithmetic for each row. Add the same if (w <= 0 || h <= 0) return; guard as LavaLampEffect before entering the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/effects/MetaballsEffect.h` around lines 37 - 42, Update
MetaballsEffect::tick() to return immediately when width or height is
non-positive, placing the if (w <= 0 || h <= 0) guard before any loop or buffer
pointer arithmetic. Keep the existing rendering logic unchanged for valid grids.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 96-100: Update the workflow environment assignment for CXX so
non-realtime matrix kinds leave it unset rather than exporting an empty string.
Preserve clang++ for the realtime kind, and use an initialization-time
CMAKE_CXX_COMPILER override only if needed to select the compiler without
setting CXX globally.

In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md:
- Around line 127-132: Update the `[[clang::nonblocking]]` plan entry to match
the checked-in `docs/metrics/hotpath-baseline.txt` count of 108 entries, or
regenerate the documentation and baseline together so all references report the
same total.

In `@moondeck/check/check_nonblocking.py`:
- Around line 227-230: Update the baseline handling around read_baseline() to
track BASELINE.exists() separately from the returned set, distinguishing a
missing baseline from an existing but empty baseline. Use file existence when
determining whether each finding is new, while retaining the known-set
membership check, and ensure the report’s “no baseline exists” status only
applies when the baseline file is absent.
- Around line 147-158: The clean-build validation in build_output must
distinguish whether -Wfunction-effects was enabled from whether diagnostics were
emitted. Add or reuse a build-capability flag/check for the warning, accept zero
diagnostics when capability is present, and only fail closed when the warning is
unavailable; ensure the incremental bypass is allowed only after the
full-capability prerequisite succeeds.

In `@moondeck/moondeck.py`:
- Around line 1806-1813: Update the log setup in the run/stream handling flow
around LOG_DIR and _running[script_id] so concurrent runs never open the shared
final log with "w". Create a PID-specific temporary log file, write the run
output there, and atomically promote it to <script_id>.log only when that
process is still the active _running[script_id]; ensure an older or killed
process cannot replace the newer run’s log.

In `@src/core/AudioService.h`:
- Around line 304-309: Move kSilence out of latestFrame() and define it at class
or namespace scope as a static constexpr AudioFrame, then update latestFrame()
to reference that shared constant while preserving the existing silence fallback
behavior.

In `@src/light/drivers/ParallelLedDriver.h`:
- Line 559: The MM_NONBLOCKING annotations are premature because these
render-loop paths still block or allocate. In
src/light/drivers/ParallelLedDriver.h#L559-L559, remove the annotation or
replace busWaitIfBusy() with nonblocking polling; in
src/light/drivers/PreviewDriver.h#L135-L135, move synchronous sendFrame() and
keptIdx_ allocation off the render path; and in
src/light/drivers/RmtLedDriver.h#L234-L234, move rmtWs2812Wait() off tick or
implement polling/state-machine behavior. Ensure each tick path avoids blocking
operations and heap allocation before retaining MM_NONBLOCKING.

---

Outside diff comments:
In `@moondeck/MoonDeck.md`:
- Around line 356-361: Update the ESP32 hot-path coverage statement in the
documentation to acknowledge that desktop Clang analysis does not inspect
src/platform/esp32 function bodies. Either link the relevant backlog item
documenting this gap or state that separate ESP32 Clang/LLVM analysis is
required before claiming full coverage.

In `@src/light/effects/MetaballsEffect.h`:
- Around line 37-42: Update MetaballsEffect::tick() to return immediately when
width or height is non-positive, placing the if (w <= 0 || h <= 0) guard before
any loop or buffer pointer arithmetic. Keep the existing rendering logic
unchanged for valid grids.

In `@src/platform/esp32/platform_esp32.cpp`:
- Around line 1115-1135: Update wifiStaStop() to also atomically store false in
wifiStaAssociated_ during teardown, alongside wifiStaConnected_. Ensure both
association and connection state are reset before a later wifiStaInit() or
static STA setup.

In `@src/platform/platform.h`:
- Around line 341-351: Make both IPv4 getters genuinely nonblocking: in
src/platform/platform.h lines 341-351, declare wifiStaGetIPv4 with the same
MM_NONBLOCKING contract as ethGetIPv4; in
src/platform/desktop/platform_desktop.cpp lines 658-677, update the getter
implementation to return cached octets and remove synchronous hostIp() route
resolution, ensuring the cache is maintained through the existing platform
connection/update flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1bfeaa66-fb23-446a-b183-245b9cb3e32d

📥 Commits

Reviewing files that changed from the base of the PR and between 5f2116d and 1d0184f.

📒 Files selected for processing (27)
  • .clang-tidy
  • .github/workflows/test.yml
  • docs/backlog/backlog-core.md
  • docs/coding-standards.md
  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md
  • docs/metrics/hotpath-baseline.txt
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • moondeck/MoonDeck.md
  • moondeck/check/check_hotpath.py
  • moondeck/check/check_nonblocking.py
  • moondeck/event/_gates.py
  • moondeck/moondeck.py
  • moondeck/moondeck_config.json
  • moondeck/moondeck_ui/app.js
  • src/core/AudioService.h
  • src/light/drivers/ParallelLedDriver.h
  • src/light/drivers/PreviewDriver.h
  • src/light/drivers/RmtLedDriver.h
  • src/light/effects/LavaLampEffect.h
  • src/light/effects/MetaballsEffect.h
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/platform_esp32.cpp
  • src/platform/esp32/platform_esp32_rmt.cpp
  • src/platform/platform.h
  • test/scenarios/light/scenario_peripheral_grid_sweep.json
  • test/scenarios/light/scenario_peripheral_switch.json
💤 Files with no reviewable changes (2)
  • moondeck/check/check_hotpath.py
  • moondeck/moondeck_config.json

Comment thread .github/workflows/test.yml Outdated
Comment on lines +127 to +132
3. ✅ **`[[clang::nonblocking]]`** — DONE. `MM_NONBLOCKING` (platform.h) on
`tick/tick20ms/tick1s`, `-Wfunction-effects` on the desktop build, the "clang-hotpath"
MoonDeck card, and `docs/metrics/hotpath-baseline.txt` freezing the 107 known
(file, callee) pairs. `check_hotpath.py` deleted; the pre-commit gate now runs
`check_nonblocking.py --incremental` (~0.7 s: it rebuilds only what the commit touched, so
it answers "did this change add a blocking call" rather than re-listing the baseline).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the documented baseline count.

This section says hotpath-baseline.txt freezes 107 pairs, but the checked-in baseline contains 108 entries on Lines 12–119. Update the count or regenerate the documentation and baseline together so the plan and tooling reports do not disagree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/history/plans/Plan-20260727` - Static analysis and repo health
tooling.md around lines 127 - 132, Update the `[[clang::nonblocking]]` plan
entry to match the checked-in `docs/metrics/hotpath-baseline.txt` count of 108
entries, or regenerate the documentation and baseline together so all references
report the same total.

Comment on lines +147 to +158
# FAIL CLOSED. -Wfunction-effects exists only on Clang 20+; CMake silently omits the flag
# on anything else, and this script would then report "0 findings" from a build that never
# ran the check — indistinguishable from a clean tree. A zero is only trustworthy if the
# warning is actually enabled, so say so instead of reporting a comfortable number.
if clean and "[-Wfunction-effects]" not in out:
print("No -Wfunction-effects diagnostics in the build output.\n"
"That means either the tree really is clean, or the compiler does not support the\n"
"warning (it needs Clang 20+; CMake omits it silently otherwise). Check with:\n"
f" grep -n 'Wfunction-effects' {(ROOT / 'CMakeLists.txt').relative_to(ROOT)}\n"
" cmake --build <dir> --clean-first 2>&1 | grep -c function-effects",
file=sys.stderr)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

found=0
while IFS= read -r file; do
  echo "== $file =="
  rg -n -- "CMAKE_CXX_COMPILER(_ID|_VERSION)?|-Wfunction-effects" "$file" || true
  found=1
done < <(fd -t f '^compile_commands\.json$|^CMakeCache\.txt$' .)

if [ "$found" -eq 0 ]; then
  echo "No compiler configuration files found" >&2
  exit 1
fi

Repository: MoonModules/projectMM

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "repo root:"
pwd
echo "tracked candidate files:"
git ls-files | rg '(^|/)(CMakeLists\.txt|moondeck/check/check_nonblocking\.py|moondeck/event/_gates\.py|compile_commands\.json|CMakeCache\.txt)$' || true

echo
echo "search Wfunction-effects and gate references:"
rg -n -- "-Wfunction-effects|function-effects|check_nonblocking|detect compiler|compiler version" . || true

Repository: MoonModules/projectMM

Length of output: 6230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== moondeck/check/check_nonblocking.py outline =="
ast-grep outline moondeck/check/check_nonblocking.py --view compact || true

echo
echo "== moondeck/check/check_nonblocking.py relevant sections =="
sed -n '1,220p' moondeck/check/check_nonblocking.py | cat -n

echo
echo "== moondeck/event/_gates.py relevant sections =="
sed -n '1,190p' moondeck/event/_gates.py | cat -n

echo
echo "== root CMakeLists relevant sections =="
sed -n '45,75p' CMakeLists.txt | cat -n

Repository: MoonModules/projectMM

Length of output: 25135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== check_clang_tidy module =="
sed -n '1,220p' moondeck/check/check_clang_tidy.py | cat -n

echo
echo "== check_clang_query module =="
sed -n '1,240p' moondeck/check/check_clang_query.py | cat -n

echo
echo "== static verifier for probe behavior and config condition =="
python3 - <<'PY'
from pathlib import Path
import re
src = Path("moondeck/check/check_nonblocking.py").read_text()
cmake = Path("CMakeLists.txt").read_text()
print("probe_looks_at_build_output_diagnostic = ", re.search(r'if clean and "(\[-Wfunction-effects\])" not in out', src) is not None)
print("clean_returns_none_on_missing_diagnostic = ", re.search(r'if clean and .* in out:\s+print\(".*\).?\s+return None', src, re.S) is not None)
print("cmake_conditional_add_compile_options = ", re.search(r'add_compile_options\(\s*-Wfunction-effects\s*-Wno-error=function-effects\s*\)', cmake, re.S) is not None)
print("cmake_requires_clang_and_version_ge_20 = ", re.search(r'if\(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 20\)', cmake) is not None)
PY

Repository: MoonModules/projectMM

Length of output: 26881


Reject full rebuilds with zero diagnostics only when -Wfunction-effects was not enabled.

CMakeLists.txt adds the warning only for Clang >= 20, so a clean Clang-build has no [-Wfunction-effects] diagnostic to probe. Use a build-capability check/flag instead of returned-warning counts for build_output(), and keep the incremental bypass restricted to when the full-capability prerequisite is satisfied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/check/check_nonblocking.py` around lines 147 - 158, The clean-build
validation in build_output must distinguish whether -Wfunction-effects was
enabled from whether diagnostics were emitted. Add or reuse a build-capability
flag/check for the warning, accept zero diagnostics when capability is present,
and only fail closed when the warning is unavailable; ensure the incremental
bypass is allowed only after the full-capability prerequisite succeeds.

Comment thread moondeck/check/check_nonblocking.py Outdated
Comment on lines +227 to +230
# Mark what is new since the baseline. Not a filter — everything is still shown.
known = read_baseline()
for r in rows:
r["new"] = bool(known) and (r["file"], r["callee"]) not in known

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Distinguish a missing baseline from an empty baseline.

--baseline legitimately writes an empty file when there are no findings. Because read_baseline() returns an empty set for both missing and empty files, bool(known) is false and later findings are marked new=False; the report also says that no baseline exists. Track BASELINE.exists() separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/check/check_nonblocking.py` around lines 227 - 230, Update the
baseline handling around read_baseline() to track BASELINE.exists() separately
from the returned set, distinguishing a missing baseline from an existing but
empty baseline. Use file existence when determining whether each finding is new,
while retaining the known-set membership check, and ensure the report’s “no
baseline exists” status only applies when the baseline file is absent.

Comment thread moondeck/moondeck.py
Comment on lines +1806 to +1813
log = None
log_bytes = 0
with suppress(OSError):
LOG_DIR.mkdir(parents=True, exist_ok=True)
log = (LOG_DIR / f"{script_id}.log").open("w", encoding="utf-8")
# Timezone-aware: a bare local timestamp is ambiguous when the log is read from
# another machine or after a DST change.
log.write(f"# {script_id} — {datetime.now().astimezone().isoformat(timespec='seconds')}\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent overlapping runs from corrupting the “last run” log.

A fast Run→Run creates two stream handlers that both open <script_id>.log with "w" and can interleave output. The older, killed process can therefore overwrite or append to the newer run’s log. Write each process to a PID-specific temporary file and atomically promote it only if that process is still _running[script_id].

Proposed direction
- log = (LOG_DIR / f"{script_id}.log").open("w", encoding="utf-8")
+ log_path = LOG_DIR / f".{script_id}.{proc.pid}.log"
+ published_log_path = LOG_DIR / f"{script_id}.log"
+ log = log_path.open("w", encoding="utf-8")
...
+ # After closing the per-process log:
+ with _lock:
+     is_current = _running.get(script_id) is proc
+ if is_current:
+     os.replace(log_path, published_log_path)
+ else:
+     log_path.unlink(missing_ok=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/moondeck.py` around lines 1806 - 1813, Update the log setup in the
run/stream handling flow around LOG_DIR and _running[script_id] so concurrent
runs never open the shared final log with "w". Create a PID-specific temporary
log file, write the run output there, and atomically promote it to
<script_id>.log only when that process is still the active _running[script_id];
ensure an older or killed process cannot replace the newer run’s log.

Comment thread src/core/AudioService.h
Comment on lines +304 to +309
static const AudioFrame* latestFrame() MM_NONBLOCKING {
// constexpr, not a function-local static: a static needs a guard variable and a
// one-time lock on first use, which is a blocking operation on the render path — and
// this is called from EIGHT audio-reactive effects' tick(). constexpr is initialized at
// compile time, so there is no guard and no lock.
static constexpr AudioFrame kSilence{};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)AudioService\.(h|hpp|cpp)$|CMakeLists|.*\.cmake|.*clang|.*compiler|.*Warnings|.*flags' | sed -n '1,120p'

echo "== location =="
fd -a 'AudioService\.h|AudioService\.cpp' . | sed 's#^\./##'

echo "== AudioService functions around latestFrame =="
ast-grep outline src/core/AudioService.h --match latestFrame --view expanded || true
wc -l src/core/AudioService.h
sed -n '260,330p' src/core/AudioService.cpp || true
sed -n '280,320p' src/core/AudioService.h || true

echo "== mm_nonblocking definitions/usages =="
rg -n "MM_NONBLOCKING|function-effects|non-blocking|nonblocking|NONBLOCKING" . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' | sed -n '1,160p'

echo "== compiler/config references =="
rg -n "\-Wfunction-effects|function-effects|Wno-function-effects|Werror|clang|CXXFLAGS|CMAKE_CXX_FLAGS|ESP" . --glob '!build/**' --glob '!dist/**' --glob '!node_modules/**' | sed -n '1,200p'

echo "== all local static constexpr in src/core? =="
rg -n "static constexpr .*\\{}|static constexpr .*;" src/core/AudioService.h src/core -g '*.{h,hpp,cpp,cxx,cc}' | sed -n '1,200p'

Repository: MoonModules/projectMM

Length of output: 50377


🌐 Web query:

Clang -Wfunction-effects constexpr local variable static constexpr does not make variable static storage duration

💡 Result:

In C++, declaring a local variable with constexpr does not imply static storage duration [1]. Storage duration and the constexpr specifier are independent concepts [2][1]. A local variable declared as constexpr has automatic storage duration by default, just like any other non-static local variable [1]. If you require the variable to have static storage duration, you must explicitly use the static keyword (e.g., static constexpr int x = 42;) [3][4]. Regarding your observation about Clang: 1. Separate Concerns: The constexpr specifier governs constant initialization and implies that the variable is a constant [5][2]. It does not modify the storage duration of the object [1]. 2. Function-Scope Static constexpr: Historically, C++ had restrictive rules regarding static variables within constexpr functions, but these were significantly relaxed in C++23 [3][4]. The fact that a variable is constexpr does not automatically promote its storage duration to static; rather, static and constexpr are orthogonal attributes that can be combined when specific lifetime requirements (like persisting across function calls) are needed [3][4]. 3. Implementation Behavior: If you observe behavior where a local constexpr variable behaves as if it has automatic storage duration (e.g., its lifetime is tied to the block scope), this is the expected and standard-compliant behavior [1]. If you are experiencing issues where a static constexpr variable is being treated as automatic, it would be a deviation from the standard, though it is more common to encounter confusion between the constant nature of the variable (which allows it to be used in constant expressions) and its storage duration (which determines its lifetime) [3][4][6]. In summary, constexpr is not a storage class specifier [2]. To ensure a local variable has static storage duration, you must explicitly declare it with static [3][4].

Citations:


🌐 Web query:

clang nonblocking attribute constexpr function local static constexpr variable static constexpr

💡 Result:

The Clang [[clang::nonblocking]] attribute is a language extension used to verify that a function never performs operations that could block execution, such as locking, heap allocation, or throwing exceptions [1][2]. This attribute applies to function types (including lambdas, blocks, and member functions) and allows the compiler to perform static analysis to ensure the function body—and any functions it calls—adhere to these performance constraints [1][3]. Regarding the use of static constexpr variables within constexpr functions, this is permitted as of C++23 [4][5]. Previously, such declarations were prohibited, even if the control flow did not reach them [4]. The restriction was lifted by the C++23 standard (via paper P2647R1) to allow static constexpr variables, as they are constant-initialized and do not introduce runtime state or side effects, thus maintaining the purity requirements of constexpr functions [6][4][7]. Key technical points: - The [[clang::nonblocking]] attribute acts as a performance contract. When applied to a function body, the compiler verifies that the function does not contain prohibited constructs. If the compiler cannot analyze a called function (e.g., an external function), it must assume that function is potentially blocking unless it is appropriately annotated [1][3]. - As of C++23, static constexpr variables are valid inside constexpr functions [4][5]. These variables are initialized at compile-time. Because their value is known constant data, they do not require thread-safety synchronization mechanisms (like those needed for runtime-initialized static local variables) at runtime [8]. - While static constexpr is allowed, general static variables (without constexpr) remain prohibited within constexpr functions because their initialization could involve dynamic code execution, violating the stateless nature required for constant expression evaluation [6][9][7].

Citations:


🌐 Web query:

"[[clang::nonblocking]]" "function-effects" local static

💡 Result:

In Clang, functions marked with the [[clang::nonblocking]] attribute are strictly prohibited from declaring static local variables [1][2][3]. The Clang compiler enforces this rule during Function Effect Analysis, and attempting to use a static local variable within such a function will trigger a compiler warning [1][4]. This restriction exists primarily for two reasons: 1. Thread-safe Initialization: Compilers typically generate a lock to ensure thread-safe initialization for static local variables (often used for Meyers singletons) [1][2]. This lock violates the performance constraints of the nonblocking attribute [5][6]. 2. Heap Allocation: The process of initializing a static local variable can inadvertently trigger heap allocation, which is also prohibited within nonblocking and nonallocating contexts [2][6]. If you need to use a lazily-constructed global that must be accessed from a nonblocking function, official Clang documentation recommends replacing the static local variable with a global variable and ensuring its initialization occurs before the nonblocking context is invoked [1][3].

Citations:


🌐 Web query:

site:clang.llvm.org/docs/FunctionEffectAnalysis.html "non-static" "static local" constexpr

💡 Result:

In C++, local variables inside a constexpr function follow standard C++ rules regarding storage duration. They are not implicitly static; by default, they have automatic storage duration unless explicitly declared as static [1]. The use of static local variables is permitted within functions, including those marked as constexpr. However, static local variables are prohibited in functions marked with certain attributes that impose strict constraints, such as the clang nonblocking attribute, because they involve initialization patterns that may require locks or heap allocation [1]. In the context of constexpr functions, note the following: 1. Automatic local variables: These are created and destroyed during the execution of the function. They are fully compatible with constant expression evaluation. 2. Static local variables: While allowed in standard C++ functions, they are generally not compatible with compile-time constant evaluation (constexpr). If a constexpr function attempts to evaluate a code path that requires the initialization or use of a static local variable at compile-time, it will typically fail to be a constant expression. Example of static usage prohibited in specific restricted contexts: void example [[clang::nonblocking]] { static Logger* logger = createLogger; // Trigger warning: nonblocking function must not have static local variables [1] }

Citations:


🌐 Web query:

site:clang.llvm.org/docs/FunctionEffectAnalysis.html "constexpr local static"

💡 Result:

It is syntactically valid in C++ to declare a variable as both static and constexpr within a function scope. While the provided documentation focuses on the performance implications of non-constexpr static local variables—specifically noting that compilers typically inject locks to ensure thread-safe initialization for such variables [1]—the constexpr specifier fundamentally alters this behavior. A variable declared as static constexpr is evaluated at compile time. Because its value is determined during compilation, the runtime initialization overhead and thread-safety concerns associated with standard static local variables do not apply [1]. In practice, using static with constexpr in a local scope is often redundant. A constexpr variable in a local scope already possesses a constant value known at compile time, and adding static does not change its constant nature or its availability for constant expressions. The static specifier merely forces the variable to have static storage duration rather than automatic storage duration.

Citations:


🌐 Web query:

clang 18 19 nonblocking static constexpr warning

💡 Result:

In Clang, the [[clang::nonblocking]] attribute is a performance constraint that allows the compiler to verify at compile-time that a function adheres to real-time safety requirements [1][2]. Functions marked [[clang::nonblocking]] are prohibited from containing language constructs or calling functions that might result in non-deterministic execution time [1][3]. Warning regarding static variables: Functions marked with [[clang::nonblocking]] are explicitly prohibited from declaring static local variables [1][4]. The compiler issues a warning for this because the initialization of static local variables (such as Meyers singletons) typically involves the generation of a lock by the compiler to ensure thread-safe initialization, which violates the non-blocking constraint [1][4]. Diagnostics and Control: - Violations of the nonblocking rules, including the use of static local variables, are diagnosed under the -Wfunction-effects warning category [1][4]. - Clang also issues a warning if a function is declared [[clang::nonblocking]] without also being noexcept, as nonblocking implies noexcept semantics; this diagnostic is controlled by the -Wperf-constraint-implies-noexcept flag [1][4]. - Note that -Wfunction-effects may be disabled by default in some versions, requiring explicit enablement if you wish to see these diagnostics [5]. Key constraints for [[clang::nonblocking]] functions include: - Must not allocate or deallocate memory [1][6]. - Must not throw or catch exceptions [1][6]. - Must not use static local variables or thread-local variables [1][7][8]. - Must not call functions that do not also have the nonblocking attribute [1][6][5]. These features were introduced and refined in recent Clang releases (starting around Clang 19) [6][8][5]. For comprehensive real-time safety, it is recommended to use these compile-time checks in conjunction with the RealtimeSanitizer (RTSan), which can detect violations at runtime [3].

Citations:


🌐 Web query:

clang 20 nonblocking function static constexpr warning

💡 Result:

In Clang 20 and later, functions marked with the [[clang::nonblocking]] attribute are subject to "Function Effect Analysis," which verifies that the function does not contain language constructs or calls that could cause blocking [1][2]. One of the specific restrictions enforced by this analysis is that a nonblocking function must not declare static local variables [2][3]. If you define a static variable—including a static constexpr variable—inside a function annotated with [[clang::nonblocking]], the compiler will emit a warning [2][3]. The reason for this restriction is that the compiler must generate a lock to ensure thread-safe initialization of static local variables (often used for Meyers singletons), which inherently violates the nonblocking guarantee [2]. Furthermore, the initialization of such variables can inadvertently trigger heap allocations, which is also prohibited in nonblocking contexts [2]. If you encounter this warning, you must remove the static local variable from the function or move it out of the nonblocking context [2]. Violations of these rules are diagnosed under the -Wfunction-effects diagnostic category [2].

Citations:


Move the silence fallback out of the function.

latestFrame() is MM_NONBLOCKING, and static constexpr AudioFrame kSilence{} is still a function-local static. Define it as a class- or namespace-scope static constexpr; otherwise Clang’s -Wfunction-effects can mark this nonblocking path as noncompliant.

Proposed fix
 class AudioService : public MoonModule {
 public:
+    static constexpr AudioFrame kSilence{};
+
     static const AudioFrame* latestFrame() MM_NONBLOCKING {
-        static constexpr AudioFrame kSilence{};
         AudioService* a = ActiveInstance<AudioService>::active();
         return a ? &a->frame_ : &kSilence;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static const AudioFrame* latestFrame() MM_NONBLOCKING {
// constexpr, not a function-local static: a static needs a guard variable and a
// one-time lock on first use, which is a blocking operation on the render path — and
// this is called from EIGHT audio-reactive effects' tick(). constexpr is initialized at
// compile time, so there is no guard and no lock.
static constexpr AudioFrame kSilence{};
static constexpr AudioFrame kSilence{};
static const AudioFrame* latestFrame() MM_NONBLOCKING {
// constexpr, not a function-local static: a static needs a guard variable and a
// one-time lock on first use, which is a blocking operation on the render path — and
// this is called from EIGHT audio-reactive effects' tick(). constexpr is initialized at
// compile time, so there is no guard and no lock.
AudioService* a = ActiveInstance<AudioService>::active();
return a ? &a->frame_ : &kSilence;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/AudioService.h` around lines 304 - 309, Move kSilence out of
latestFrame() and define it at class or namespace scope as a static constexpr
AudioFrame, then update latestFrame() to reference that shared constant while
preserving the existing silence fallback behavior.

// Not suppressed: a report of what blocks the render path is worthless if the two worst
// offenders are hidden from it. Moving the wait off the render path is backlogged
// (backlog-core: hot path).
void tick() MM_NONBLOCKING override {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Apply MM_NONBLOCKING only after the complete render path satisfies the contract.

These three ticks still reach blocking waits, synchronous socket I/O, or allocation. Remove the annotations until each path is redesigned as nonblocking, otherwise Clang rejects the call graph or the runtime contract is false.

  • src/light/drivers/ParallelLedDriver.h#L559-L559: remove the annotation or replace busWaitIfBusy() with nonblocking polling.
  • src/light/drivers/PreviewDriver.h#L135-L135: move synchronous sendFrame() work and keptIdx_ allocation off the render path.
  • src/light/drivers/RmtLedDriver.h#L234-L234: move rmtWs2812Wait() off tick or replace it with a poll/state machine.

As per coding guidelines, render-loop callees must avoid blocking operations and heap allocation; as per path instructions, light-driver loop methods must not block or allocate.

📍 Affects 3 files
  • src/light/drivers/ParallelLedDriver.h#L559-L559 (this comment)
  • src/light/drivers/PreviewDriver.h#L135-L135
  • src/light/drivers/RmtLedDriver.h#L234-L234
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/ParallelLedDriver.h` at line 559, The MM_NONBLOCKING
annotations are premature because these render-loop paths still block or
allocate. In src/light/drivers/ParallelLedDriver.h#L559-L559, remove the
annotation or replace busWaitIfBusy() with nonblocking polling; in
src/light/drivers/PreviewDriver.h#L135-L135, move synchronous sendFrame() and
keptIdx_ allocation off the render path; and in
src/light/drivers/RmtLedDriver.h#L234-L234, move rmtWs2812Wait() off tick or
implement polling/state-machine behavior. Ensure each tick path avoids blocking
operations and heap allocation before retaining MM_NONBLOCKING.

Sources: Coding guidelines, Path instructions

Twenty-one copies of the same "nothing to draw" check are gone from the effects
and replaced by one in Layer::tick, which already owns the enabled/role/extrude
gates. A dead channelsPerLight guard is deleted outright. clang-tidy is complete
as a report, and the lizard baseline turns out to pin far less than it claimed.

KPI: 16384lights | Desktop:856KB | ESP32:1498KB | tick:7258us(FPS:137) | heap:8277KB | lizard:136w

**Orchestration belongs to the caller.**
An effect answering "is there a grid at all?" is answering a question about the
pipeline, not about itself. Layer::tick already decides which children run and
when; the grid check is the same kind of decision, so it lives there once rather
than in every effect an author might write next. Effects may now assume
width/height/depth >= 1.

The guard gates only the effect loop, not the whole tick: the modifier pass below
advances per-frame state (RandomMap accumulates a bpm phase off elapsed()), and
skipping it during a zero-grid interval would leave that phase stale and the
chain in the wrong place when the grid returns.

**Light domain**
- Layer::tick holds the single degenerate-grid gate; 21 guards removed across 20
  effects (w/h/d, and the cols/rows and sizeX/sizeY aliases of the same thing).
- channelsPerLight() < 3 deleted from 15 effects as dead code. The member
  defaults to 3, the only setter is called with 3 everywhere in src, tests and
  scenario JSON, and the sole non-3 value in the tree is 4 (RGBW). There is no
  control, no persistence key and no HTTP write path that can lower it.
- MetaballsEffect carried a comment describing a guard that was not there;
  FreqMatrixEffect kept half of one (len but not cols). Both removed — the rule
  has one home now, and half a guard reads as "this axis is special".

**Core**
- platform_esp32: wifiStaStop() now clears wifiStaAssociated_. A stale true from
  a torn-down STA would let netSetStaticIPv4 apply a static IP to nothing.

**Tests**
- The 0x0x0 row of the effect sweep drove Layer::tick, which now returns before
  reaching any effect — so it was testing the new guard 39 times instead of the
  39 effects. It calls the effect directly on the degenerate grid, which is what
  proves the removals safe rather than assuming it. All 39 survive.

**Scripts/MoonDeck**
- clang-tidy reports 30 findings (was 47; a vendored-header filter and zeroed
  uint8_t out[4] account for most of the drop). WarningsAsErrors stays empty by
  design, and .clang-tidy, the plan and the backlog now say so instead of
  promising a ratchet: a report states what it finds and consumers decide.
- check_nonblocking distinguishes a missing baseline from an empty one. Empty
  means "nothing is known-good", so everything is new; only a missing file means
  no baseline was ever taken.
- CI passes the realtime lane its compiler via -DCMAKE_CXX_COMPILER=clang++-20
  rather than exporting CXX. An empty CXX="" on the other lanes is not the same
  as unset, and some toolchain probes read it as a broken compiler path.
  UNVERIFIED: this only exercises on push.

**Docs**
- New backlog card: lizard measures 94 of 2404 functions under a mis-parsed name.
  Its C++ parser loses the name on some bodies and falls back to the first
  keyword inside, so SolidEffect::tick is measured as
  SolidEffect::static_cast<lengthType>. Because whitelizard.txt matches by name,
  35 of 162 baseline entries pin nothing, and the check reports FAIL - 9 NEW on
  an unmodified tree. Re-baselining does not fix it: it would freeze today's
  fallback names, which shift again on the next edit inside the body.
- FreqSawsEffect's baseline entry re-keyed to the name lizard now emits. Its
  complexity fell from CCN 24 to 13; only the key changed, not the standard.

**Reviews**
- Reviewer (pre-commit, 38 files): 3 findings, all fixed above — the vacuous
  0x0x0 sweep row, the Metaballs comment, the FreqMatrix half-guard. It
  independently confirmed the channelsPerLight deletions and the modifier-pass
  reasoning.

Gates: 9 passed, 0 failed, 3 skipped (device-model catalog, firmware list and JS
tests are untriggered by this diff). Desktop 0 warnings, ESP32 S3 0 warnings,
872 unit tests, 19 scenarios.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ewowi

ewowi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #57. Closed automatically when the head branch next was renamed to next-iteration (which is the branch name the CodeQL workflow actually triggers on). Same commits, no content lost.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
moondeck/check/check_nonblocking.py (1)

68-77: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align baseline-state documentation with the new flag.

read_baseline() says a missing file means “everything is new,” but main() deliberately sets new=False until a baseline exists. Update the docstring/comments to describe “no comparison yet”; otherwise future changes may reintroduce the missing-versus-empty baseline bug.

Also applies to: 227-234

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/check/check_nonblocking.py` around lines 68 - 77, Update the
documentation in read_baseline() and the corresponding comments in main() to
describe a missing baseline as “no comparison yet,” matching main()’s new=False
behavior until a baseline exists. Do not alter the baseline parsing or new-flag
logic.
src/platform/esp32/platform_esp32.cpp (1)

404-413: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Publish the static-address snapshot atomically, not just its state flag.

ethStaticIp_/ethStaticGw_/etc. and the WiFi equivalents remain plain arrays. netSetStaticIPv4() writes them on the caller/render task while the link/association handlers read them during re-pin. The release/acquire flag only synchronizes the first publication; later edits while the flag is already true can race and produce a torn IPv4 configuration. Serialize the update or publish a versioned immutable snapshot.

Also applies to: 483-492

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/esp32/platform_esp32.cpp` around lines 404 - 413, The static
IPv4 configuration arrays updated by netSetStaticIPv4() must not be read
concurrently by link or association event handlers. Serialize reads and writes
of ethStaticIp_/ethStaticGw_ and the corresponding WiFi arrays with a shared
synchronization mechanism, or replace them with a versioned immutable snapshot;
ensure re-pin handlers obtain one consistent snapshot while preserving the
existing state-flag behavior.
docs/backlog/backlog-core.md (1)

390-392: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep report semantics in one canonical document.

The report-only, baseline, and pre-commit behavior here duplicates moondeck/MoonDeck.md and the implementation plan. Keep this backlog entry focused on architectural remediation and link to the canonical tooling documentation so counts and gate behavior do not drift.

As per path instructions, documentation keeps each fact in one canonical location and links elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/backlog/backlog-core.md` around lines 390 - 392, Refocus this backlog
entry on architectural remediation by removing duplicated descriptions of
-Wfunction-effects reporting, the hotpath baseline, and pre-commit gating.
Replace them with links to the canonical tooling documentation and
implementation plan, preserving only the remediation context so these facts
remain maintained in one location.

Source: Path instructions

♻️ Duplicate comments (1)
moondeck/check/check_nonblocking.py (1)

128-160: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not use emitted warnings as the capability probe.

A clean Clang 20+ build with zero findings has no [-Wfunction-effects] diagnostic, so the current clean-build check reports failure exactly when the tree becomes clean. Conversely, --incremental skips that check and can silently pass under GCC or older Clang. Validate compiler/version/flag capability from the build configuration for both modes, then accept zero diagnostics as clean.

Also applies to: 204-218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/check/check_nonblocking.py` around lines 128 - 160, Replace the
diagnostic-output check in build_output with a capability probe based on the
configured compiler/version and whether -Wfunction-effects is enabled, rather
than searching for emitted warnings. Apply this validation for both clean and
incremental builds, while still returning the build output and treating zero
diagnostics as a valid clean result when capability is confirmed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/metrics/whitelizard.txt`:
- Line 43: Update the whitelist entry for FreqSawsEffect to target the stable
mm::FreqSawsEffect::tick override/baseline instead of the ambiguous
mm::FreqSawsEffect::for token, preserving the existing complexity baseline and
violation tracking for tick.

In `@moondeck/MoonDeck.md`:
- Around line 362-366: Update the spelling in MoonDeck.md to use American
English consistently, replacing any occurrences of “analyse,” “analysed,” or
“analyser” with “analyze,” “analyzed,” or “analyzer,” including the changed
section and elsewhere in the document.

In `@src/light/layers/Layer.h`:
- Around line 165-174: Align the zero-grid contract across the three referenced
sites: in src/light/layers/Layer.h lines 165-174, revise the orchestration
comments to clarify that Layer gates effect dispatch without making non-zero
dimensions an effect precondition; in test/unit/light/unit_Effects_gridsweep.cpp
lines 11-16, document that direct invocation verifies effect safety at zero
dimensions; and in lines 74-82, retain the direct call while removing the claim
that zero-grid safety is outside the effect contract. Ensure effects remain safe
for all grid sizes and tick rates, including 0×0×0, without crashes or
zero-truncated animation math.

In `@test/unit/core/unit_Buffer.cpp`:
- Around line 42-43: Extend the lint suppression covering the moved-from
assertions in the buffer test so both CHECK statements, including a.count() == 0
and the nearby a.data() assertion, are exempted from the specified move
warnings. Use a second directive or a scoped NOLINTBEGIN/NOLINTEND without
changing the assertions.

In `@test/unit/core/unit_NetworkModule.cpp`:
- Around line 101-105: Update the comment beside the zero-initialized out buffer
in the parseDottedQuad test so it only describes behavior covered by the
assertions; remove the claim about failed parses comparing against zero unless
the CHECK_FALSE cases are extended with explicit unchanged-output assertions.

---

Outside diff comments:
In `@docs/backlog/backlog-core.md`:
- Around line 390-392: Refocus this backlog entry on architectural remediation
by removing duplicated descriptions of -Wfunction-effects reporting, the hotpath
baseline, and pre-commit gating. Replace them with links to the canonical
tooling documentation and implementation plan, preserving only the remediation
context so these facts remain maintained in one location.

In `@moondeck/check/check_nonblocking.py`:
- Around line 68-77: Update the documentation in read_baseline() and the
corresponding comments in main() to describe a missing baseline as “no
comparison yet,” matching main()’s new=False behavior until a baseline exists.
Do not alter the baseline parsing or new-flag logic.

In `@src/platform/esp32/platform_esp32.cpp`:
- Around line 404-413: The static IPv4 configuration arrays updated by
netSetStaticIPv4() must not be read concurrently by link or association event
handlers. Serialize reads and writes of ethStaticIp_/ethStaticGw_ and the
corresponding WiFi arrays with a shared synchronization mechanism, or replace
them with a versioned immutable snapshot; ensure re-pin handlers obtain one
consistent snapshot while preserving the existing state-flag behavior.

---

Duplicate comments:
In `@moondeck/check/check_nonblocking.py`:
- Around line 128-160: Replace the diagnostic-output check in build_output with
a capability probe based on the configured compiler/version and whether
-Wfunction-effects is enabled, rather than searching for emitted warnings. Apply
this validation for both clean and incremental builds, while still returning the
build output and treating zero diagnostics as a valid clean result when
capability is confirmed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4294f205-1725-403e-8e07-b7c40f2d678c

📥 Commits

Reviewing files that changed from the base of the PR and between 1d0184f and 4a2effa.

📒 Files selected for processing (39)
  • .clang-tidy
  • .github/workflows/test.yml
  • docs/backlog/backlog-core.md
  • docs/history/plans/Plan-20260727 - Static analysis and repo health tooling.md
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • docs/metrics/whitelizard.txt
  • moondeck/MoonDeck.md
  • moondeck/check/check_clang_tidy.py
  • moondeck/check/check_nonblocking.py
  • src/light/effects/BlurzEffect.h
  • src/light/effects/BouncingBallsEffect.h
  • src/light/effects/FixedRectangleEffect.h
  • src/light/effects/FreqMatrixEffect.h
  • src/light/effects/FreqSawsEffect.h
  • src/light/effects/GEQ3DEffect.h
  • src/light/effects/GEQEffect.h
  • src/light/effects/LavaLampEffect.h
  • src/light/effects/LissajousEffect.h
  • src/light/effects/MetaballsEffect.h
  • src/light/effects/Noise2DEffect.h
  • src/light/effects/NoiseMeterEffect.h
  • src/light/effects/ParticlesEffect.h
  • src/light/effects/PraxisEffect.h
  • src/light/effects/RingsEffect.h
  • src/light/effects/RipplesEffect.h
  • src/light/effects/RubiksCubeEffect.h
  • src/light/effects/SolidEffect.h
  • src/light/effects/SphereMoveEffect.h
  • src/light/effects/StarFieldEffect.h
  • src/light/effects/StarSkyEffect.h
  • src/light/effects/TetrixEffect.h
  • src/light/effects/TextEffect.h
  • src/light/layers/Layer.h
  • src/platform/esp32/platform_esp32.cpp
  • test/scenarios/light/scenario_peripheral_grid_sweep.json
  • test/unit/core/unit_Buffer.cpp
  • test/unit/core/unit_NetworkModule.cpp
  • test/unit/light/unit_Effects_gridsweep.cpp
💤 Files with no reviewable changes (23)
  • src/light/effects/BlurzEffect.h
  • src/light/effects/TextEffect.h
  • src/light/effects/LissajousEffect.h
  • src/light/effects/PraxisEffect.h
  • src/light/effects/GEQ3DEffect.h
  • src/light/effects/SphereMoveEffect.h
  • src/light/effects/ParticlesEffect.h
  • src/light/effects/RipplesEffect.h
  • src/light/effects/FreqMatrixEffect.h
  • src/light/effects/NoiseMeterEffect.h
  • src/light/effects/BouncingBallsEffect.h
  • src/light/effects/LavaLampEffect.h
  • src/light/effects/StarFieldEffect.h
  • src/light/effects/RingsEffect.h
  • src/light/effects/MetaballsEffect.h
  • src/light/effects/Noise2DEffect.h
  • src/light/effects/GEQEffect.h
  • src/light/effects/RubiksCubeEffect.h
  • src/light/effects/FixedRectangleEffect.h
  • src/light/effects/StarSkyEffect.h
  • src/light/effects/TetrixEffect.h
  • src/light/effects/SolidEffect.h
  • src/light/effects/FreqSawsEffect.h

src/main.cpp:mm_main # CCN 24, NLOC 148
src/light/layers/Layer.h:mm::Layer::buildFoldedLUT # CCN 24, NLOC 68
src/light/effects/FreqSawsEffect.h:mm::FreqSawsEffect::tick # CCN 24, NLOC 62
src/light/effects/FreqSawsEffect.h:mm::FreqSawsEffect::for # CCN 13, NLOC 36 (was 24/62; lizard cannot name this tick and falls back to an inner token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C4 'whitelizard|baseline|FreqSawsEffect|function|NAME' \
  moondeck/check/check_lizard.py docs/metrics/whitelizard.txt

Repository: MoonModules/projectMM

Length of output: 15849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== referenced file outline =="
ast-grep outline src/light/effects/FreqSawsEffect.h --view expanded || true

echo "== relevant source snippets =="
cat -n src/light/effects/FreqSawsEffect.h | sed -n '1,220p'

echo "== diff summary/name changes around FreqSawsEffect =="
git diff -- docs/metrics/whitelizard.txt | sed -n '1,140p'

echo "== exact baseline entries =="
python3 - <<'PY'
from pathlib import Path
p=Path("docs/metrics/whitelizard.txt")
for i,line in enumerate(p.read_text().splitlines(),1):
    if "FreqSawsEffect" in line:
        print(f"{i}: {line}")
PY

Repository: MoonModules/projectMM

Length of output: 12702


🌐 Web query:

lizard linter C++ lambdas keyword token function name behavior

💡 Result:

Lizard, a static code complexity analyzer, treats C++ lambda expressions by aggregating them into the parent function rather than treating them as independent, top-level functions [1][2]. This design choice is intended to simplify complexity analysis, as nested constructs like lambdas are often considered idiomatic or secondary to the primary function's logic [2]. Technical behavior details: - Parsing: Lizard's C++ parser (implemented in clike.py) uses finite state machines to identify functions [3][4][5]. Because lambdas are nested, the tool's logic directs them to aggregate into the parent scope [1]. - Limitations: Due to the complexity of parsing C++ syntax without a full compiler frontend, Lizard may misinterpret certain lambda structures [6][7]. Users have reported issues where lambdas, particularly those with complex capture lists [...] or nested braces, can cause the parser to incorrectly track brackets or misidentify the function state [1]. - Function Names: Since lambdas are not treated as independent functions, they do not appear as separate entries in Lizard's function list output [8]. Consequently, they do not have independent function names or separate cyclomatic complexity metrics in the standard report [1][8]. - Tokenization: Lizard utilizes a token-based pipeline [5]. While it successfully tokenizes much of C++, its inability to perform full macro expansion or semantic analysis means that advanced or highly obfuscated C++ constructs—including some lambda-heavy patterns—can confuse the bracket-stacking logic used to determine function boundaries [6][7]. In summary, if you are analyzing code where lambda complexity is significant, Lizard's default behavior is to hide this complexity within the parent function's total score rather than surfacing it as individual function metrics [2]. If the parser struggles with a specific lambda, it is often due to the limitations of its lightweight, regex-based state machine approach to parsing C++ [6][4].

Citations:


🌐 Web query:

GitHub terryyin/lizard lark python lizard_languages clike.py __init__ function_name keyword issue

💡 Result:

In the lizard static analysis tool, the clike.py module handles C/C++ parsing using a state-machine-based approach [1][2]. The function name identification process involves a specialized state, often managed by _state_function, which listens for function-like patterns [3][2]. Regarding the specific concern about function names, keywords, and parsing: 1. Function Name Identification: The tool uses a state machine to track tokens. A potential function is only confirmed when the parser encounters an opening brace { [1]. The internal logic includes checks to prevent non-word tokens from being incorrectly identified as function names [4]. 2. Keyword Handling: The parser must distinguish between actual function names and reserved keywords. For example, in the code, specific checks are implemented (such as those handling void or operator tokens) to ensure that keywords are not misidentified as function names during the parameter or definition parsing phases [3]. 3. Common Issues: If you are encountering an issue where a keyword is being misidentified as a function name, or a function name is being missed, it often relates to the interaction between the token generation and the parallel state machines (CLikeStates, CLikeNestingStackStates) [1][2]. The repository includes regression tests specifically for scenarios like function name detection (e.g., ensuring class or other keywords are handled correctly) [4]. If you are debugging a specific parsing error, ensure your lizard version is up to date, as many parsing improvements—particularly regarding raw string literals or complex function declarations—are implemented by refining the tokenizer patterns in clike.py [5]. If a specific keyword is causing issues, you can examine the _state_function or _state_dec methods in clike.py to see how that token is currently being consumed by the state machine [3].

Citations:


Preserve a stable baseline identity for FreqSawsEffect::tick.

The whitelist entry now uses mm::FreqSawsEffect::for, which can match a keyword/token inside the function instead of the callable method and changes the recorded complexity from 24/62. Use a stable .tick override/baseline entry so the gate continues to track violations for mm::FreqSawsEffect::tick.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/metrics/whitelizard.txt` at line 43, Update the whitelist entry for
FreqSawsEffect to target the stable mm::FreqSawsEffect::tick override/baseline
instead of the ambiguous mm::FreqSawsEffect::for token, preserving the existing
complexity baseline and violation tracking for tick.

Comment thread moondeck/MoonDeck.md
Comment on lines +362 to +366
The gap is real but narrow: `src/platform/esp32/` has no tick methods (it is free functions the
tick path calls into), and while a call INTO one of them is reported at the call site, the
function's own body is never analyzed. A platform function that blocks internally without
carrying `MM_NONBLOCKING` is invisible. Closing that needs an xtensa clang — backlogged as
"ESP32 clang/LLVM toolchain" in backlog-core.md.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Normalize spelling to American English.

The changed section uses analyzed, while LanguageTool reports mixed analyze/analyse variants elsewhere in this document. Normalize all occurrences to analyze, analyzed, and analyzer.

As per coding guidelines, **/*.md uses American English spelling throughout.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~364-~364: Do not mix variants of the same word (‘analyze’ and ‘analyse’) within a single text.
Context: ... site, the function's own body is never analyzed. A platform function that blocks intern...

(EN_WORD_COHERENCY)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/MoonDeck.md` around lines 362 - 366, Update the spelling in
MoonDeck.md to use American English consistently, replacing any occurrences of
“analyse,” “analysed,” or “analyser” with “analyze,” “analyzed,” or “analyzer,”
including the changed section and elsewhere in the document.

Sources: Coding guidelines, Linters/SAST tools

Comment thread src/light/layers/Layer.h
Comment on lines +165 to +174
// A degenerate grid has nothing to draw. This is orchestration — the Layer owns the
// decision to run the effect pass at all, the same way it owns the enabled/role gates
// below — so it is checked ONCE here rather than repeated as a guard clause in every
// effect's tick(). Effects may assume width/height/depth are all >= 1.
//
// It gates only the EFFECT pass, not the whole tick: the modifier pass below advances
// per-frame state (a beat-driven RandomMap) that must keep running so the chain is in
// the right phase when the grid comes back.
const bool hasGrid = width_ > 0 && height_ > 0 && depth_ > 0;
for (uint8_t i = 0; hasGrid && i < childCount(); i++) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the zero-grid contract across production and tests.

The Layer guard correctly skips normal effect dispatch for degenerate grids, but the comments in both files incorrectly make non-zero dimensions an effect precondition.

  • src/light/layers/Layer.h#L165-L174: document this as Layer-level orchestration gating, not permission for effects to assume non-zero dimensions.
  • test/unit/light/unit_Effects_gridsweep.cpp#L11-L16: state that direct invocation verifies effect safety at zero dimensions.
  • test/unit/light/unit_Effects_gridsweep.cpp#L74-L82: retain the direct call and remove the claim that zero-grid safety is outside the effect contract.

As per path instructions, effects must work at every grid size and tick rate, including 0×0×0, without crashes or animation math truncating to zero.

📍 Affects 2 files
  • src/light/layers/Layer.h#L165-L174 (this comment)
  • test/unit/light/unit_Effects_gridsweep.cpp#L11-L16
  • test/unit/light/unit_Effects_gridsweep.cpp#L74-L82
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/layers/Layer.h` around lines 165 - 174, Align the zero-grid
contract across the three referenced sites: in src/light/layers/Layer.h lines
165-174, revise the orchestration comments to clarify that Layer gates effect
dispatch without making non-zero dimensions an effect precondition; in
test/unit/light/unit_Effects_gridsweep.cpp lines 11-16, document that direct
invocation verifies effect safety at zero dimensions; and in lines 74-82, retain
the direct call while removing the claim that zero-grid safety is outside the
effect contract. Ensure effects remain safe for all grid sizes and tick rates,
including 0×0×0, without crashes or zero-truncated animation math.

Source: Path instructions

Comment on lines +42 to 43
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
CHECK(a.data() == nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'NOLINTNEXTLINE|CHECK\(a\.(data|count)\(' \
  test/unit/core/unit_Buffer.cpp

Repository: MoonModules/projectMM

Length of output: 722


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- relevant file context ---\n'
sed -n '1,90p' test/unit/core/unit_Buffer.cpp

printf '\n--- clang-tidy/lint config candidates ---\n'
git ls-files | rg -n '(^|/)(\.clang-tidy|clang-tidy|CMakeLists\.txt|Makefile|compile_commands\.json)$' || true

printf '\n--- references to moved-from assertions ---\n'
rg -n -C1 'moved-from state|a\.count|\(\(&&|&&\)\)' test/unit/core/unit_Buffer.cpp

Repository: MoonModules/projectMM

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant file context ---'
sed -n '1,90p' test/unit/core/unit_Buffer.cpp

printf '%s\n' ''
printf '%s\n' '--- clang-tidy/lint config candidates ---'
git ls-files | rg -n '(^|/)(\.clang-tidy|clang-tidy|CMakeLists\.txt|Makefile|compile_commands\.json)$' || true

printf '%s\n' ''
printf '%s\n' '--- references to moved-from assertions ---'
rg -n -C1 'moved-from state|a\.count|\(\(&&|&&\)\)' test/unit/core/unit_Buffer.cpp

Repository: MoonModules/projectMM

Length of output: 3528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- .clang-tidy ---'
sed -n '1,220p' .clang-tidy

printf '%s\n' ''
printf '%s\n' '--- run-clang-tidy references/scripts ---'
rg -n -i 'run-clang-tidy|clang-tidy|NOLINT|bugprone-use-after-move|cppcoreguidelines-use-after-move' . --glob '!build/**' --glob '!dist/**' || true

printf '%s\n' ''
printf '%s\n' '--- relevant doctest/matchers if present ---'
fd -a . . | rg 'doctest' | head -20

Repository: MoonModules/projectMM

Length of output: 27066


Cover both moved-from assertions with the suppression.

NOLINTNEXTLINE only suppresses the single next line, so CHECK(a.count() == 0); is still unprotected under the repo’s clang-tidy config. Add a second directive or use NOLINTBEGIN/NOLINTEND.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/core/unit_Buffer.cpp` around lines 42 - 43, Extend the lint
suppression covering the moved-from assertions in the buffer test so both CHECK
statements, including a.count() == 0 and the nearby a.data() assertion, are
exempted from the specified move warnings. Use a second directive or a scoped
NOLINTBEGIN/NOLINTEND without changing the assertions.

Comment on lines +101 to +105
// Zero-initialized: the analyzer cannot see that parseDottedQuad fills every octet on
// success, so it reads the CHECKs below as comparing garbage. Cheaper to state the
// starting value than to argue about it, and a failing parse then compares against a
// known 0 rather than whatever was on the stack.
uint8_t out[4] = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the test comment aligned with the assertions.

Line 104 says a failed parse compares against known zero, but the test never inspects out after a CHECK_FALSE. Remove that claim or add explicit unchanged-output assertions for rejected inputs.

🧰 Tools
🪛 Cppcheck (2.21.0)

[style] 103-103: The function 'appearsInUi' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/core/unit_NetworkModule.cpp` around lines 101 - 105, Update the
comment beside the zero-initialized out buffer in the parseDottedQuad test so it
only describes behavior covered by the assertions; remove the claim about failed
parses comparing against zero unless the CHECK_FALSE cases are extended with
explicit unchanged-output assertions.

ewowi added a commit that referenced this pull request Jul 28, 2026
Three sanitizer lanes were failing: two on a test line from the previous commit
that called effects on a zero grid, one on a clang-20 diagnostic GCC does not
emit. Desktop config files (WiFi PSKs, MQTT passwords) now land 0600 instead of
0644, and CodeQL stops re-reporting vendored code. Plan-20260727 is complete.

KPI: 16384lights | Desktop:856KB | ESP32:1498KB | tick:7382us(FPS:135) | heap:8271KB | lizard:136w

**The zero-grid contract runs one way.**
The previous commit moved the degenerate-grid guard into Layer::tick, which left
the sweep's 0x0x0 row testing the Layer 39 times instead of the 39 effects. The
fix I reached for — calling fx->tick() directly on a zero grid — asserts a
contract no effect makes, and it crashed: GEQ3DEffect divides by width() via
imap over a zero range, so ASan took SIGFPE and TSan caught doctest's signal
handler allocating. Same one line, two lanes.

Effects MAY assume width/height/depth >= 1; that is the whole point of the single
Layer gate. The 0x0x0 row pins the Layer's behaviour (clean no-op, zero-byte
buffer) and the three non-degenerate rows cover effect bodies, where they run.
The header now says so and names GEQ3D, so the next person does not re-add it.

**Tests**
- unit_Effects_gridsweep: direct-tick call removed, header corrected.
- mm_tests gets -Wno-error=double-promotion. doctest::Approx takes a double, so
  every CHECK against a float literal promotes inside doctest.h, which is
  vendored. -Wdouble-promotion stays -Werror for the firmware — the Xtensa has no
  FPU, so a stray promotion is a silent softfloat call in the render path — it
  just cannot also police a third-party header. Test target only, error only.

**Core**
- platform_desktop: one openTempOwnerOnly helper behind both atomic-write paths.
  std::fopen creates 0666 & ~umask (0644 in practice) and these are
  /.config/*.json holding WiFi PSKs and MQTT passwords. POSIX gets O_CREAT|O_EXCL
  with an explicit 0600; Windows keeps plain fopen (no mode_t — files inherit the
  parent directory ACL, which is that platform's answer to the same question).
  Verified on disk and end-to-end: a live control change against the running
  desktop binary rewrote SystemModule.json as -rw-------.

**Docs/CI**
- .github/codeql-config.yml excludes test/doctest.h, the only vendored source in
  the repo and the source of the one critical we do not own. paths-ignore is the
  standard mechanism rather than dismissing the same alert after every scan.
- CodeQL stays a sweep, never a PR gate, and the workflow now records that as
  settled rather than provisional. Same rule as clang-tidy and the hot-path
  check: a report states what it finds and consumers decide.
- Plan-20260727 steps 6 and 7 marked done. Step 7's "file modes 0666 -> 0600" was
  never a literal 0666 — nothing in the tree ever had one; it was fopen's default
  creation mode, which is why grepping for it found nothing. Open alerts are 0.

**Branch**
- Renamed next -> next-iteration, local and remote. The CodeQL workflow triggers
  on next-iteration, so with the branch named next it had silently never run on a
  push — the last scan was two commits stale. Deleting the old remote branch
  closed PR #56; recreated as #57 with the same commits and content.

Gates: all green, 8 changed files. Desktop 0 warnings, ESP32 S3 0 warnings, 872
unit tests, 19 scenarios, full ASan suite clean locally (the lane that failed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant