shared/message_keys.json plus C and Kotlin codegen (#7) #94

Merged
robert merged 1 commit from tooling/message-keys-codegen into main 2026-09-04 16:56:42 +02:00
Owner

Summary

Implements #7: shared/message_keys.json plus C and Kotlin codegen, generated from docs/PROTOCOL.md
sections 2-5 (not docs/PLAN.md — D28, the 2026-09-01 correction), including all three of the issue's
update comments (protocol version + CONFIG_PAGES; PLAN.md to PROTOCOL.md as the source; per-type
sentinels, STATE_ACK/CMD_TIME, SCREEN_W/H/SHAPE, NAV_REJOIN_BEARING/M, the renumbered MAP_*
keys, and HR_SAMPLES' actual (uint8 dt, uint8 bpm) pair format).

What's here

  • shared/message_keys.json — every key in PROTOCOL.md sections 2-3 (52 keys), the current
    protocol_version, per-type sentinels, and PROTOCOL.md section 4's update-rate policy as
    heartbeat_ms/period_ms per group.
  • tools/gen_message_keys.py — stdlib-only Python (no third-party deps), run via uv run so CI
    needs nothing baked into any Docker image. generate writes both outputs; check regenerates into
    memory and diffs against what's checked in (used by CI). Fails loudly (ContractError) on a
    duplicate key id or a gap in a group's id sequence — PROTOCOL.md deliberately leaves gaps between
    groups (1-6, 10-24, 30-33, ...) but never within one, so the gap check is scoped per group.
  • tools/test_gen_message_keys.py — 8 tests, including the duplicate-id/gap acceptance criterion
    against deliberately broken fixtures, plus a check that the real committed contract validates clean.
  • tools/check_field_id_coverage.py + tools/test_field_id_coverage.py — cross-checks
    watchapp/src/c/fields.h's FieldId enum against the contract (NFR-C10 / the issue's own added
    criterion): every FieldId has a wire key or is documented watch-local, no wire key names a
    FieldId that doesn't exist.
  • watchapp/src/c/proto.h and companion/pebble/.../Proto.kt — generated output, checked in.
  • ride_link.c's four hand-copied WIRE_KEY_* #defines (added in #11 because no generated header
    existed yet) are replaced with the generated PROTO_KEY_* constants — exactly what #11's own
    closing comment asked for.
  • state.h's RideCmd enum called its own numbering "an interpretation, not a documented fact",
    because PROTOCOL.md gave CMD's value vocabulary ("start / pause / resume / ...") as prose only,
    never a numbered table. Closed by adding a numbered CMD value table to PROTOCOL.md section 3 that
    matches RideCmd's existing values exactly, rather than inventing a different numbering — nothing
    has shipped to a device yet, so the already-implemented ordering became the documented fact (D48:
    reopened with a checked fact, not a redesign argument). shared/message_keys.json does not
    generate these values itself — they're a key's payload vocabulary, not a message key, and stay out
    of this contract's scope. No mismatch was found or introduced; flagging this per the task's request
    to check for one.

fast-lane.yml

The lint-and-secrets job's "generated-code freshness (G5, C5, C10)" step was a placeholder that
printed a notice and exited once shared/message_keys.json existed. It's now real:
tools/gen_message_keys.py check (regenerate + diff, the actual NFR-C5(a) gate) plus
tools/check_field_id_coverage.py, and a new step runs both tools' unit tests. Runs via uv run
(installed with the same curl command tooling/docker/pebble-toolchain/Dockerfile already uses for
pebble-tool) rather than requiring a ci-tools image rebuild+push — that image has no python3, and
rebuilding/pushing it is documented as a manual, occasional step Robert runs locally
(tooling/docker/README.md), not something this PR should trigger as a side effect.

Verified for real

  • pebble build succeeds for emery, gabbro and basalt with the generated proto.h actually
    compiled into ride_link.c.
  • watchapp/tests/* (hand-compiled with gcc, no cmake binary in this sandbox): all 96 assertions
    pass across test_fields/test_page/test_state/test_page_render_geometry — no regression.
  • :companion:pebble:compileDebugKotlin and :companion:pebble:assembleDebug both succeed with the
    generated Proto.kt, using the real Android SDK + JDK 21 (ANDROID_HOME/JAVA_HOME per tonight's
    setup).
  • :companion:core:test passes clean.
  • tools/test_gen_message_keys.py + tools/test_field_id_coverage.py: 13 tests pass.

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

## Summary Implements #7: `shared/message_keys.json` plus C and Kotlin codegen, generated from `docs/PROTOCOL.md` sections 2-5 (not `docs/PLAN.md` — D28, the 2026-09-01 correction), including all three of the issue's update comments (protocol version + `CONFIG_PAGES`; PLAN.md to PROTOCOL.md as the source; per-type sentinels, `STATE_ACK`/`CMD_TIME`, `SCREEN_W/H/SHAPE`, `NAV_REJOIN_BEARING/M`, the renumbered `MAP_*` keys, and `HR_SAMPLES`' actual `(uint8 dt, uint8 bpm)` pair format). ## What's here - `shared/message_keys.json` — every key in PROTOCOL.md sections 2-3 (52 keys), the current `protocol_version`, per-type sentinels, and PROTOCOL.md section 4's update-rate policy as `heartbeat_ms`/`period_ms` per group. - `tools/gen_message_keys.py` — stdlib-only Python (no third-party deps), run via `uv run` so CI needs nothing baked into any Docker image. `generate` writes both outputs; `check` regenerates into memory and diffs against what's checked in (used by CI). Fails loudly (`ContractError`) on a duplicate key id or a gap in a group's id sequence — PROTOCOL.md deliberately leaves gaps *between* groups (1-6, 10-24, 30-33, ...) but never *within* one, so the gap check is scoped per group. - `tools/test_gen_message_keys.py` — 8 tests, including the duplicate-id/gap acceptance criterion against deliberately broken fixtures, plus a check that the real committed contract validates clean. - `tools/check_field_id_coverage.py` + `tools/test_field_id_coverage.py` — cross-checks `watchapp/src/c/fields.h`'s `FieldId` enum against the contract (NFR-C10 / the issue's own added criterion): every `FieldId` has a wire key or is documented watch-local, no wire key names a `FieldId` that doesn't exist. - `watchapp/src/c/proto.h` and `companion/pebble/.../Proto.kt` — generated output, checked in. ## ride_link.c / state.h reconciliation - `ride_link.c`'s four hand-copied `WIRE_KEY_*` `#define`s (added in #11 because no generated header existed yet) are replaced with the generated `PROTO_KEY_*` constants — exactly what #11's own closing comment asked for. - `state.h`'s `RideCmd` enum called its own numbering "an interpretation, not a documented fact", because PROTOCOL.md gave `CMD`'s value vocabulary ("start / pause / resume / ...") as prose only, never a numbered table. Closed by adding a numbered `CMD` value table to PROTOCOL.md section 3 that matches `RideCmd`'s existing values exactly, rather than inventing a different numbering — nothing has shipped to a device yet, so the already-implemented ordering became the documented fact (D48: reopened with a checked fact, not a redesign argument). `shared/message_keys.json` does not generate these values itself — they're a key's *payload* vocabulary, not a message key, and stay out of this contract's scope. No mismatch was found or introduced; flagging this per the task's request to check for one. ## fast-lane.yml The `lint-and-secrets` job's "generated-code freshness (G5, C5, C10)" step was a placeholder that printed a notice and exited once `shared/message_keys.json` existed. It's now real: `tools/gen_message_keys.py check` (regenerate + diff, the actual NFR-C5(a) gate) plus `tools/check_field_id_coverage.py`, and a new step runs both tools' unit tests. Runs via `uv run` (installed with the same curl command `tooling/docker/pebble-toolchain/Dockerfile` already uses for pebble-tool) rather than requiring a `ci-tools` image rebuild+push — that image has no `python3`, and rebuilding/pushing it is documented as a manual, occasional step Robert runs locally (`tooling/docker/README.md`), not something this PR should trigger as a side effect. ## Verified for real - `pebble build` succeeds for **emery, gabbro and basalt** with the generated `proto.h` actually compiled into `ride_link.c`. - `watchapp/tests/*` (hand-compiled with `gcc`, no `cmake` binary in this sandbox): all 96 assertions pass across `test_fields`/`test_page`/`test_state`/`test_page_render_geometry` — no regression. - `:companion:pebble:compileDebugKotlin` and `:companion:pebble:assembleDebug` both succeed with the generated `Proto.kt`, using the real Android SDK + JDK 21 (`ANDROID_HOME`/`JAVA_HOME` per tonight's setup). - `:companion:core:test` passes clean. - `tools/test_gen_message_keys.py` + `tools/test_field_id_coverage.py`: 13 tests pass. https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
shared/message_keys.json plus C and Kotlin codegen (#7)
Some checks failed
dev-artifact / build-pbw (push) Failing after 0s
dev-artifact / build-apk (push) Failing after 0s
dev-artifact / publish (push) Has been skipped
fast-lane / host-c-tests (push) Failing after 0s
fast-lane / jvm-tests (push) Failing after 0s
fast-lane / pebble-build (push) Failing after 0s
fast-lane / lint-and-secrets (push) Failing after 0s
fast-lane / meta-declares-required-jobs (push) Failing after 0s
fast-lane / host-c-tests (pull_request) Failing after 0s
fast-lane / jvm-tests (pull_request) Failing after 0s
fast-lane / pebble-build (pull_request) Failing after 0s
fast-lane / lint-and-secrets (pull_request) Failing after 0s
fast-lane / meta-declares-required-jobs (pull_request) Failing after 0s
1b60592ada
Generates watchapp/src/c/proto.h and companion/pebble's Proto.kt from a
single shared/message_keys.json contract, per the issue's three update
comments:

- Every key in docs/PROTOCOL.md sections 2-5 (the 2026-09-01 correction:
  generated from PROTOCOL.md, not the superseded PLAN.md table, D28),
  including PROTO_VERSION, CONFIG_PAGES/CONFIG_SEQ, HR_SAMPLES' actual
  (uint8 dt, uint8 bpm) batch format, NAV_CUE_CONFIDENCE, STATE_ACK/
  CMD_TIME, SCREEN_W/H/SHAPE, NAV_REJOIN_BEARING/M and the renumbered
  MAP_* keys (2026-09-02 update).
- Per-type unavailable sentinels (0xFF/0xFFFF/0xFFFFFFFF), derived from
  each key's wire type rather than hand-entered per key.
- PROTOCOL.md section 4's update-rate policy generated as heartbeat/
  period-ms constants, per group, not left as prose.
- tools/gen_message_keys.py fails loudly (ContractError, with a test) on
  a duplicate key id or a gap in a group's id sequence.
- tools/check_field_id_coverage.py cross-checks fields.h's FieldId enum
  against the contract (NFR-C10): every FieldId has a wire key or is
  documented watch-local, and no wire key names a FieldId that doesn't
  exist.

ride_link.c's four hand-copied WIRE_KEY_* defines (added in #11 because no
generated header existed yet) are replaced with the generated PROTO_KEY_*
constants, as #11's own closing comment asked for.

state.h's RideCmd enum called its own numbering "an interpretation, not a
documented fact", since PROTOCOL.md gave CMD's value vocabulary as prose
only. Closed by documenting a numbered CMD-value table in PROTOCOL.md
section 3 that matches RideCmd's existing values exactly — nothing has
shipped to a device, so the already-implemented ordering became the fact
rather than a new argument overriding it (D48). message_keys.json does not
generate these values itself: they are a key's payload vocabulary, not a
message key, and out of this contract's scope.

.forgejo/workflows/fast-lane.yml's "generated-code freshness" step is now a
real regenerate-and-diff gate (tools/gen_message_keys.py check) plus the
FieldId coverage check, run via `uv run` so the step needs nothing baked
into the ci-tools image — uv self-provisions Python, the same install
command tooling/docker/pebble-toolchain/Dockerfile already uses.

Verified for real, not just reviewed:
- `pebble build` succeeds for emery/gabbro/basalt with the generated
  proto.h compiled into ride_link.c.
- watchapp/tests/* (hand-compiled with gcc, no cmake binary in this
  sandbox) all pass: 96 assertions across test_fields/test_page/
  test_state/test_page_render_geometry.
- `:companion:pebble:compileDebugKotlin` and `:companion:pebble:
  assembleDebug` both succeed with the generated Proto.kt, using the real
  Android SDK + JDK 21.
- `:companion:core:test` passes clean (no regression).
- tools/test_gen_message_keys.py and tools/test_field_id_coverage.py (13
  tests) pass, including the duplicate-id/gap-in-sequence acceptance
  criterion.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit a65255f585 into main 2026-09-04 16:56:42 +02:00
Sign in to join this conversation.
No description provided.