CI now mirrors the local dev path. The workflow installs mise,
trusts the config, installs all mise-pinned tools (mdbook, just),
and invokes `just build-site` for the whole multi-version build.
There is no longer any build logic inside the workflow YAML.
Changes:
- `mise.toml` now also pins `just = "1.50.0"` so `mise install`
brings in both mdbook and just.
- `scripts/build-site.sh` (new, POSIX sh, shellcheck-clean) takes
the multi-version build loop out of the workflow and parks it
next to the rest of the build tooling. Iterates v* tags via
`git worktree`, sets `MDBOOK_OUTPUT__HTML__SITE_URL` per
version, emits `_site/versions.json` and `_site/index.html`
(root redirect to /latest/).
- `justfile` gains a `build-site` recipe — `sh scripts/build-site.sh`.
- `.github/workflows/mdbook.yml` is now four short steps inside
the build job: jdx/mise-action (install: false) → `mise trust`
→ `mise install` → `just build-site`. The previous inline
multi-line bash block is gone.
- `.gitignore` adds `/_site/` alongside the existing `/book/`.
Local smoke test: `just build-site` with no v* tags produces
`_site/{latest/, versions.json=["latest"], index.html}` exactly as
CI will. Future `v0.1.0` tag will add `_site/v0.1.0/` and update
`versions.json` without any code changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three intertwined changes — split conceptually but landed together
because they share the same workflow rewrite:
1. MULTI-VERSION SITE. The published site now keeps every prior spec
version alongside the rolling latest build (docs.rs / readthedocs
pattern):
- .github/workflows/mdbook.yml builds latest from current HEAD and
each v* tag in a git worktree, placing each into _site/<ver>/.
Each per-version build gets MDBOOK_OUTPUT__HTML__SITE_URL set to
/meshcore-spec/<ver>/ so the sitemap/canonical links resolve
correctly per version.
- Workflow emits _site/versions.json (newest first) and a root
index.html that redirects to /latest/.
- theme/version-picker.js is a small client-side dropdown that
fetches /<base>/versions.json and lets the reader switch to the
same chapter in any other version. Slots into mdbook's menu bar
(.right-buttons); falls back to a fixed top-right widget if the
menu isn't found.
- theme/version-picker.css styles it to fit the navy theme.
- book.toml wires the picker files via additional-js /
additional-css; site-url defaults to /meshcore-spec/latest/ for
the main build (CI overrides per tag).
- Old tags built without these files render without a picker, which
is fine — users won't typically need to navigate AWAY from an
older version. v0.1.0 onward will all carry the picker.
Spec versions use independent semver and do NOT mirror MeshCore
firmware versions; each spec release records the upstream commit
hash it was validated against in CHANGELOG.md.
2. MISE-PINNED TOOLS. mise.toml pins cargo:mdbook so local dev and
CI use the same version. The workflow installs everything via
jdx/mise-action instead of peaceiris/actions-mdbook, making
mise.toml the single source of truth for tool versions.
3. JUSTFILE. justfile gives short names (build / serve / clean /
validate / setup) for the common tasks. Recipes run under
`mise exec -- sh -c …` so they always pick up the mise-pinned
tools (sh is intentional, keeping recipes POSIX-compatible).
Documentation: README's spec link now mentions the versioning model;
src/00-overview.md gets a "Spec Versions" subsection explaining the
URL layout and version-picker.
Verified locally: `just build` produces a clean book/ with the
version-picker JS/CSS injected (hashed filenames, slotted into
theme/). JS parses clean under `node --check`.
Note for first-time deploy: GitHub Pages source must be set to
"GitHub Actions" in repo settings (one-time, manual). After that,
push to main builds + deploys latest, and pushing a v* tag adds
the snapshot to the site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The book's first page previously claimed the spec was "the
authoritative definition of the wire format" derived from the
firmware. That misrepresents the project's stance. Replace the
existing "Status" block with a more honest "Status and Provenance"
block that mirrors the README disclosure:
- This is an unofficial, independent project — NOT part of the
upstream MeshCore project, not affiliated, not endorsed.
- Primarily AI-authored (Claude / Opus 4.x) under Stephen Waits's
direction, with human proofreading and editing.
- The upstream MeshCore C++ firmware is the source of truth;
this spec is downstream. CHANGELOG records the upstream commit
hash per pass.
- DongLoRa observations refine the spec but never override
upstream.
- The corpus is a useful signal but not a substitute for
cross-checking against upstream firmware.
Visible to anyone landing on https://swaits.github.io/meshcore-spec/
or reading the rendered overview chapter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Set up the protocol spec as an mdBook and add a GitHub Actions
workflow that builds it on every push/PR and deploys to GitHub
Pages on main. The repo is now both a machine-readable corpus and
a browsable, searchable site.
Structure changes (one-time, mechanical):
- Rename the spec source directory from `spec/` to `src/` to match
mdBook's default convention. All 21 chapter files move; their
internal relative cross-links are unchanged.
- Add `src/SUMMARY.md` as the mdBook table of contents, grouping
chapters under Wire Format, Payloads, Cryptography & Identity,
and Routing & Higher-Layer Protocols.
- Add `book.toml` at repo root: title, author, search enabled,
fold enabled, edit-on-GitHub links, navy theme.
- Add `.github/workflows/mdbook.yml`: build on push + PR, deploy
on push to main only (configure-pages + upload-pages-artifact +
deploy-pages from actions/).
- Add `/book/` to .gitignore.
Reference updates so the old path is fully retired:
- Every `spec_ref` in `corpus/**/*.json` rewritten from `spec/` to
`src/` (54 files).
- Schema description text and example path in
`corpus/schema/test-vector.schema.json`.
- `README.md`: prominent link to the published site near the top;
"What's Here" updated for the new layout.
- `CONTRIBUTING.md`: spec-editing instructions point at `src/`
and note the SUMMARY.md requirement.
- `CLAUDE.md`: directory-layout notes updated.
CHANGELOG.md historical entries intentionally keep their `spec/`
paths since those record what the paths were at the time of those
commits.
After this lands, the user needs to enable GitHub Pages in repo
settings with source = "GitHub Actions" (one-time, manual). The
workflow will then publish to https://swaits.github.io/meshcore-spec/
on every push to main.
Verified locally: `mdbook build` produces a clean book/ tree with
all 21 chapters rendered and the SUMMARY-driven sidebar populated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Record this pass at the top of CHANGELOG.md:
- Upstream baseline: meshcore-dev/MeshCore @ 1a7b361 (2026-04-28).
Every normative claim was cross-checked against this commit.
- Process: adopted conventional commits; established
"upstream-firmware-is-the-golden-source-of-truth" as a load-bearing
principle for the project.
- Added: ack-crc test vectors; explicit ACK CRC algorithm; attempt
semantics including tail-byte encoding for attempt > 3; reliable
DM delivery; multipart direct-only and finalizer plain-ACK;
sender-behavior/transmission-priority section in routing; README
provenance + Author.
- Clarified: AES plaintext zero-padding scheme; MULTIPART chain
descending count + plain finalizer.
- Corrected: 17-routing previously misrepresented upstream's
path-replacement as a hash-size/SNR heuristic; upstream actually
replaces unconditionally.
- Out of scope: ai-bot-specific values (route TTL, SNR margin,
1-byte hash rejection, 150ms PATH_RETURN, 0x1424 sync word,
pending-ACK 64/125s) were considered and dropped since they are
not upstream behavior. Radio/hardware-state firmware items also
excluded as out of protocol scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prepare the repo for public release by stating openly:
- The spec is AI-authored (Claude / Anthropic) under Stephen Waits's
direction. It is an independent project, not affiliated with the
upstream MeshCore project.
- The official MeshCore C++ firmware is the golden source of truth for
all protocol behavior; when this spec and upstream disagree, upstream
wins.
- Clarifications have been driven by observations from building
DongLoRa firmware and apps against live deployments, but DongLoRa
divergences from upstream are treated as DongLoRa bugs, not spec
changes.
- Conformance against this corpus is a useful signal but not a
substitute for cross-checking against upstream.
Tighten Project Status to reference the new section. Add an Author
section with attribution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes:
1. CORRECTION. The path-learning subsection previously described the
reference implementation's path-replacement as "replace only if
the new path's hash size is not smaller and SNR is not materially
worse" — but upstream's BaseChatMesh::onContactPathRecv() does an
UNCONDITIONAL replace, with an inline comment flagging hash-size/
SNR selection as future work. The spec now reflects upstream's
actual behavior and notes that quality heuristics are an
implementation extension, not a reference-implementation behavior.
Likewise, removed the implication that the reference implementation
has a cached-path TTL — it doesn't.
2. ADDITION. Document PATH_RETURN scheduling delay
(TXT_ACK_DELAY=200ms default in upstream BaseChatMesh.cpp:9), with
the rationale of spacing PATH_RETURN, ACK, and reply across
distinct slots.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(BaseChatMesh.cpp:304-307 unconditional replace; cpp:228, 255
TXT_ACK_DELAY-spaced PATH_RETURN emission).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clarify that the MULTIPART ACK chain terminates with a plain
PAYLOAD_TYPE_ACK (not a MULTIPART with remaining=0). For K extra
copies, the wire sequence is:
MULTIPART(remaining=K), MULTIPART(remaining=K-1), ...,
MULTIPART(remaining=1), then PAYLOAD_TYPE_ACK (plain).
All copies share the same ack_crc. Receivers MUST dedup at the
application layer by ack_crc, since each packet's hash differs.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(Mesh::routeDirectRecvAcks, src/Mesh.cpp:362 — while-loop emits
MULTIPART(remaining=extra...1), then createAck(crc) plain ACK).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten the encryption steps to spell out:
- Plaintext is zero-padded on the right to a multiple of 16 bytes
before AES-128-ECB encryption.
- The padding is NOT PKCS#7 (not unambiguous); the receiver must
disambiguate trailing zeros using the inner plaintext format
(e.g., for TXT_MSG, by C-string NUL termination scan).
- Renumber steps so the padding step is explicit and ordered before
encryption, instead of being a parenthetical on the encrypt step.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(Utils::encrypt / Utils::decrypt, src/Utils.cpp:30-, comment
"dest may contain trailing zero bytes in final block").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Sender Behavior (Informational)" section to Section 17 that
captures the cross-implementation conventions for how senders should
behave above the wire format:
- Path Learning: cache the reversed accumulated path from received
flood-routed packets; replace only when the new path is at least as
good (hash size, SNR threshold implementation-defined).
- PATH_RETURN: proactive emission lets the original sender switch to
direct routing without waiting for an explicit path probe.
- Flood Fallback and Retries: track outstanding DMs by expected
ack_crc, increment attempt on each retry, hold timestamp constant,
fall back from direct to flood after N unacknowledged direct
retries.
- Transmission Priority (sender-originated) table: ACK > PATH >
direct reply > flood reply > req/resp > group > advert.
All of these are informational guidance; numeric tunings remain
implementation-defined. Complements the existing forwarding-priority
table.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(BaseChatMesh send/recv flow, Mesh::createPathReturn,
handleReturnPathRetry).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Multipart ACK Usage Constraints" subsection making explicit
what upstream enforces in code but the spec previously left implicit:
- MULTIPART-wrapped ACKs MUST only be emitted on direct-routed return
paths. When no direct path is known, the sender MUST fall back to a
plain PAYLOAD_TYPE_ACK via flood. Matches sendAckTo() and
routeDirectRecvAcks() in upstream.
- All MULTIPART and plain copies of the same ACK carry an identical
4-byte ack_crc; receivers MUST dedup by CRC at the application
layer (Section 16 packet-hash dedup does not catch them because the
wrapper byte makes each copy hash-distinct).
- Typical inter-copy delay ~300 ms plus the direct retransmit delay;
exact count via getExtraAckTransmitCount() (reference default 0).
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(Mesh::routeDirectRecvAcks, BaseChatMesh::sendAckTo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additions to the TXT_MSG section:
1. Attempt Counter Semantics. Spells out that bits 0-1 of
txt_type_attempt carry attempt & 3, the input the ACK CRC depends
on. For attempt > 3, upstream appends a [NUL][attempt_full] tail
after the text (BaseChatMesh.cpp:410-412), recovering the full
attempt value at the cost of 2 bytes of text capacity. The tail is
NOT part of the ACK CRC input, so attempt=4 and attempt=0 collide
on CRC and senders relying on >3 must verify the trailing byte.
2. Reliable DM Delivery. Documents the sender-side ACK tracking
contract: maintain a pending-ACK table keyed by expected CRC,
retry with attempt incremented, consume MULTIPART-wrapped or plain
ACKs identically, and the receive-side option to cache replies per
peer for duplicate-DM loss recovery.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(BaseChatMesh::composeMsgPacket, onPeerDataRecv TXT_MSG handling).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spell out the SHA-256-truncation algorithm previously described in
prose:
ack_crc = SHA-256(plaintext_prefix || sender_pub_key)[0..3]
where plaintext_prefix = timestamp(4) || txt_type_attempt(1) || text
(excluding any trailing NUL terminator). Add the SIGNED_PLAIN variant
(includes 4-byte signature prefix, hashes against receiver's pub_key).
Reference the upstream functions that implement each side.
Resolves the ACK CRC vagueness called out in the 2026-04-01 audit.
Paired with the new corpus/crypto/sha256/ack-crc.json vectors.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(BaseChatMesh.cpp:222, 249, 407; Utils::sha256 two-fragment form).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add four SHA-256-truncation vectors covering ACK CRC computation with
incrementing attempt values (0..3) over a stable (timestamp, text,
sender_pub_key) triple. Demonstrates that each retry produces a
distinct expected CRC, fixing the algorithmic ambiguity noted in the
2026-04-01 audit.
Algorithm: SHA-256(timestamp(4) || txt_type_attempt(1) || text ||
sender_pub_key(32))[0..3] as little-endian uint32.
Validated against upstream meshcore-dev/MeshCore @ 1a7b361
(BaseChatMesh::composeMsgPacket, src/helpers/BaseChatMesh.cpp:407).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>