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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .actrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-P ubuntu-latest=catthehacker/ubuntu:act-latest
32 changes: 32 additions & 0 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by `uv run moondeck/docs/gen_moonwalk.py --issue-form` from the log-issue process in moonwalk/; edit the process, not this file.
name: Bug report
description: A bug report with the device's state attached.
labels:
- bug
body:
- type: markdown
attributes:
value: 'Describe what you saw and hand over the state of the device it happened on; diagnosing it is ours. Not sure it is a bug? Ask on [Discord](https://discord.gg/TC8NSUSCdV) first. A feature request needs no template and no diagnostics: describe the use case.


[Log an issue](https://moonmodules.org/MoonLight/moonwalk.html#log-issue)'
- type: textarea
id: version
attributes:
label: Which version and firmware does the device run?
description: Check the version on the Firmware card.
- type: textarea
id: diagnostics
attributes:
label: The diagnostics of System, Firmware and the card that went wrong
description: Copy each card's diagnostics.
- type: textarea
id: describe
attributes:
label: Steps to reproduce, what should happen, and what happens instead
description: Describe the steps, the expected and the actual.
- type: textarea
id: photo
attributes:
label: A photo or screenshot of what you see
description: Add a photo of what you see.
4 changes: 3 additions & 1 deletion .github/actions/ccache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ runs:
- if: inputs.install == 'true' && runner.os == 'macOS'
shell: bash
run: brew install ccache
- uses: actions/cache@v4
# act sets ACT, and runs no JavaScript action nested in a composite one; a cache only speeds a run, so a local run goes without it.
- if: ${{ !env.ACT }}
uses: actions/cache@v4
with:
path: .ccache
key: ccache-${{ inputs.key }}-${{ github.sha }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ on:
# Both branches are listed on purpose. A workflow whose trigger names a branch nobody is
# pushing to runs never, and reports nothing while looking healthy — this job sat idle for
# two commits when the working branch was called `next` instead of `next-iteration`
# (docs/work/past/lessons.md). `main` keeps it firing once this branch is merged and retired.
# (docs/contributing/lessons.md). `main` keeps it firing once this branch is merged and retired.
push:
branches:
- main
Expand Down
351 changes: 54 additions & 297 deletions .github/workflows/release.yml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ jobs:
- uses: ./.github/actions/ccache
with:
key: sanitizer-${{ matrix.kind }}
# The job declares the build tools it needs rather than leaning on the runner's preinstalls, so it runs on any Ubuntu image, act's included.
- name: install the build tools
run: sudo apt-get install -y cmake
# The whole suite under one sanitizer, as check_sanitizers.py runs it locally. RTSan is clang-only, so that lane names clang-20;
# the others take the runner's default compiler. The address lane writes the unit results as JUnit for the annotations: one lane is enough.
- name: build + run unit tests under ${{ matrix.kind }}sanitizer
Expand Down
13 changes: 4 additions & 9 deletions .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,9 @@ BasedOnStyles =
BasedOnStyles =

# Exempt for the reasons check_prose.py gives: quoted material, generated pages, vendor code.
# All of docs/work/ is exempt: past and future are quoted or inherited material, and a plan in
# present is a working document that ends at its PR rather than a page anyone reads twice.
[docs/work/past/*.md]
BasedOnStyles =
[docs/work/present/*.md]
BasedOnStyles =
[docs/work/future/*.md]
# All of docs/work/ is exempt: a plan is a working document that ends at its PR rather than a page
# anyone reads twice, and backlog_mirror/ is generated from the Project's cards.
[docs/work/**]
BasedOnStyles =
[docs/friend-repos/*.md]
BasedOnStyles =
Expand All @@ -63,8 +59,7 @@ BasedOnStyles =

# CLOSING THE NET. A page the sweep has finished is held to every rule on the whole file: a
# suggestion becomes an error there, so a regression fails the gate instead of annotating a PR.
# Add a page here when `vale <page>` reports nothing; the sweep plan in docs/work/present/ is the
# list. When every page is here, the two prose scripts go and this section becomes the default.
# Add a page here when `vale <page>` reports nothing. When every page is here, the two prose scripts go and this section becomes the default.
[docs/reference/installing-on-linux.md]
BasedOnStyles = MoonLight
MoonLight.Weasel = error
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The product owner is the critical success factor. They review every line before

**An agent posts on GitHub only what the product owner has read**: a comment, a review reply or an issue goes out after they said so. Text in their name to another project or community carries no agent attribution line or trailer.

**Report the real situation.** A reason to stop is a real one (a failing theory, a broken tree, a decision needed), never the time of day, and an item reported as open exists under `docs/work/future/`.
**Report the real situation.** A reason to stop is a real one (a failing theory, a broken tree, a decision needed), never the time of day, and an item reported as open exists as a card on [the Project](https://github.com/users/MoonModules/projects/5).

**Reverting is the product owner's call**, whatever prompted it: a contradicting doc, a reviewer finding, a failing check, or the agent's own second thoughts. State the case and wait.

Expand Down Expand Up @@ -84,7 +84,7 @@ Every activity is a [MoonWalk](docs/moonwalk.md) process, the one home of its st

### Main and branch

**The product owner creates every branch**, and main stays releasable: [the branch](docs/moonwalk.md#branch). A change goes from [a spec and a plan](docs/moonwalk.md#pick-spec-plan) to a pull request the plan describes. **Moving the plan to `docs/work/past/plans/` is the product owner's call**, because "the code is written" is not "the plan is realized".
**The product owner creates every branch**, and main stays releasable: [the branch](docs/moonwalk.md#branch). A change goes from [a spec and a plan](docs/moonwalk.md#pick-spec-plan) to a pull request the plan describes. **Closing the plan, as a closed issue labeled `plan`, is the product owner's call**, because "the code is written" is not "the plan is realized".

**A contributed pull request is one topic, max 400 changed lines and ten files**, tests included, based on main: [Contribute a pull request](docs/moonwalk.md#contribute-pr). The contributor vouches for every line, agent-written or not, answers reviewers in their own words, and says what an agent wrote and what was tested. **A contribution is reviewed and finished as [Review and finish a contribution](docs/moonwalk.md#review-contribution) describes**. The contributor fixes their own code first and the maintainer finishes, case by case. The merge keeps their commits and authorship.

Expand Down Expand Up @@ -116,4 +116,4 @@ On "run pre-release": [Release](docs/moonwalk.md#release). Every commit and merg

Published at [moonmodules.org/MoonLight](https://moonmodules.org/MoonLight/); sources under `docs/`, laid out in [the documentation hierarchy](docs/contributing/documentation-standards.md#the-hierarchy). Docs describe the system as it is; git is the history; specs precede implementation.

`docs/work/` is the exemption to present tense: `future` is what does not exist yet, `present` is being built, `past` is what shipped and the lessons it taught. Agents read it when planning, on request, and it shrinks under mandatory subtraction like everything else.
`docs/work/` is the exemption to present tense: it holds the plans being built and `backlog_mirror/`, the generated copy of [the Project](https://github.com/users/MoonModules/projects/5)'s cards, where the backlog lives; design studies are in [docs/design/](docs/design/index.md). A shipped or stopped plan is a [closed issue labeled `plan`](https://github.com/MoonModules/MoonLight/issues?q=label%3Aplan%20is%3Aclosed), and hard-won lessons are in [lessons.md](docs/contributing/lessons.md). Agents read the work when planning, on request, and it shrinks under mandatory subtraction like everything else.
8 changes: 7 additions & 1 deletion docs/assets/moonwalk.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// MoonWalk's journey map: every process from moonwalk/*.yaml, drawn from moonwalk.json as a story map, stages (or systems) as columns and each process's steps inside it. The model is docs/work/future/moonwalk.md.
// MoonWalk's journey map: every process from moonwalk/*.yaml, drawn from moonwalk.json as a story map, stages (or systems) as columns and each process's steps inside it. The model is docs/explanation/architecture/moonwalk.md.
// Two hosts: on the docs site it adds a map button to every page's header and draws the map on the MoonWalk page, through Material's document$ so instant navigation mounts it too; the device's app imports it from the site and calls drawMap with its own state, so a device shows what it has done and what comes next.

const ROOT = new URL("../", import.meta.url);
Expand Down Expand Up @@ -485,6 +485,12 @@ function showPanel(panel, model, pid, done, onDone, host = {}) {
}
const meta = [model.stages.find((s) => s.id === p.stage)?.title, p.actor, ...(p.systems || [])].join(" · ");
panel.append(control("h3", {}, p.title), control("p", { className: "mm-map-meta" }, meta), control("p", {}, p.goal));
// The open backlog cards whose Process field names this process, each linking its card on the Project.
if (p.open?.length) {
const list = control("p", { className: "mm-map-meta" }, `Open work (${p.open.length}): `);
p.open.forEach((c, i) => list.append(i ? ", " : "", c.card ? control("a", { href: c.card, target: "_blank", rel: "noopener" }, c.title) : c.title));
panel.append(list);
}
// Done on the device is what the state shows, so it has no checkbox to undo and says on what it rests, at the top where a reader looks for it.
if (host.device?.has(pid)) {
const why = conditionText(p.steps.flatMap((s) => s.then || []));
Expand Down
10 changes: 6 additions & 4 deletions docs/contributing/coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,19 @@ Option B (children first on `tick`; parent first on `setup` / `defineControls` /

### Per-type behavior lives with the type

A struct or enum that is the semantic owner of some data owns the functions that interpret, serialize or validate it. A control descriptor, a packet and a module role are each such an owner. They live next to the type, never at the call sites. Free functions in the same `.cpp` count; member methods on the owning class are stronger; virtual methods on a base class are strongest. The wrong shape is the same `switch (type)` repeated in every consumer: adding a variant means hunting across N files, and the compiler cannot say when one is missed.
A struct or enum that is the semantic owner of some data owns the functions that interpret, serialize or validate it. A control descriptor, a packet and a module role are each such an owner. They live next to the type, never at the call sites. The wrong shape is the same `switch (type)` repeated in every consumer: adding a variant means hunting across N files, and the compiler cannot say when one is missed.

Three concrete patterns, all already common in this codebase:

- **Discriminator + free functions in the type's own file.** `ControlType` + `writeControlValue` / `applyControlValue` / `controlTypeName` in [Control.cpp](../src/core/module/Control.cpp); `parseDottedQuad` / `formatDottedQuad` next to `ControlType::IPv4` in [Control.h](../src/core/module/Control.h); `WhiteMode` + `rebuild()` in [Correction.h](../src/light/drivers/Correction.h). Best when the discriminator is a plain enum and the operations are small.
- **Methods on the owning class.** [Buffer.h](../src/light/layers/Buffer.h)'s `allocate` / `free` / `clear`; [Scheduler.h](../src/core/module/Scheduler.h)'s `addModule` / `tick` / `prepareTree`; [ControlList](../src/core/module/Control.h)'s `addX` family. Best when the class has identity and the operations naturally form a small interface.
- **Virtual methods on a base class.** [MoonModule.h](../src/core/module/MoonModule.h)'s lifecycle (`setup`, `tick`, `tick1s`, `defineControls`, `prepare`, …). Best when polymorphism is already in play.

Counter-example to avoid: a `switch (c.type)` on `ControlType` duplicated across the HTTP server, the filesystem module and the scenario runner. That shape forces a new ControlType to be added in four places, and the compiler can't catch a missed switch on a non-exhaustive enum. The per-type dispatch instead lives next to `ControlType` in [Control.cpp](../src/core/module/Control.cpp); consumers call `writeControlValue(sink, c)` and don't need to know the enum's shape.
Counter-example to avoid: a `switch (c.type)` on `ControlType` duplicated across the HTTP server, the filesystem module and the scenario runner, so a new ControlType is added in four places and the compiler can't catch a missed one. The dispatch lives next to `ControlType` in [Control.cpp](../src/core/module/Control.cpp) instead; consumers call `writeControlValue(sink, c)`.

When a `switch (type)` outside the type's home file is legitimate: the caller has a genuinely different concern. The HTTP server mapping `ApplyResult` to status codes is a transport policy, not per-type behavior, and scenario_runner's `switch (JsonVal::type)` dispatches on *its own* discriminator. The rule is "per-type dispatch lives with the type", not "switches are banned".
A `switch (type)` outside the type's home file is legitimate when the caller has a different concern: the HTTP server mapping `ApplyResult` to status codes is transport policy, and scenario_runner's `switch (JsonVal::type)` dispatches on its own discriminator.

**The UI follows the same rule**: `app.js` dispatches on control types, never on module names, and a module that needs more than the generic controls brings its own widget from its own file.

**A flat parser that returns a zero sentinel for a missing key must be paired with a presence check before the value is applied as authoritative.** `json::parseInt(json, key)` returns 0 for an absent key, indistinguishable from a real 0. On a persistence-overlay load path that clobbers a non-zero default when an older or partial file omits the key. Guard with `json::hasKey()` first: an absent key leaves the control untouched (its default stands); a present key (even value 0) applies. Any "control resets to its default/0 after reboot" symptom is this overlay smell, not a control-init bug.

Expand Down Expand Up @@ -208,7 +210,7 @@ Which checks run at which lifecycle event is defined once, in the [commit](../mo

## Debugging and verification

Hard-won discipline for diagnosing hardware and infrastructure failures, distilled from the war stories in [lessons.md](../work/past/lessons.md).
Hard-won discipline for diagnosing hardware and infrastructure failures, distilled from the war stories in [lessons.md](lessons.md).

- **Prove the failure is *about* the change before editing code.** When something fails right after a change, re-run it isolated and probe the actual end state. Confirm the artifact under test is the one you built: process uptime, the `build` timestamp, what is bound to the port. A stale process, a loaded machine, or an async-confirmation timeout reads as a regression it isn't.
- **A status/dimension assertion does not prove the pipeline renders.** A correctness test for a mapping or effect asserts the buffer or LUT is non-empty with the expected coverage, beyond the declared dimensions looking right.
Expand Down
4 changes: 2 additions & 2 deletions docs/contributing/documentation-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ Every page serves one of four reader needs, and only one. This is [Diátaxis](ht

The test for any page is the cell it sits in. A tutorial that stops to explain, or a reference that starts to teach, is two pages: move the other half to where it belongs.

Two kinds of page sit outside the grid on purpose. **The rules**, [coding-standards.md](coding-standards.md) and this page in `contributing/` and [CLAUDE.md](../../CLAUDE.md) at the repo root, are for contributors and bind every change. `legal/` sits outside it too. **Work on its way to the code** lives under `docs/work/`, apart from the pages that describe the system. `future` holds what is planned, `present` what is being built, which moves to `past` at its PR, and `past` the closed plans, shipped or cancelled.
Two kinds of page sit outside the grid on purpose. **The rules**, [coding-standards.md](coding-standards.md) and this page in `contributing/` and [CLAUDE.md](../../CLAUDE.md) at the repo root, are for contributors and bind every change. `legal/` sits outside it too. **Work on its way to the code** lives under `docs/work/`, apart from the pages that describe the system. It holds the plans being built; the backlog is [the Project](https://github.com/users/MoonModules/projects/5)'s cards, mirrored to `backlog_mirror`, a survey before a plan is a [design study](../design/index.md) in `design/`, and a plan that ships or stops becomes a [closed issue labeled `plan`](https://github.com/MoonModules/MoonLight/issues?q=label%3Aplan%20is%3Aclosed).

Two scales below a page. **A module** has one reference card and one header (see [Module pages](#module-pages)); **a line of code** carries its own reason in a comment (see [Comments](#comments)).

**The split that decides everything else.** A hand-written page says what the code cannot; the header is the code's own documentation. So a fact about behavior belongs in the `.h`, beside the code. A fact about intent, cost or sequence has no member to sit on, and is written.

**A shipped plan is a working document, not testimony.** It may be edited, trimmed, or deleted: whatever its PR already carries is duplication. Only a dated record stating what was true at a moment (release notes, an inventory quoted from another project) is kept unrewritten.
**A shipped plan is a working document, not testimony.** Its closed issue keeps what no PR carries, and may be edited or trimmed: whatever its PR already carries is duplication. A dated record of a past state, such as release notes or an inventory quoted from another project, stays as written.

## Writing

Expand Down
Loading
Loading