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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ BasedOnStyles =
# suggestion becomes an error there, so a regression fails the gate instead of annotating a PR.
# Add a page here when `vale <page>` reports nothing; the sweep plan in docs/work/present/ is the
# list. When every page is here, the two prose scripts go and this section becomes the default.
[docs/how-to/installing-on-linux.md]
[docs/reference/installing-on-linux.md]
BasedOnStyles = MoonLight
MoonLight.SentenceLength = error
MoonLight.Weasel = error
Expand Down
200 changes: 15 additions & 185 deletions CLAUDE.md

Large diffs are not rendered by default.

50 changes: 4 additions & 46 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ if(MSVC)
# -Wextra. The codebase uses explicit static_cast at the sites
# where loss-of-data is the real concern; suppressing here brings
# MSVC noise level back in line with GCC.
# C5285 — vendored doctest.h (v2.4.11) forward-declares `std::tuple` as a
# libstdc++/libc++ portability workaround; the VS 2026 MSVC STL
# newly flags that as "specializing a standard template". It's in
# third-party header code we don't own and GCC/Clang never warn.
add_compile_options(/W4 /WX /wd4702 /wd4244 /wd4267 /wd5285)
add_compile_options(/W4 /WX /wd4702 /wd4244 /wd4267)
# Our sources are UTF-8 and every other compiler reads them that way. MSVC alone assumes the
# system codepage for both the source and the execution charset, so a non-ASCII literal raises
# C4566 ("cannot be represented in the current code page") and /WX makes it fatal. /utf-8 sets
Expand Down Expand Up @@ -75,52 +71,14 @@ else()
# <streambuf>, <stdio2.h>), so no source change can reach them. GCC 16 is clean, which is
# why this went unnoticed locally: the maintainer's GCC is 16 and CI's is 13.
# A version test rather than a flag probe, deliberately: the flags all EXIST here, it is
# their judgement that is wrong, so CheckCXXCompilerFlag would happily confirm them.
# their judgment that is wrong, so CheckCXXCompilerFlag would happily confirm them.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 16)
add_compile_options(-Wno-error=restrict
add_compile_options(-Wno-error=null-dereference
-Wno-error=restrict
-Wno-error=stringop-overflow
-Wno-error=format-truncation)
endif()

# -Wnull-dereference also fires on GCC 16.2.1, past the version bound above, but only for one
# real class of site: a plain `this->member_` read inlined through a lambda/callback chain
# (MoonModule::enabled()'s `enabled_ && !held_`, reached through several tests' std::function
# plumbing) where GCC's analysis loses track of `this` and can't prove it non-null. There is no
# source fix for that shape (testing `this == nullptr` in a member function is not meaningful),
# so it stays unconditional for GCC pending a version where it is confirmed to stop. Two other
# sites GCC flagged under the same warning turned out to be real, provably-safe `rootNode()`
# calls gated on a prior `json::parse()` success and are fixed directly instead (StateDocument.cpp,
# ControlModule.h::readHeader) — this exception covers only the this-across-a-thunk shape now.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_compile_options(-Wno-error=null-dereference)
endif()

# -Warray-bounds= fires on GCC 16.2.1 too: MoonModule::setName's std::memcpy(name_, n, len)
# is bound-checked (len < sizeof(name_), a real fixed array) before the call, but GCC 16.2.1,
# inlining it through several unit tests' DOCTEST_ANON_FUNCs, deduces a 0-byte region for
# name_ at some of those call sites and reports offset 0 as out of [0, 0] bounds — a false
# positive from the same aggressive-inlining-loses-the-real-size class as the others here.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_compile_options(-Wno-error=array-bounds=)
endif()

# -Wmaybe-uninitialized fires on GCC 16.2.1 too: unit_Hub75Driver_pins.cpp reads out[i] only
# for i < n (n itself checked == 14 just above), but GCC 16.2.1 flags out[14]/out[15] anyway,
# past the loop's own dynamic bound — a `fixedPins(out, 16)` return value it isn't correlating
# with the loop condition after inlining, not a real read of memory the loop never reaches.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
add_compile_options(-Wno-error=maybe-uninitialized)
endif()

# Vendored doctest.h always includes <ciso646> on Clang, unconditionally, to detect libc++ via
# _LIBCPP_VERSION (doctest's own long-standing quirk: github.com/doctest/doctest/issues/126, #356).
# A Clang new enough to deprecate that pre-C++20 header turns its #warning into a build failure
# under -Werror. Third-party header code we don't own, so this stays unconditional rather than
# version-gated, matching the MSVC C5285 suppression above for the same vendored file.
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options("-Wno-error=#warnings") # quoted: a bare # starts a CMake comment, truncating the rest of the file
endif()

# Hot-path discipline, enforced by the compiler. MM_NONBLOCKING marks tick/tick20ms/tick1s
# (platform.h); -Wfunction-effects then checks TRANSITIVELY that nothing they reach
# allocates or blocks, through the whole call graph. Clang 20+ only; the flag does not
Expand Down
53 changes: 33 additions & 20 deletions docs/assets/moonwalk.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function elements(model, by, keep = () => true) {
out.push({ data: { id: nid, pid, parent: `col:${column.id}`, label: p.title, kind: "process", actor: p.actor, stepped: steps.length > 0 },
position: { x: x * COL, y } });
steps.forEach((s, k) => {
out.push({ data: { id: `${nid}/${s.id}`, pid, parent: nid, label: s.say, kind: "step", actor: s.actor },
out.push({ data: { id: `${nid}/${s.id}`, pid, sid: s.id, parent: nid, label: s.say, kind: "step", actor: s.actor },
position: { x: x * COL, y: y + (k + 1) * STEP_H } });
if (k) out.push({ data: { id: `next:${nid}/${k}`, source: `${nid}/${steps[k - 1].id}`, target: `${nid}/${s.id}`, kind: "next" } });
});
Expand Down Expand Up @@ -270,14 +270,22 @@ export function doneOnDevice(model, stateDoc) {
return done;
}

// The processes a contributor's runs show done: every step the change triggers has a pass that still holds, and there is at least one. `steps` is what MoonDeck's /api/gates reports per process, each step triggered or not and fresh or not.
// Script steps together, as one process's or one subprocess's: triggered when any is, fresh when every triggered one is and there is at least one.
function together(list) {
const needed = list.filter((s) => s.triggered);
return { triggered: needed.length > 0, fresh: needed.length > 0 && needed.every((s) => s.fresh), running: needed.some((s) => s.running) };
}

// A step's status from MoonDeck's /api/gates (`steps`, per process, each script step triggered or not and fresh or not): its own run, or for a step that runs a subprocess, that subprocess's steps together. Undefined for a step that runs no script.
export function gateOf(steps, pid, s) {
const sub = s.when?.process;
if (sub) return steps?.[sub]?.length ? together(steps[sub]) : undefined;
return steps?.[pid]?.find((r) => r.process === pid && r.id === s.id); // a process's list holds its subprocesses' steps too, keyed by their own process
}

// The processes a contributor's runs show done: every step the change triggers has a pass that still holds, and there is at least one.
export function doneBySteps(steps) {
const done = new Set();
for (const [pid, list] of Object.entries(steps || {})) {
const needed = list.filter((s) => s.triggered);
if (needed.length && needed.every((s) => s.fresh)) done.add(pid);
}
return done;
return new Set(Object.entries(steps || {}).filter(([, list]) => together(list).fresh).map(([pid]) => pid));
}

// A run's duration as a reader takes it in: `42s`, `3m05s`.
Expand Down Expand Up @@ -384,11 +392,9 @@ function stepNote(s, p) {
return notes.length ? ` (${notes.join("; ")})` : "";
}

// A step's call as a reader types it: a MoonDeck script with its arguments, or a command; null for a step that runs nothing.
// A step's call as a reader types it, a MoonDeck script with its arguments, a command or the curl line of an API call, which the generator writes once into the map; null for a step that runs nothing.
export function commandOf(step) {
const r = step.when?.run;
if (!r) return null;
return r.script ? `uv run ${r.script}${r.args ? ` ${r.args}` : ""}` : r.command || null;
return step.command ?? null;
}

// The call as code with a button that copies it. Where the browser keeps the clipboard from an insecure page, as on a device's own http address, the button selects the text for a manual copy.
Expand Down Expand Up @@ -479,7 +485,7 @@ function showPanel(panel, model, pid, done, onDone, host = {}) {
host.offChoice?.(p, chosen); // the map grays out the steps of the options not chosen
// A step that does not apply, for another choice or not needed for this change, is grayed as on the map, or left out when the reader hides them.
const steps = p.steps || [];
const runOf = (s) => host.steps?.[pid]?.find((r) => r.id === s.id);
const runOf = (s) => gateOf(host.steps, pid, s);
const off = steps.map((s) => !applies(s, chosen) || runOf(s)?.triggered === false);
let current = null;
const sectionOf = steps.map((s) => (current = s.section || current));
Expand Down Expand Up @@ -521,8 +527,14 @@ function showPanel(panel, model, pid, done, onDone, host = {}) {
line.append(b);
}
block.append(line);
const how = howTo(s) || linkOf(s, host);
if (how) block.append(how);
// A step that runs a subprocess opens it, as a choice opens a process.
const sub = model.processes[s.when?.process];
if (sub && host.open) {
const into = control("p", { className: "mm-map-next" }, sub.title);
into.addEventListener("click", () => host.open(s.when.process));
block.append(into);
}
for (const how of [howTo(s), linkOf(s, host)]) if (how) block.append(how); // a call shows both: the line to send and its entry on the REST API page
if (s.detail) { const d = control("div"); d.innerHTML = host.helpMarkdown(s.detail); block.append(d); }
view.append(block);
});
Expand All @@ -540,8 +552,7 @@ function showPanel(panel, model, pid, done, onDone, host = {}) {
const li = control("li");
li.append(s.say, stepNote(s, p));
li.dataset.step = s.id;
const how = howTo(s) || linkOf(s, host);
if (how) li.append(how);
for (const how of [howTo(s), linkOf(s, host)]) if (how) li.append(how);
ol.append(li);
}
panel.append(ol);
Expand Down Expand Up @@ -714,7 +725,7 @@ export async function drawMap(host, opts = {}) {
chips.append(chip);
}
// A step that does not apply, for the reader's choice or not needed for this change, leaves the layout when the reader hides them.
const keepStep = (pid, s) => !hideOff.checked || (applies(s, chosenOf(model.processes[pid], opts.choices)) && opts.steps?.[pid]?.find((r) => r.id === s.id)?.triggered !== false);
const keepStep = (pid, s) => !hideOff.checked || (applies(s, chosenOf(model.processes[pid], opts.choices)) && gateOf(opts.steps, pid, s)?.triggered !== false);
const cy = window.cytoscape({ container: canvas, elements: elements(model, by.value, keepStep), style: style(host), layout: { name: "preset" }, minZoom: 0.05, maxZoom: 3, autoungrabify: true }); // the layout is the model's order, so a block taps but never drags
const nodesOf = (pid) => cy.nodes().filter((n) => n.data("pid") === pid);
// A drag that starts on a block pans the map, as one on the background does; a tap without moving still opens the block.
Expand Down Expand Up @@ -748,7 +759,8 @@ export async function drawMap(host, opts = {}) {
});
// A script step the change does not need grays out, and one whose pass still holds is done, over its process's own mark.
cy.nodes("[kind='step']").forEach((n) => {
const r = opts.steps?.[n.data("pid")]?.find((x) => n.id().endsWith(`/${x.id}`));
const s = model.processes[n.data("pid")].steps.find((st) => st.id === n.data("sid"));
const r = gateOf(opts.steps, n.data("pid"), s);
if (r) n.toggleClass("mm-skip", !r.triggered).toggleClass("done", r.fresh);
});
return visible;
Expand Down Expand Up @@ -820,8 +832,9 @@ export async function drawMap(host, opts = {}) {
showPanel(panel, model, pid, done, mark, host_);
fit(target.union(target.children()));
};
host_.open = open;
cy.on("tap", "node[kind='process']", (ev) => { if (!tapped()) return; remember(ev.target.data("pid")); showPanel(panel, model, ev.target.data("pid"), done, mark, host_); });
cy.on("tap", "node[kind='step']", (ev) => { if (!tapped()) return; const pid = ev.target.data("pid"); remember(pid); toStep(pid, ev.target.id().split("/").pop()); });
cy.on("tap", "node[kind='step']", (ev) => { if (!tapped()) return; const pid = ev.target.data("pid"); remember(pid); toStep(pid, ev.target.data("sid")); });

// Where the reader is: the processes touching the cards in view, or the page they came from. A link naming one process opens it; anything else opens on the choices.
const focus = opts.ownsAddress ? new URLSearchParams(location.search).get("focus") || "" : "";
Expand Down
Loading
Loading