Skip to content

Static analysis as reports: compiler-checked hot path, clang-tidy, lizard, clang-query, CodeQL - #57

Merged
ewowi merged 7 commits into
mainfrom
next-iteration
Jul 28, 2026
Merged

Static analysis as reports: compiler-checked hot path, clang-tidy, lizard, clang-query, CodeQL#57
ewowi merged 7 commits into
mainfrom
next-iteration

Conversation

@ewowi

@ewowi ewowi commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What this lands

A static-analysis stack where every layer is a report, not a gate, plus the refactor and fixes that came out of using it.

Compiler-checked hot-path discipline. 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 a regex over a tick body cannot see: it reads the text and is blind to what its callees do. The attribute is inherited by overrides, so three annotations cover ~90 modules, and it sits in tickChildren's member-pointer type — without that, indirect dispatch is a hole every module's tick escapes through. check_hotpath.py (170 lines of regex) is deleted; its gate is now the compiler check running incrementally in 0.7s.

The stack, documented in docs/testing.md § Static analysis: compiler warnings → clang-tidy → sanitizers → RTSan + [[clang::nonblocking]] → CodeQL, with lizard counting complexity for the trend and clang-query as the home for bespoke AST rules. Each has a MoonDeck card. RealtimeSanitizer joins the sanitizer matrix as the runtime half of the same attribute.

Why nothing gates. WarningsAsErrors is empty, CodeQL never runs on pull_request, the hot-path check never fails the event. A gate nobody can satisfy gets disabled rather than obeyed, and it pushes people to suppress a finding under time pressure — the opposite of why the tool is there. Reports state what they find; consumers decide. The exception is compiler warnings, which are -Werror because they are few and fixed on sight.

Orchestration moved to the caller. 21 copies of if (w <= 0 || h <= 0) return; are gone from 20 effects, replaced by one gate in Layer::tick — which already owns the enabled/role/extrude/timing decisions. Effects may now assume width/height/depth >= 1. The gate covers only the effect loop: the modifier pass below advances per-frame state (RandomMap's bpm phase) that must keep running so the chain is in the right phase when the grid returns. A dead channelsPerLight() < 3 guard came out of 15 more effects — the member defaults to 3, is never set below it anywhere in src/, tests or scenario JSON, and has no control, persistence key or HTTP write path.

Fixes found along the way

  • wifiStaStop() did not clear wifiStaAssociated_; a stale true would apply a static IP to a torn-down interface. Four cross-task flags made atomic.
  • Desktop config files (/.config/*.json — WiFi PSKs, MQTT passwords) were created 0644. One openTempOwnerOnly helper now creates them 0600 on POSIX; Windows keeps fopen, where files inherit the parent ACL. Verified on disk and end-to-end against the running binary.
  • A Windows build break (<numbers> included inside a POSIX-only guard) and a GCC break (unguarded clang pragma under -Werror).

Reviewed

The Reviewer agent ran twice over the branch diff. Ten findings, all fixed, including a real bug: ParticlesEffect::initParticles runs from prepare(), not tick(), so Layer::tick's gate never covered it — it is safe only because trail_ is empty on a zero grid, which is now stated at the site.

Three findings were this branch's own rules failing on their last application: a de-duplication link pointing at the wrong section (it resolved to a real heading, so it looked fine while landing readers somewhere else), a "report not a gate" copy that survived the pass meant to collapse it, and a CodeQL trigger naming only next-iteration — which would have gone silent the moment this branch merged and was retired.

External review (CodeRabbit) was skipped deliberately — auto-review is disabled repo-wide in .coderabbit.yaml, and the PO chose to merge on the Reviewer passes plus green CI rather than take the round trip.

Verification

131 files, +1318/−889. All 8 PR checks pass, including the three sanitizer lanes and CodeQL. 7 pre-merge mechanical gates pass. Desktop 0 warnings, ESP32-S3 0 warnings, 872 unit tests, 19 scenarios.

Measured, not assumed: the guard refactor was A/B-built against its parent — desktop scenario ticks 129/130/131µs before, 127/130/131µs after. The KPI line's ESP32 tick moved on this branch, but that number is read from a live bench device, so it reflects the attached board rather than this code.

Known and recorded, not fixed

  • 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. Backlogged with the measured numbers; re-baselining does not fix it.
  • 30 clang-tidy findings remain, triaged and visible rather than suppressed. The nine unchecked-string-to-number-conversion sites were each read and confirmed safe.
  • performance.md is unchanged — its numbers are hardware-measured.

Plan-20260727 is complete and deleted; its durable content lives in testing.md, lessons.md, the backlog, and — for the negative results — in check_clang_query.py beside the matcher they constrain.

🤖 Generated with Claude Code

ewowi and others added 4 commits July 28, 2026 16:02
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>
…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>
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>
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>
@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: 7fcd39bc-6e34-49b7-8b34-962654a7f81c

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:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next-iteration

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.

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>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

…review

Plan-20260727 is complete and deleted; its durable content now lives in
testing.md, lessons.md and the backlog, in present tense. The pre-merge Reviewer
found six things — one real bug, one backlog card asking for shipped work, and
four cases of the subtraction pass this branch skipped.

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

**A deleted file is not a finished subtraction.**
Removing check_hotpath.py and the plan left three references pointing at them,
including a CMakeLists comment claiming the ESP32 build still runs the deleted
script and a -Wno-error whose only stated removal criterion was "once
check_hotpath reports zero". Both read as live instructions. Deleting the thing
is half the job; the other half is every place that mentioned it.

**Light domain**
- ParticlesEffect: initParticles() is called from prepare(), NOT tick(), so
  Layer::tick's degenerate-grid gate does not cover it — its guard was removed
  with the 21 that the gate does cover. It is safe today only because resize(0)
  frees trail_, which is a different guard doing the work by accident. That is
  now stated at the site: without it, every particle seeds from (rand8() * w) >> 4
  with w == 0.

**Core**
- platform_esp32: the last raw read of a converted atomic made explicit. It sat
  in netSetStaticIPv4, next to an explicit store — the one place the diff broke
  its own convention.

**Docs**
- Plan-20260727 deleted. testing.md gains a Static analysis section (the five
  layers, the one-rule-one-owner table, report-not-gate, and Verify a zero before
  believing it with the seven silent-failure modes); lessons.md gains the
  /code-scanning/alerts non-default-branch trap; the backlog keeps the lizard
  mis-parse card. testing.md had nothing on static analysis before this.
- Removed the clang-query backlog card added earlier this branch: everything it
  asked for — constexpr exclusion, no size threshold, the name_[16] false
  positive — already shipped and is documented on the card. A backlog item that
  requests built work is worse than none; it gets picked up and redone.
- De-duplicated what the plan move scattered. "Report, not a gate" had four
  hand-written homes and "verify a zero" two; each now states the rule once and
  the rest reference it. The CodeQL workflow header described an unanswered POC,
  which stopped being true when it found and fixed three criticals.
- Dropped two wrong cached counts rather than re-caching them: .clang-tidy said
  47 clang-tidy findings (30) and test.yml said ~50 blocking calls (the baseline
  file has 107). Both now name the file instead of a number.
- CLAUDE.md § Commit: "commit now" applies to the diff the PO just reviewed, and
  any later edit cancels it. Written down because I got this wrong twice —
  "we commit in one go" answers how MANY commits, not WHEN.

**Reviews**
- Reviewer (pre-merge, 126 files): 6 findings, all fixed above. It confirmed the
  channelsPerLight deletions are genuinely dead code, that Layer::tick's gate
  placement is right, and that the hot path gained no per-frame cost.

Not done, deliberately: performance.md is unchanged (its numbers are
hardware-measured), and the permission list is pruned 98 -> 71 locally but NOT
snapshotted — it has doubled against the approved 34 and the additions need PO
review, not an agent's.

Gates: 7 mechanical passed, 0 failed. Desktop 0 warnings, ESP32 S3 0 warnings,
872 unit tests, 19 scenarios.

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

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…deQL trigger

Four findings from the branch review, three of them this branch's own rules
failing on their last application: a de-duplication link pointing at the wrong
section, a "report not a gate" copy that survived the pass that collapsed it, and
a CodeQL trigger that would have gone silent the moment this branch merges.

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

**A reference to the wrong home is worse than the duplication it replaced.**
MoonDeck.md pointed at coding-standards.md#tests for the -Wfunction-effects
transitivity rule, which actually lives under ## Static checks. The anchor
resolves to a real heading, so nothing looks broken — the reader just lands on
test-file placement and concludes the rule is not written down. Duplication is at
least visible; a confidently wrong pointer is not.

**Docs/CI**
- codeql.yml: the push trigger now lists `main` as well as `next-iteration`. It
  named only this branch, so once this merges the job would fire on a branch
  nobody pushes to and report nothing while looking healthy — the exact
  silent-idle failure lessons.md was written to record two commits ago, and the
  second time this branch has hit it.
- codeql.yml: the POC framing is gone from the trigger comment too. The top
  header was rewritten last commit to say the POC is over; the block ten lines
  below still called this "the POC branch" and said the workflow lives here
  "until the POC is judged". Same edit, half applied.
- codeql.yml: dropped the second full-length copy of the report-not-gate
  argument. The same file both referenced the single home and restated it.
- testing.md: "A analyser" -> "An analyser", first line of a new section on a
  published docs site.

**Scripts/MoonDeck**
- check_clang_query.py records the three AST constraints the array rule was built
  around, two of which are NEGATIVE results and had no home once the plan was
  deleted: there is no size-threshold matcher (hasSize is exact-match,
  sizeGreaterThan does not exist); varDecl alone silently misses every class
  member and needs fieldDecl beside it; and "fixed number vs named constant" is
  not recoverable, because the AST folds kMaxLanes to [16] before a matcher sees
  it. A negative result with no home is one somebody re-derives.

**Reviews**
- Reviewer (pre-merge, 130 files): verified all six findings from the previous
  pass as completely fixed (it re-audited all 34 atomic uses and found no raw
  reads remaining), then raised these four. It also confirmed no hot-path cost
  was added and that the plan deletion lost nothing else.

Gates: 7 mechanical passed, 0 failed. CI green on the pushed commit including all
three sanitizer lanes and CodeQL. ESP32 S3 rebuilt (platform.h changed after the
last build), 0 warnings.

Still open for the PO, deliberately not done here: CodeRabbit skipped PR #57
because auto-review is disabled repo-wide by config, so external review has not
run on this PR; the permission list is pruned 98 -> 71 but not snapshotted; the
PR description still describes only the first commit; performance.md is
unchanged since its numbers are hardware-measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ewowi ewowi changed the title Compiler-checked hot-path discipline and tier-zero warnings Static analysis as reports: compiler-checked hot path, clang-tidy, lizard, clang-query, CodeQL Jul 28, 2026
@ewowi
ewowi merged commit bc42feb into main Jul 28, 2026
8 checks passed
@ewowi
ewowi deleted the next-iteration branch July 28, 2026 21:09
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.

3 participants