Feature model: manifest, generator, gates, and the P2 ratchet - #168
Merged
Merged
Conversation
The firmware is switchable today through ~11 hand-maintained -D flags, per-environment source filters and a web-page cut, described in prose across platformio.ini and seven analysis documents. docs/analysis/MODELO_DE_RECURSOS.md lays out a model that turns that into a single source; this is its P1 foundation: tools/features.toml the manifest: what each firmware profile turns on tools/gen_features.py derives tools/generated/profiles.ini and a JSON model tools/check_features.py proves the derived environments reproduce today's check_features.py resolves both the hand-written platformio.ini and the generated profiles with PlatformIO's own resolver (pio project config) and asserts, per firmware environment, the same compiler flags, the same compiled translation units (the filter resolved against src/), the same lib_ignore, custom_web_omit, custom_fs_pages and build_type. All six match: release/test/test_https/asserts 67 units, alpha/air 57. The gate can fail — a mutation to the manifest is caught, including its propagation through `extends`. Byte-identical build proof: each of the six images, built from platformio.ini and from the generated profiles (pio run -c, clean both sides), produces an identical firmware.bin sha256. The generated filter order was written to match today's, so link order (which has cost 427 B in this project) does not move the image. platformio.ini and src/ are untouched: this is the fidelity proof that lets the switchover happen next, not the build source yet. Both checks run in the CI gates job. SIMUT_FACTORY_RESET is modelled as-is (a dead flag, noted by the gate); P0 removes it from both sides, byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
platformio.ini now includes tools/generated/profiles.ini via extra_configs and no longer defines [env:pico_w_*]; it keeps only pico_base and the native_* test environments. The six firmware environments are the ones the manifest (tools/features.toml) describes, generated by tools/gen_features.py. The per-profile rationale that lived in the removed env blocks — the measured byte counts, why mDNS is off in test, why test_https diets three pages, why the Air carries the full CLI since the v2.4.9-beta diet — moved into the manifest beside each profile. The note on why there is no debug environment stayed in platformio.ini. Proven byte-identical: each of the six firmware.bin built from the switched platformio.ini has the same sha256 as the image built before the switch (clean builds, recorded baselines). The generated filter order was written to match the hand-written one, so link order does not move the image. `pio test -e native` still passes 185/185 — the native environments are untouched. check_features.py keeps its meaning: it now compares the profiles the build uses (the committed profiles.ini, included via extra_configs) against the manifest generated on the fly, so it catches drift between the manifest, the generated file and the build. gen_features.py --check keeps the generated files in sync. This completes P1 of docs/analysis/MODELO_DE_RECURSOS.md except simut_features.h (feature macros as a generated header for the code's #if), which belongs with the P2 seams. SIMUT_FACTORY_RESET is still modelled (a dead flag) to keep the image identical; P0 removes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
SIMUT_FACTORY_RESET was set by release/test/test_https (and asserts, via release) and read nowhere in src/ — check_features flagged it and the survey in docs/analysis/MODELO_DE_RECURSOS.md had already found it. Removed from the manifest; after the P1 switchover the flag reached the build through the generated profiles.ini, so the manifest is the only place left to remove it. All six firmware.bin rebuild byte-identical to before, which confirms the image never read the flag — removing a live -D would have moved the image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
…iets (P0) Three comments disagreed with the measured numbers the project itself records, which the house treats as a defect: - src/simut_config.h called SIMUT_BLUETOOTH "BLE UART, ~22 KB". It is classic SPP (SerialBT/RFCOMM) and costs 64,732 B of flash and 16,416 B of .bss, measured (the [pico_base] note in platformio.ini). - The same block called SIMUT_MDNS "Negligible flash cost". LEAmDNS is ~15 KB (15,376 B measured). - src/ota/config_snapshot.cpp gave the snapshot margin as "~3.4 KiB", a v21 figure. On v25 it is 1,430 B (SystemConfig 6738 B + 4 of CRC against 8172 usable). tools/build_webui_gz.py adds pico_w_air to SHIPPING_ENVS. Air is published by release-ota.yml and was the one published image missing from the set, so nothing refused a page diet from leaking into it. It declares no custom_fs_pages, so this is a guard: all six images rebuild byte-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
The switchover moved the six firmware environments out of platformio.ini into the generated tools/generated/profiles.ini, so check_air_consistency.py's C6 check — which read the [env:pico_w_air] comment block from platformio.ini to confirm it agreed with -DSIMUT_CLI_FULL (finding F18) — could no longer find the env and failed. Retargeted C6 to tools/features.toml, where the cli_full setting and the rationale that explains it now live; the check is unchanged (the two must agree) and still fails on a mismatch (negative control confirmed). Also refreshed the gen_features.py docstring. All ten CI gates-job checks and the seven native suites pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
P2 of docs/analysis/MODELO_DE_RECURSOS.md extracts each feature into its own translation unit; before moving any code, this establishes the ratchet that makes the work measurable and keeps it from going backwards. tools/check_feature_sprawl.py counts the `#if SIMUT_` sites per feature macro and per file in src/ and holds them to a high-water mark in tools/feature_sprawl.json — the same mechanism as the flash budget, for entanglement. A site added to a shared file fails the gate; extracting a feature into its own TU lowers the mark. The starting scoreboard is 283 sites across 24 macros (SIMUT_AIR 41/15, SIMUT_DISPLAY_TFT 39/20, SIMUT_SENSOR_DS18B20 35/13, SIMUT_CLI_FULL 34/9, SIMUT_PANEL_PIN 27/13). It runs in the CI gates job and knows how to fail: an injected `#if SIMUT_AIR` in a shared file is caught (negative control verified). simut_config.h is excluded — it defines the macros, it does not spread them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3
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.
What
docs/analysis/MODELO_DE_RECURSOS.mdlays out a model for making SIMUT'sfeatures switchable: a single manifest describes what each firmware profile
turns on, a generator derives the PlatformIO config from it, and gates keep the
two from drifting. This PR is the P1 foundation of that model, the first
P0 cleanups it enables, and the P2 ratchet that will discipline the
seam extractions to come.
tools/features.toml— the manifest: the six firmware profiles, theirdisplay choice, toggles, source-file cuts and
lib_ignore, with the measuredrationale for each (the byte counts that lived in
platformio.ini's envblocks moved here).
tools/gen_features.py— derivestools/generated/profiles.ini(thePlatformIO config) and
features_model.json(for the future configurator).tools/check_features.py— proves, via PlatformIO's own resolver, thatthe environments the build uses resolve — flag for flag and translation-unit
for translation-unit — to what the manifest describes.
platformio.ininow includes the generated profiles viaextra_configsand nolonger defines
[env:pico_w_*]; it keepspico_baseand thenative_*testenvironments.
Why it is safe
The switch is proven byte-identical: each of the six
firmware.binbuiltfrom the switched
platformio.inihas the same sha256 as the image built beforethe switch (clean builds, both sides). The generated source-filter order was
written to match the hand-written one, so link order does not move the image.
Verified locally, everything CI runs:
gates-job checks (secrets, log codes, authorization, licence,Ângulo, fsguard, Air consistency, the two feature gates, the entanglement
watermark, history merge);
Both feature gates run in the CI
gatesjob, so the manifest and the buildcannot drift apart. Each gate has a negative control (a mutation is caught).
P0 cleanups included
SIMUT_FACTORY_RESETflag (read nowhere insrc/); the siximages rebuild byte-identical, which confirms it was dead.
numbers: the Bluetooth cost (classic SPP, 64,732 B + 16,416 B, not "BLE UART,
~22 KB"), the mDNS cost (~15 KB, not "negligible"), and the OTA snapshot
margin (1,430 B on v25, not the v21 "~3.4 KiB").
pico_w_airtoSHIPPING_ENVSso a page diet cannot leak into the onepublished image that was missing from it (unchanged, it declares no diet).
C6consistency gate — which read the air env fromplatformio.ini— to the manifest where the flag now lives.P2 ratchet included
tools/check_feature_sprawl.pycounts the#if SIMUT_sites per featuremacro and per file in
src/and holds them to a high-water mark intools/feature_sprawl.json— the same mechanism as the flash budget, forentanglement. A site added to a shared file fails; extracting a feature into
its own translation unit (the P2 goal) lowers the mark. The starting
scoreboard is 283 sites across 24 macros (
SIMUT_AIR41/15,SIMUT_DISPLAY_TFT39/20,SIMUT_SENSOR_DS18B2035/13,SIMUT_CLI_FULL34/9,
SIMUT_PANEL_PIN27/13). It runs in the CIgatesjob.This establishes the discipline; the actual seam extractions (a translation
unit per feature type, which will make that scoreboard drop) are follow-up PRs.
Not in this PR (deliberately)
simut_features.h(feature macros as a generated header for the code's#if) — belongs with the first P2 seam.s_intandloggingEnabled— removing a user-facingcontrol is a product decision.
/api/reset_touch_cal,/api/themes) under TFT —changes the alpha/air image, better reviewed on its own.
entanglement scoreboard.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Jq4auDQppGzmAx1gLKn1z3