fix(forgeblock): render the five instrument parameter blocks on both static surfaces #214

Merged
peterswimm merged 1 commit from fix/forgeblock-param-coverage into main 2026-10-17 16:34:23 +00:00
Owner

Greens Test Generator / test, red on main since 2026-10-14.

#210 ("instrument parameter blocks") and #211 ("handheld (clap) destination tab") added five blocks to the ForgeBlock vocabulary — page_strip, param_page, param_knob, param_enum, param_toggle — with no WordPress and no email renderer branch. Each fell through to its surface's placeholder, and the vocabulary count the coverage test pins was left at 33 against a vocabulary of 38. Eleven tests: test_wp_renders ×5, test_email_renders ×5, test_full_vocab_covered.

Why it landed red, because the obvious explanation is wrong. The paths filter in test-generator.yml sits under push: only; pull_request: carries none. The lane did run on both PRs and reported failure on both heads — ee73ffe2 at 00:36Z, f2d350b at 01:10Z — and both were merged within minutes. The gate was not skipped and not filtered out. It was visible and merged over, which means Test Generator / test is not a blocking context on main. See the Impact Statement.

Scope

Five renderer branches on each static surface, in forgeblock_pattern.py (WordPress FSE pattern) and forgeblock_email.py (HTML email). These are separate code paths — if t == ... chains, not a shared mapping table — so both were edited. Every branch reuses the file's existing helpers (_heading, _facts/_fact_set, _bullets, _p, render_sequence) and takes every colour from the palette, never a literal hex.

A parameter control renders as an inert specimen, not a live control. This is not a style preference; it follows from two constraints already in the tree. The vocabulary's own group note says range, default, unit and value names "come from the client's own parameter table, which each plugin build already checks against its OPSL device… UDG never restates a number the DSP owns." forgeblock_email.py adds that email cannot host live inputs. So a specimen names the label, the OPSL binding, the control kind and the role, and states no value:

<h4>Cutoff</h4>
<ul><li>Parameter: filter.cutoff</li><li>Control: slider</li><li>Role: primary</li></ul>

param_page is a composite (of: [param_knob, param_enum, param_toggle]), so its controls go through the same renderer at depth, the way column_set already recurses into its children; its title is h3 and the controls h4 beneath it. page_strip renders the ordered page list with the entry point marked, since static output cannot switch pages.

The count bump stays a literal. 33 → 38, not a derived len(). That assertion is the only thing that caught this drift; deriving it would remove the tripwire and let the next block land unrendered in silence.

Out of scope, deliberately: forgeblock.yml is untouched — the five blocks are already defined and valid, and flipping used: false would drift dist/*/artifact.html and pull a large generated diff into a fix that does not need one. _PROGRAMMATIC is untouched; explicit branches supersede it for these five. The unknown-type fallthrough is untouched, since test_forgeblock_pattern.py:56 asserts an unknown type still renders the placeholder. The ASCII renderer already covers all 38 via its [{t}] fallback.

Testing

Measured in the CI container (python:3.12-slim, Toilville_Rituals checked out beside the repo, as the workflow does — without the sibling, test_pipeline_invariants.py fails rather than skips and inflates the count):

result
main @ 9302554 — control 11 failed, 510 passed, 1 skipped
this branch 529 passed, 1 skipped, 0 failed

521 + 8 new tests = 529.

Four new assertions, because the coverage test has no teeth on its own. test_wp_renders/test_email_renders only assert the output is non-empty and lacks the placeholder, so a branch returning "x" satisfies them. Measured: against deliberately hollowed branches, 76/76 of those cases still pass. The new assertions caught it 4/4. Each was verified against a failing control, not just the happy path:

Control Result
branches hollowed to "x" coverage tests 76/76 pass; the 4 new ones fail
specimen made to restate block["value"] test_param_control_states_no_value fails ×3

The assertions: each specimen must name its label and its OPSL parameter on all three surface modes (WP, email, letter); param_page must emit its nested controls; page_strip must list its pages and mark the current one; and a specimen must not restate a DSP-owned value even when the block carries value/default/min/max/unit.

No generated-output drift. build_surfaces.py check, artifact.py check --all (13 boards) and forgeblock_validate.py check (38 blocks, 28 fixtures) all report clean, and the tree stays clean after running them — expected, since all five blocks are used: false and no deck renders them yet. yamllint .forgejo/workflows/ clean.

Impact Statement

Risk: low. Five new branches on two static renderers, reachable only by a block type that previously produced a placeholder comment. No existing block's output changes — the drift gates above confirm it byte-for-byte. No token, theme, pattern or asset is touched.

The trigger widening is a smaller fix than it looks, and I want to be precise about it. Adding udg/**, tools/** and this workflow to the push: paths closes a real gap: the ForgeBlock coverage tests are driven by udg/config/forgeblock.yml, so a merge to main touching only udg/ previously produced no post-merge run from this lane at all. But it is not what would have stopped #210/#211 — those were PRs, pull_request: has no paths filter, and the lane ran and failed and was merged anyway.

The actual gap this PR does not close: Test Generator / test appears not to be a required status check on main. Two PRs merged with it red. Making it required is a repo-settings change, not a code change, and is deliberately not in this PR — but until someone makes it, nothing prevents the next recurrence. Branch-protection settings need an authenticated read to confirm, which I could not do.

One rule this now enforces: a new block in forgeblock.yml without a renderer branch on both static surfaces fails the lane twice — once on the count literal, once per surface — and the new assertions mean a stub branch does not buy a pass.

Greens `Test Generator / test`, red on main since 2026-10-14. #210 ("instrument parameter blocks") and #211 ("handheld (clap) destination tab") added five blocks to the ForgeBlock vocabulary — `page_strip`, `param_page`, `param_knob`, `param_enum`, `param_toggle` — with no WordPress and no email renderer branch. Each fell through to its surface's placeholder, and the vocabulary count the coverage test pins was left at 33 against a vocabulary of 38. Eleven tests: `test_wp_renders` ×5, `test_email_renders` ×5, `test_full_vocab_covered`. **Why it landed red, because the obvious explanation is wrong.** The `paths` filter in `test-generator.yml` sits under `push:` only; `pull_request:` carries none. The lane *did* run on both PRs and reported `failure` on both heads — `ee73ffe2` at 00:36Z, `f2d350b` at 01:10Z — and both were merged within minutes. The gate was not skipped and not filtered out. It was visible and merged over, which means **`Test Generator / test` is not a blocking context on `main`**. See the Impact Statement. ## Scope **Five renderer branches on each static surface**, in `forgeblock_pattern.py` (WordPress FSE pattern) and `forgeblock_email.py` (HTML email). These are separate code paths — `if t == ...` chains, not a shared mapping table — so both were edited. Every branch reuses the file's existing helpers (`_heading`, `_facts`/`_fact_set`, `_bullets`, `_p`, `render_sequence`) and takes every colour from the palette, never a literal hex. **A parameter control renders as an inert specimen, not a live control.** This is not a style preference; it follows from two constraints already in the tree. The vocabulary's own group note says range, default, unit and value names "come from the client's own parameter table, which each plugin build already checks against its OPSL device… UDG never restates a number the DSP owns." `forgeblock_email.py` adds that email cannot host live inputs. So a specimen names the label, the OPSL binding, the control kind and the role, and states no value: ``` <h4>Cutoff</h4> <ul><li>Parameter: filter.cutoff</li><li>Control: slider</li><li>Role: primary</li></ul> ``` `param_page` is a composite (`of: [param_knob, param_enum, param_toggle]`), so its controls go through the same renderer at depth, the way `column_set` already recurses into its children; its title is `h3` and the controls `h4` beneath it. `page_strip` renders the ordered page list with the entry point marked, since static output cannot switch pages. **The count bump stays a literal.** `33` → `38`, not a derived `len()`. That assertion is the only thing that caught this drift; deriving it would remove the tripwire and let the next block land unrendered in silence. **Out of scope, deliberately:** `forgeblock.yml` is untouched — the five blocks are already defined and valid, and flipping `used: false` would drift `dist/*/artifact.html` and pull a large generated diff into a fix that does not need one. `_PROGRAMMATIC` is untouched; explicit branches supersede it for these five. The unknown-type fallthrough is untouched, since `test_forgeblock_pattern.py:56` asserts an unknown type still renders the placeholder. The ASCII renderer already covers all 38 via its `[{t}]` fallback. ## Testing **Measured in the CI container** (`python:3.12-slim`, `Toilville_Rituals` checked out beside the repo, as the workflow does — without the sibling, `test_pipeline_invariants.py` fails rather than skips and inflates the count): | | result | |---|---| | `main` @ `9302554` — control | **11 failed**, 510 passed, 1 skipped | | this branch | **529 passed**, 1 skipped, 0 failed | 521 + 8 new tests = 529. **Four new assertions, because the coverage test has no teeth on its own.** `test_wp_renders`/`test_email_renders` only assert the output is non-empty and lacks the placeholder, so a branch returning `"x"` satisfies them. Measured: against deliberately hollowed branches, **76/76 of those cases still pass**. The new assertions caught it 4/4. Each was verified against a failing control, not just the happy path: | Control | Result | |---|---| | branches hollowed to `"x"` | coverage tests 76/76 pass; the 4 new ones **fail** | | specimen made to restate `block["value"]` | `test_param_control_states_no_value` **fails** ×3 | The assertions: each specimen must name its label and its OPSL parameter on all three surface modes (WP, email, letter); `param_page` must emit its nested controls; `page_strip` must list its pages and mark the current one; and a specimen must not restate a DSP-owned value even when the block carries `value`/`default`/`min`/`max`/`unit`. **No generated-output drift.** `build_surfaces.py check`, `artifact.py check --all` (13 boards) and `forgeblock_validate.py check` (38 blocks, 28 fixtures) all report clean, and the tree stays clean after running them — expected, since all five blocks are `used: false` and no deck renders them yet. `yamllint .forgejo/workflows/` clean. ## Impact Statement **Risk: low.** Five new branches on two static renderers, reachable only by a block type that previously produced a placeholder comment. No existing block's output changes — the drift gates above confirm it byte-for-byte. No token, theme, pattern or asset is touched. **The trigger widening is a smaller fix than it looks, and I want to be precise about it.** Adding `udg/**`, `tools/**` and this workflow to the `push:` paths closes a real gap: the ForgeBlock coverage tests are driven by `udg/config/forgeblock.yml`, so a merge to `main` touching only `udg/` previously produced no post-merge run from this lane at all. But it is **not** what would have stopped #210/#211 — those were PRs, `pull_request:` has no paths filter, and the lane ran and failed and was merged anyway. **The actual gap this PR does not close:** `Test Generator / test` appears not to be a required status check on `main`. Two PRs merged with it red. Making it required is a repo-settings change, not a code change, and is deliberately not in this PR — but until someone makes it, nothing prevents the next recurrence. Branch-protection settings need an authenticated read to confirm, which I could not do. **One rule this now enforces:** a new block in `forgeblock.yml` without a renderer branch on both static surfaces fails the lane twice — once on the count literal, once per surface — and the new assertions mean a stub branch does not buy a pass.
fix(forgeblock): render the five instrument parameter blocks on both static surfaces
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Test Generator / lint-workflows (pull_request) Successful in 24s
Test Generator / lint-prose (pull_request) Successful in 33s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 23s
Security Scans / Gitleaks secret scan (pull_request) Successful in 1m4s
Security Scans / Trivy filesystem scan (pull_request) Successful in 1m5s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 1m17s
Block Validation / validate (pull_request) Successful in 1m32s
Test Generator / test (pull_request) Successful in 4m42s
62fabe9786
`Test Generator / test` has been red on main since 2026-10-14. #210
("instrument parameter blocks") and #211 ("handheld (clap) destination tab")
added five blocks to the ForgeBlock vocabulary -- page_strip, param_page,
param_knob, param_enum, param_toggle -- with no WordPress and no email
renderer branch, so each fell through to its surface's placeholder, and
without bumping the vocabulary count the coverage test pins.

Eleven tests: test_wp_renders x5, test_email_renders x5, and
test_full_vocab_covered asserting 33 against a vocabulary of 38.

WHY IT LANDED RED, since the obvious explanation is wrong: the `paths` filter
in this workflow sits under `push:` only, and `pull_request:` carries none, so
the lane did run on both PRs. It reported `failure` on both heads --
ee73ffe2 at 00:36Z and f2d350b at 01:10Z -- and both were merged within
minutes. The gate was not skipped or filtered out. It was visible and merged
over, which means `Test Generator / test` is not a blocking context on main.
Widening the trigger below does NOT address that; see the Impact Statement on
the PR. Making the check required is a repo-settings change and is not in
this commit.

A parameter control binds to a parameter by its OPSL identifier and nothing
else: range, default, unit and value names come from the client's own
parameter table, which each plugin build already checks against its OPSL
device, and the vocabulary's own group note says UDG never restates a number
the DSP owns. forgeblock_email.py adds that email cannot host live inputs. So
on a static surface these render as an inert specimen -- the label, the OPSL
binding, the control kind and the role -- and state no value. param_page is a
composite, so its controls go through the same renderer at depth, the way
column_set already recurses into its children. page_strip renders the ordered
page list with the entry point marked, since static output cannot switch
pages.

The coverage test only asserts output is non-empty and lacks the placeholder,
so a branch returning "x" would satisfy it -- measured: 76/76 of its cases
pass against deliberately hollowed branches. Four new assertions give it
teeth: each specimen must name its label and its OPSL parameter on all three
surface modes, param_page must emit its nested controls, page_strip must list
its pages and mark the current one, and a specimen must not restate a
DSP-owned value even when the block carries one. Each was verified against a
failing control.

The 33 -> 38 bump stays a literal, not a derived count. That assertion is the
only thing that caught this drift, and deriving it would remove the tripwire.

Verified in the CI container (python:3.12-slim, Toilville_Rituals checked out
beside the repo): main at 9302554 gives 11 failed / 510 passed / 1 skipped;
this branch gives 529 passed / 1 skipped. build_surfaces, artifact --all and
forgeblock_validate all report clean -- no committed-output drift, since no
deck uses these blocks yet (all five are `used: false`).
peterswimm deleted branch fix/forgeblock-param-coverage 2026-10-17 16:34:23 +00:00
Sign in to join this conversation.
No description provided.