Simplification pass: canonical C++23, modern CMake, one form per idea - #34
Merged
Conversation
The -header-filter regex ended with a stray '}', which no header path can
match, so clang-tidy silently skipped every project header while the docs
claimed header enforcement. Fix the regex (and use PROJECT_SOURCE_DIR so
the filter stays correct if the project is ever embedded in a superbuild).
Fix the diagnostics the working filter surfaced:
- give every enum an explicit std::uint8_t base
- use 'override' on derived-interface destructors in pin.hpp
- add missing [[nodiscard]] (Dispatcher::Dispatch, HostUart query methods)
- drop the default argument on the virtual Uart::Receive (no caller used it)
- remove redundant {} member initializers
- rename nlohmann adl_serializer parameters, and suppress, with a stated
reason, the checks that the NLOHMANN_* macro expansions cannot satisfy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/.DS_Store was Finder metadata with no reason to be tracked, and compile_commands.json was a tracked symlink into the gitignored build/ tree — dangling on a fresh clone until the exact 'host' preset was configured, and wrong for every other preset. Ignore both, and have configure recreate the symlink pointing at the build tree configured last, which is what IDE tooling actually wants. Narrow the *.pdf ignore to the one reference book (plus its extracted text) so real documentation can't be silently swallowed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toolchain file set CMAKE_{C,CXX,ASM}_COMPILER with plain set(), which
shadows the cache variables a CMakeUserPresets.json entry provides — so
the documented macOS/Homebrew flow silently fell back to clang-18, which
does not exist there. Guard each compiler assignment with
if(NOT DEFINED ...) so an explicit choice always wins and
HOST_TOOLCHAIN_PATH/-VERSION remain the fallback.
Drop CMAKE_LINKER from the example presets (it names a linker, not the
compiler driver; CMake links through the compiler) and the two custom
tool variables nothing consumed. LLVM_COV_PATH/LLVM_PROFDATA_PATH stay:
the code-coverage module reads them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- external/ held only an empty CMakeLists.txt yet was wired into the build and into three coverage-exclusion patterns - src/libs/board/nrf52832_dk/ contained a single 0-byte CMakeLists.txt and was referenced by nothing - test/ held no targets, only a placeholder comment (now in CLAUDE.md's Testing section) and byte-identical copies of src/'s .clang-format and .clang-tidy - py/CMakeLists.txt was a pure pass-through; the root now adds py/host-emulator directly - remove a '# board.cpp)' remnant and two commented-out statements in host_pin.cpp whose explanation already lives in the adjacent prose Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pyproject.toml pins requires-python >=3.14 and .python-version selects 3.14; a reader following the README's 3.11+ would install a Python that uv sync rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The STM32 CubeMX vendor trees (~4,700 lines of generated C, linker scripts, and startup assembly) were never compiled: the ARM presets cannot configure while only the host MCU backend exists, and the two copies of stm32f3xx_hal_conf.h had already drifted apart. Remove them; git history preserves them for when hardware work starts, and regenerating from CubeMX will be fresher then anyway. What remains is the honest teaching artifact: the ARM toolchain files and hidden configure presets. Configuring an ARM preset now stops with a message naming the unimplemented backend instead of a raw add_subdirectory error, and the broken ARM build/workflow presets that advertised runnable builds are gone. Toolchain cleanups in armgcc.cmake: - move warning policy out (a project decision, not a toolchain one) - fix the CMAKE_CXX_ASM_FLAGS_DEBUG_INIT typo so ARM Debug gets -O0 - drop the undefined LD_FLAGS expansion, the manual CMAKE_CROSSCOMPILING (derived from CMAKE_SYSTEM_NAME), unused tool variables, and commented-out code - add FIND_ROOT_PATH_MODE PROGRAM/LIBRARY so cross builds cannot pick up host programs or libraries Preset cleanups: drop the empty include list, the no-op hidden 'arm' preset, the duplicated test-preset execution block, and the stm32cubef7 dependency declaration nothing could reach. CMAKE_RUNTIME_OUTPUT_DIRECTORY moves from the presets into the top-level CMakeLists.txt where build layout belongs. Docs updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the three ambient mechanisms the build rested on with explicit, target-scoped ones: - COMMON_COMPILE_OPTIONS (pasted into 15 target_compile_options calls) and the directory-scoped include_directories(src) become two INTERFACE targets: project_warnings (warning policy, with generator expressions for the compiler-conditional parts) and project_options (src/ include root, -fno-rtti, links project_warnings). Every target now names its dependency instead of inheriting directory state. - Gating on the invented CMAKE_PRESET name string is gone. EMBEDDED_CPP_MCU and EMBEDDED_CPP_BOARD are declared cache variables defaulting to host, host-only dependencies key off the MCU backend, and tests key off BUILD_TESTING — so a plain 'cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/host-clang.cmake' configures and builds with no preset at all, and inheriting user presets no longer need to restate anything. - The global CMAKE_CXX_FLAGS append of -stdlib=libc++ moves to the host toolchain file as CMAKE_CXX_FLAGS_INIT, the supported home for a global ABI decision (it must reach FetchContent-built dependencies too, which no target property can). Also, in the same spirit: - include(CTest) sits right after project(), so no add_test can be silently discarded; py/ integration tests are added only for host - interface libraries declare their headers via FILE_SET HEADERS (which carries the include root as a usage requirement and readies install rules); the inert LINKER_LANGUAGE properties on them are gone - the venv for the Python tests is now a CTest setup fixture running 'uv sync --frozen' (a no-op when already synced) instead of an execute_process at configure time, so configure is fast and offline; cmake/python_venv.cmake is folded into py/host-emulator/CMakeLists.txt - integration tests locate apps via $<TARGET_FILE:...> instead of hand-building output paths - FetchContent: the deprecated GetProperties/POPULATED dance is a plain MakeAvailable; the misused option(CPPZMQ_BUILD_TESTS) is a proper cache set; the duplicate googletest MakeAvailable and the hand-rolled deprecated GTest::GTest alias are gone (tests link GTest::gtest_main); GIT_TAGs pin commit hashes with the tag name in a comment; ETL is dropped entirely — nothing in src/ used it - sys (the entry point) is an OBJECT library so main.o links into each app directly instead of relying on archive symbol-resolution order - format.cmake uses PROJECT_SOURCE_DIR throughout - the root CMakeLists.txt is organized into labeled sections (setup / dependencies / tooling / targets) Existing build trees need a fresh configure (rm -rf build/) because -stdlib=libc++ now arrives via *_INIT, which only applies to new caches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The encode -> send -> receive -> decode -> status-check exchange was written six times across the three host peripherals, in three different styles: host_pin chained and_then, host_uart mixed a chain with a manual if-cascade inside the last lambda, and host_i2c wrote the whole thing imperatively. In a codebase meant to be copied from, the same protocol step written three ways is three competing house styles. Transact<Response>(transport, request), next to Encode/Decode, is now the single canonical exchange — including folding the emulator's status field into the error channel — and the peripherals reduce to a request literal plus a transform of the response. This also makes GetState honor the response status, which the hand-rolled version forgot. Along the way, in the same files: - Decode takes std::string_view by value (a const& to a view is an anti-idiom, and two callers were building a std::string just to bind the reference), parses via the non-throwing overload, and documents the one remaining catch at the nlohmann boundary; string_view-by-value is now used across Receiver, Dispatcher, and the predicates - Receiver gains the virtual destructor every other interface already had, plus a comment stating its 'unexpected means not mine' contract - request/response literals rely on the message structs' defaulted type and object fields, as host_pin already did - HostPin's input-direction guard moves into SendState, the funnel all three setters share; Toggle becomes the same and_then chain as its neighbours - the Error<->JSON table now covers all twelve enumerators (it silently covered five) and says where its Python mirror lives - the stray semicolon after Encode is gone Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The interfaces promised transfer modes that nothing implements: the host I2C's SendDataDma was byte-identical to SendDataInterrupt (both just invoked the callback on the blocking result), and the host UART's IsBusy/Available/Flush returned constants while SendAsync/ReceiveAsync filled a receive_buffer_ member that nothing ever read. Four tests existed solely to prove the identical wrappers behaved identically. Keep what the host genuinely implements — blocking SendData/ReceiveData on I2C, and Init/Send/Receive/SetRxHandler on UART — and delete the rest, including HostUart's now-unused busy_/callback/buffer state and the async-response half of its dispatcher Receive path. The interface docs and docs/PROJECT_PLAN.md record async/interrupt/DMA modes as future work, to be reintroduced when a hardware platform can implement them with genuinely different behavior worth teaching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three apps were the same file three times: identical headers modulo
the class name, and identical AppMain bodies that discarded the real
error by returning kUnknown — teaching a reader to throw away error
information at exactly the layer that reports it. AppMain is now a
one-line RunApp<App>(board) call (app.hpp) that propagates the first
real error from Init or Run.
Blinky::Run's loop dressed a no-op in monadic ceremony — the status was
reassigned each iteration and never inspected, and its or_else was the
identity on the error channel. It now reads as the two checks it is.
The unreachable 'return {}' after each app's infinite loop is gone.
main() reported failure through iostream and left via exit(), which
skips destructors — so the transport's careful shutdown path never ran
on failure. It now returns (destructors run), reports the actual error
code via std::println, and keeps a documented top-level catch: project
code is exception-free, but the host build links libraries that throw
(cppzmq, nlohmann-json), and main is where an escaped exception becomes
a failing exit code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ReceiverMap paired every receiver with a routing predicate, but the only predicate ever installed — five times over, from three copies of the same IsJson helper — was a JSON sniff that told the dispatcher nothing about which receiver owned the message. The real routing already lived inside each receiver, which decodes the message and checks the addressed name. So the predicate layer was paid for and unused. ReceiverMap is now just the ordered receivers; Dispatch offers the message to each until one claims it (the Receiver contract's 'unexpected means not mine'), and IsJson is gone from all three sites. HostBoard no longer builds its Dispatcher twice: the dispatcher observes the receiver map by reference, so it can be a plain member constructed up front while Init() fills the map after the components exist — which also retires the six-step narration that explained the double construction. The dispatcher tests now exercise the claim contract with a NamedReceiver that accepts only its own messages (storing what it received by value — the old fixture kept a string_view into a dead temporary), the two which-receiver-wins cases collapse into one parameterized test, and the empty do-nothing fixture is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents
ZmqTransport carried ~130 lines of POSIX endpoint arbitration — flock
ownership, an AF_UNIX connect(2) liveness probe, and their rationale —
inline in the class a learner reads to understand 'how does the C++ side
talk to the emulator'. That machinery now lives in its own translation
unit (endpoint_lock.hpp/.cpp) with its prose intact, and ServerThread's
bind phase reads as the three lines of intent it always was: take the
lock, probe for a foreign owner, bind.
TransportConfig is an aggregate again. Its Logger& member forced two
user-provided constructors, which killed designated initializers — the
idiom the rest of the codebase leans on — and both transport test files
had grown a MakeConfig helper with an apologetic comment to work around
it. The member is now a reference_wrapper defaulting to DefaultLogger(),
callers write TransportConfig{.send_timeout = ..., .logger = ...}, and
the workarounds are gone.
Comments that narrated the code's history in past tense are rewritten to
describe present behavior; the history lives here in the log instead.
The 'reduce cognitive complexity' apology over the Log helpers is gone —
they stay on their merits as brevity.
Also: delay.hpp puts #pragma once first, names its parameter duration
(callers pass milliseconds; only the conversion target was micro), and
documents the blocking semantics; transport.hpp loses its empty private
section and gains [[nodiscard]] and an interface comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UART and I2C test fixtures were ~110 near-verbatim duplicated lines each — socket/thread lifecycle, dispatcher/transport wiring, readiness probe, teardown ordering, endpoint cleanup — differing only in the ~25 lines of protocol logic. Any lifecycle fix had to be made twice, and the explanatory comments were already duplicated prose. They now derive from HostPeripheralTest (host_peripheral_test_infra.hpp), which owns all of that and asks a concrete fixture for exactly two things: construct the peripheral (MakeReceiver) and play the emulator's side of one exchange (HandleRequest). The redundant 'ret == 0 then ret <= 0' double-check in the copied poll loops collapses to one condition. test_support.hpp consolidates the rest of the copied scaffolding: - one RecordingLogger (Count + Contains) replaces two byte-identical logger classes that differed only in their query method - MakeEndpoint/EndpointPath/RemoveEndpointArtifacts replace four copies of per-process endpoint naming and of the socket+.lock cleanup loop test_messages now round-trips all six message types through one TYPED_TEST instead of testing one type three times — strictly more coverage (the std::byte adl_serializer path included) in less code — keeps a wire-format pin for the exact JSON the Python emulator parses, and adds the valid-JSON-wrong-shape decode case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three peripheral modules were written by copy-paste: handle_message,
handle_response, and the setter pair were near-identical three times
over, and the 12-line install/restore wait ritual appeared seven times —
already drifted, since the pin and I2C wait handlers chained the
previously-installed hook while the UART ones silently dropped it, so a
nested UART wait lost the outer callback. All of that now lives once in
Peripheral (peripheral.py): message routing keyed on a class-level
OBJECT_TYPE, the request/response hooks, _transact for the blocking
request/reply exchange (pin set/get and uart send_data were the same
sequence three times), and _wait_for(predicate, timeout), which chains
by construction. The seven wait_for_* methods collapse to predicates.
The wire vocabulary — Request/Response, Pin/Uart/I2C, Set/Get/Send/
Receive, and the status codes — is now a set of StrEnums in common.py
that name emulator_message_json_encoder.hpp as their C++ mirror. Status
covers all twelve C++ Error values instead of five; PinDirection uses
the wire spellings Input/Output instead of an IN/OUT that disagreed
with the C++ table; and since a StrEnum serializes as its value, the
fifteen '.name' call sites disappear. PinState/PinDirection follow suit.
Deleted, as called by nothing anywhere: the five name-lookup wrappers on
DeviceEmulator (uart_initialized, get_uart_tx_data, clear_uart_tx_data,
uart_send_to_device, get_pin_state — tests use the typed accessors),
I2C.read_from_device, and set_on_response on all three peripherals.
DeviceEmulator routes through one dict + _dispatch instead of an
if/elif chain feeding three near-identical _handle_* methods, and
detects JSON by parsing it (json.JSONDecodeError) rather than sniffing
braces. A pin Set from the device is now honored only for output pins —
the emulator, not the device, drives inputs — matching the C++ side's
guard.
main() no longer sends a stray 'Hello' probe and blocks on the reply
(scaffolding from before the README pointed users at it); it just runs
the emulator until Ctrl-C, sets up logging there instead of at import
(a library import should not mutate global logging state), and is
installed as the 'host-emulator' console script the README now invokes.
Imports are unconditional throughout: no 'if TYPE_CHECKING' guards, and
no 'from __future__ import annotations'. The guard splits a module into
two import graphs — one the type checker sees, one that exists at
runtime — hiding real dependencies and defeating runtime annotation
introspection, and its legitimate uses (import cycles, heavy optional
dependencies) do not occur in this package. On Python 3.14, which this
project requires, PEP 649's lazily evaluated annotations remove the
runtime-cost rationale for both idioms. ruff's flake8-type-checking
ruleset ('TC') is unselected accordingly — it exists to enforce exactly
that pattern — with the convention recorded in pyproject.toml.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nfig Test cleanups: - the eleven '_ = fixture # ensure X is running' lines become @pytest.mark.usefixtures marks, and the two vacuous 'is not None' asserts on fixtures are gone (a fixture returning None fails at setup) - conftest imports endpoint_path from the package instead of carrying a verbatim copy, types the application-fixture factory instead of returning Any, and reports a missing executable via pytest.fail rather than a bare assert (stripped under python -O) - an autouse fixture uninstalls any on_request/on_response hooks a test left installed — the silent cross-test contamination module-scoped fixtures allow. Buffers stay explicitly managed by the tests that care: some sharing is deliberate (the UART greeting arrives once, at app start), so a blanket reset would be wrong - the I2C address/pattern and UART greeting literals copied from the C++ apps are module constants naming their source of truth Tooling: - pytest-cov is removed: nothing wired it up (coverage is the C++ llvm-cov pipeline), so it was installed into every venv for nothing - mypy gets files = [src, tests], so 'uv run mypy' with no arguments checks both trees — CI previously checked only src while tests/ ran unchecked; CI and CLAUDE.md shorten accordingly - the pythonpath entry gains the one-line comment saying why it exists alongside the editable install DeviceEmulator grows all_peripherals() so tests reset hooks through a public accessor rather than reaching into the routing table. The tests follow the package's no-TYPE_CHECKING convention: imports are unconditional, and conftest's AppFixture alias is a PEP 695 'type' statement (lazily evaluated by design) rather than a quoted alias inside a guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host-debug and host-release compose services were a third naming
layer over workflow presets that already exist and needed extending by
hand for each new workflow; the one embedded-cpp-dev service now takes
the workflow command at run time, which is also how CI invokes it.
CI ran the container as root and then chown'd the coverage output —
repairing the ownership running as root had just broken. It now runs as
the runner's own uid/gid with a writable HOME for uv's cache, so
nothing needs repairing. The compose file documents that UID/GID must
be exported explicitly (bash does not export its UID shell variable, so
the ${UID:-1000} fallback was silently taken everywhere).
The devcontainer no longer retags the image: one tag from one
Dockerfile, so a devcontainer build serves docker compose run too
instead of the two shadowing and rebuilding over each other.
The LLVM major version is declared once as a Dockerfile ARG; format.sh
reads CLANG_FORMAT_MAJOR with the same default, and the remaining
references cross-reference the pin instead of each hardcoding it with
its own reminder comment.
Running CI as a non-1000 uid exposed the seam between the image's two
uses: the devcontainer base image creates /home/vscode with mode 750, so
any uid but vscode's cannot traverse into /home/vscode/workspace at all
(CMake reported the presets file as "not found"; root had been masking
it). Rather than loosen the devcontainer's home directory, CI gets its
own compose overlay -- the batch counterpart of .devcontainer's -- that
mounts the checkout at /workspace and sets a writable HOME. One image,
two uses, each with its own overlay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
py/host-emulator/README.md was a 0-byte file — and the JSON protocol, which is the project's entire teaching point, was documented nowhere: it existed only as string literals split across the C++ encoder and three Python modules. The README is now the canonical protocol document (envelope, per-object operation matrix, field sets, status codes) and states explicitly that the C++ and Python vocabularies mirror each other. CLAUDE.md points at it instead of restating the contribution steps without the contract. PROJECT_PLAN.md described a slightly different project than the one in the repo: I2C was simultaneously '#1 priority, stub needs completion' and marked complete; i2c_demo was missing from the shipped-apps list; milestone 2 targeted the Nucleo but its success criterion named the Discovery; the test-count claim was arithmetically wrong on its face and stale besides. The plan is now forward-looking only — milestones, priorities, open debt, decision log — and defers current status to the README and 'ctest -N', which cannot go stale. The status that was tracked in five overlapping places now lives in one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nehalkpatel
force-pushed
the
refactor/simplification
branch
from
August 29, 2026 16:54
e153dfb to
0f1f962
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A codebase-wide simplification pass against the project's stated goals: well structured, easy to follow, canonical constructs, modern C++23, modern CMake (Professional CMake 21st ed. as the reference), and docs that match behavior. Quality only — no functional features. 17 commits, each leaving the build green; both
host-debugandhost-releaseworkflows pass from a clean tree.Credibility fixes (docs vs. reality)
-header-filterregex ended with a stray}no path could match, so every project header went unanalyzed while the docs claimed enforcement. Fixing it surfaced a wave of real diagnostics (enum bases, missingoverride/[[nodiscard]], a default argument on a virtual), all fixed.host-clang.cmakeno longer silently overrides compilers chosen inCMakeUserPresets.json, so the documented macOS/Homebrew flow can work.src/.DS_Store, and thecompile_commands.jsonsymlink into gitignoredbuild/(dangling on fresh clones) — the link is now regenerated at configure time.Build system: modern CMake core
project_options/project_warningsINTERFACE targets replaceCOMMON_COMPILE_OPTIONSpasted into 15 call sites and the single directory-scopedinclude_directories()the entire include graph rested on.CMAKE_PRESETname-string gating is gone;EMBEDDED_CPP_MCU/EMBEDDED_CPP_BOARDare declared cache variables defaulting to host, socmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain/host-clang.cmakeconfigures and builds. Presets are a convenience layer again, and user presets no longer restate anything.GetProperties/POPULATEDdance,GTest::gtest_maininstead of a hand-rolled deprecated alias, misusedoption()fixed; ETL dropped (nothing used it).enable_testing()right afterproject(); the Python venv is a CTest setup fixture (uv sync --frozen, a no-op once synced) instead of network I/O at configure time; integration tests locate apps via$<TARGET_FILE:...>.FILE_SET HEADERS; the entry point is an OBJECT library somain.odoesn't rely on archive symbol-resolution order.-O0).C++: one canonical form per idea
Transact<Response>(transport, request): the encode→send→receive→decode→status-check exchange was written six times in three competing styles across the host peripherals; it's now one monadic helper and the peripherals reduce to a request literal plus a transform.IsJson) is gone, andHostBoardno longer constructs its dispatcher twice.RunApp<App>helper; real errors propagate tomain(), which reports viastd::printlnand returns (destructors run) instead ofexit().endpoint_lock.*;TransportConfigis an aggregate again (designated initializers work; both test workarounds deleted); history-tense comments rewritten to present tense with the history moved to the commit log.Tests
HostPeripheralTestinfra +test_support.hppdelete ~300 duplicated fixture lines (two identical recording loggers, four copies of endpoint naming/cleanup, ~110-line copied UART/I2C fixtures).test_messagesround-trips all six message types via oneTYPED_TEST— less code, strictly more coverage, including the customstd::byteserializer.Python emulator
Peripheralclass replaces ~250 copy-pasted lines across pin/uart/i2c — fixing a real drift where UART's wait handlers silently dropped a previously installed hook.StrEnums incommon.pyexplicitly mirroring the C++ encoder;Statuscovers all twelve error values (was five);PinDirectionuses the wire spellings.uv run host-emulatoris a proper console script; imports are unconditional (if TYPE_CHECKINGandfrom __future__ import annotationsremoved as project convention — PEP 649 on Python 3.14 obsoletes the rationale; ruff'sTCruleset unselected accordingly).@pytest.mark.usefixtures, shared protocol constants naming their C++ source of truth, and an autouse hook-reset; mypy now coverstests/too (uv run mypy), and unused pytest-cov is gone.CI / Docker / docs
chown; the devcontainer shares the image tag instead of rebuilding a second one; the LLVM pin is declared once.py/host-emulator/README.md(was 0 bytes) is now the canonical wire-protocol document — envelope, per-object operation matrix, field sets, status codes, and the mirror rule.docs/PROJECT_PLAN.mdis forward-looking only; its self-contradictions (I2C both "Bump black from 24.2.0 to 24.3.0 in /py/host-emulator #1 priority stub" and complete, wrong board names, stale test counts) are resolved, with a decision-log entry for this pass.Notes for reviewers
rm -rf build/:-stdlib=libc++now arrives viaCMAKE_CXX_FLAGS_INITin the toolchain file, which only applies to fresh caches.🤖 Generated with Claude Code