fix(converter): see grids, forms and link labels - 100% design copy captured #110

Merged
peterswimm merged 1 commit from fix/toilville-source-regressions into main 2026-09-09 23:45:59 +00:00
Owner

First step toward 1:1 on #102. Design copy reaching the ForgeBlock tree goes
from 97/100/98/92/80% per page to 100% on all five.

Four converter fixes — three are the same underlying bug

A container's own text was dropped whenever the walker only recursed into child
elements.

1. _grid_columns() only matched repeat(<digit>. Every grid in the design
is repeat(auto-fit, minmax(Npx, 1fr)) or an explicit track list
(150px 1fr auto, 208px 1fr, 1.35fr 1fr) — so none were detected and
every grid flattened. Now resolves auto-fit/auto-fill from the child count
and counts explicit tracks paren-aware, so minmax()'s comma doesn't split.

2. A column that IS its own text vanished. Table cells and teaser fields are
<span>2026-07-14</span> with no child elements, so _convert_children
produced nothing and the column was skipped. That is how the news titles and the
console's intent/handler/p95 values disappeared the moment grids started being
detected.

3. A text-only <a> emitted nothing — losing every plain link label ("All
news →", "Enter the portal →", "Browse RFCs →", "Read the post →"). Now emits a
one-button actions run, which is what it is.

4. input could not be produced at all — no heuristic, no data-block
case. Adds both, plus bare <button>. The design's controls carry no
name/id/value
, so the action_id is synthesised from the label text and
select options are read from <option> text.

The Contact form now yields 5 input blocks (2 text, email, a 4-choice
select, textarea) where it previously yielded nothing.

Two source regressions fixed — both mine

  • The Spelwork Documentation grid was broken markup. My CTA-annotation pass
    in #101 classified the six full-bleed card links as buttons (padding +
    background matched is_button()) and the line-based wrapper assumed
    single-line anchors, so </div> closed inside the still-open <a>,
    orphaning every card's content. All five sources now parse with zero tag
    mismatches
    (checked with html.parser).
  • background-color: #cccccc left on the three Home section headers — the grey
    bars visible in the review renders.

And a silent test hole

test_forgeblock_grammar located the sibling Rituals checkout as
../Toilville_Rituals relative to the source tree. That's wrong inside a git
worktree, so 18 grammar tests silently skipped instead of running. Now asks
git for the real repo root — 26 pass in both the main checkout and a worktree.

Verification

  • just check green; pytest generator/tests/ tools/wp_theme_ingestor/test_*.py267 passed
  • All five sources well-formed, 0 mismatches, 0 unclosed tags
  • Cross-brand: zero copy lost on any of the other five brands. fux-capacity,
    pardner and tctd each gain a column_set; peterswimm and tctd gain an
    actions run; fux-capacity gains 5 words it was dropping.

What this does not do

Structure is now captured; it is not yet rendered faithfully. column_set
renders as generic wp:columns and input renders as plain paragraphs, so the
patterns are still not 1:1. The block work (generalized card-grid, the
terminal, console, ritual demo, and wish-form on Contact) follows in
toilville_rituals, with adapters here after.

https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78

First step toward 1:1 on #102. **Design copy reaching the ForgeBlock tree goes from 97/100/98/92/80% per page to 100% on all five.** ## Four converter fixes — three are the same underlying bug A container's own text was dropped whenever the walker only recursed into child *elements*. **1. `_grid_columns()` only matched `repeat(<digit>`.** Every grid in the design is `repeat(auto-fit, minmax(Npx, 1fr))` or an explicit track list (`150px 1fr auto`, `208px 1fr`, `1.35fr 1fr`) — so **none** were detected and every grid flattened. Now resolves `auto-fit`/`auto-fill` from the child count and counts explicit tracks paren-aware, so `minmax()`'s comma doesn't split. **2. A column that IS its own text vanished.** Table cells and teaser fields are `<span>2026-07-14</span>` with no child elements, so `_convert_children` produced nothing and the column was skipped. That is how the news titles and the console's intent/handler/p95 values disappeared the moment grids started being detected. **3. A text-only `<a>` emitted nothing** — losing every plain link label ("All news →", "Enter the portal →", "Browse RFCs →", "Read the post →"). Now emits a one-button `actions` run, which is what it is. **4. `input` could not be produced at all** — no heuristic, no `data-block` case. Adds both, plus bare `<button>`. The design's controls carry **no `name`/`id`/`value`**, so the `action_id` is synthesised from the label text and select options are read from `<option>` text. The Contact form now yields **5 `input` blocks** (2 text, email, a 4-choice select, textarea) where it previously yielded nothing. ## Two source regressions fixed — both mine - **The Spelwork Documentation grid was broken markup.** My CTA-annotation pass in #101 classified the six full-bleed card links as buttons (`padding` + `background` matched `is_button()`) and the line-based wrapper assumed single-line anchors, so `</div>` closed *inside* the still-open `<a>`, orphaning every card's content. **All five sources now parse with zero tag mismatches** (checked with `html.parser`). - `background-color: #cccccc` left on the three Home section headers — the grey bars visible in the review renders. ## And a silent test hole `test_forgeblock_grammar` located the sibling Rituals checkout as `../Toilville_Rituals` relative to the source tree. That's wrong inside a git worktree, so **18 grammar tests silently skipped instead of running**. Now asks git for the real repo root — 26 pass in both the main checkout and a worktree. ## Verification - `just check` green; `pytest generator/tests/ tools/wp_theme_ingestor/test_*.py` — **267 passed** - All five sources well-formed, 0 mismatches, 0 unclosed tags - **Cross-brand: zero copy lost on any of the other five brands.** `fux-capacity`, `pardner` and `tctd` each gain a `column_set`; `peterswimm` and `tctd` gain an `actions` run; `fux-capacity` gains 5 words it was dropping. ## What this does not do Structure is now *captured*; it is not yet *rendered* faithfully. `column_set` renders as generic `wp:columns` and `input` renders as plain paragraphs, so the patterns are still not 1:1. The block work (generalized `card-grid`, the terminal, console, ritual demo, and `wish-form` on Contact) follows in toilville_rituals, with adapters here after. https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78
fix(converter): see grids, forms and link labels — 100% design copy captured
Some checks failed
PR policy check / policy-gate (pull_request) Failing after 1s
Security Scans / Trivy filesystem scan (pull_request) Successful in 10m47s
Security Scans / Gitleaks secret scan (pull_request) Successful in 10m39s
Test Generator / lint-workflows (pull_request) Successful in 2m24s
Test Generator / lint-prose (pull_request) Successful in 2m33s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 1m20s
Test Generator / test (pull_request) Successful in 5m56s
a1bed787e0
Design copy reaching the ForgeBlock tree goes from 97/100/98/92/80% per page to
100% on all five. Four fixes, three of which are the same underlying bug: a
container's own text was dropped whenever the walker only recursed into child
ELEMENTS.

1. _grid_columns() only matched `repeat(<digit>`. Every grid in the toilville
   design is `repeat(auto-fit, minmax(Npx, 1fr))` or an explicit track list
   (`150px 1fr auto`, `208px 1fr`, `1.35fr 1fr`), so NONE were detected and every
   grid flattened. Now resolves auto-fit/auto-fill from the child count and
   counts explicit tracks (paren-aware, so minmax() commas don't split).

2. A column that IS its own text vanished. Table cells and teaser fields are
   `<span>2026-07-14</span>` with no child elements, so _convert_children
   produced nothing and the column was skipped — which is how the news titles
   and the console's intent/handler/p95 values disappeared once grids started
   being detected at all.

3. A text-only <a> emitted nothing, losing every plain link label ("All news",
   "Enter the portal", "Browse RFCs", "Read the post"). Now emits a one-button
   actions run, which is what it is.

4. `input` could not be produced at all — no heuristic, no data-block case. Adds
   both, plus <button>. The design's controls carry no name/id/value, so the
   action_id is synthesised from the label text and select options are read from
   the <option> text. The Contact form now yields 5 input blocks (2 text, email,
   a 4-choice select, textarea) instead of nothing.

Two source regressions fixed, both mine:

  - The Spelwork Documentation grid was BROKEN markup. My CTA-annotation pass in
    #101 classified the six full-bleed card links as buttons (padding +
    background matched) and the line-based wrapper assumed single-line anchors,
    so the closing div landed inside the still-open anchor, orphaning every
    card's content. All five sources now parse with zero tag mismatches.
  - background-color #cccccc left on the three Home section headers — visible as
    grey bars in the review renders.

Also: test_forgeblock_grammar located the sibling Rituals checkout relative to
the source tree, which is wrong inside a git worktree — 18 grammar tests
silently SKIPPED instead of running. Now asks git for the real repo root.

Cross-brand: zero copy lost on any of the five other brands; fux-capacity,
pardner and tctd each gain a column_set, peterswimm and tctd gain an actions run.

Claude-Session: https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78
peterswimm deleted branch fix/toilville-source-regressions 2026-09-09 23:45:59 +00:00
Author
Owner

Reworked: the vocabulary decides, the client renders

Peter's steer — ritual blocks are universal, clients like Gutenberg decide how
to render them
— meant the adapter approach in this branch was backwards. It
pattern-matched runs of ForgeBlocks and bypassed the vocabulary entirely.
Rewritten.

Vocabulary (udg/config/forgeblock.yml)

  • code — a monospaced literal. of: [text], enums: kind: [terminal, source].
    There was no way to say "terminal transcript"; diagram renders to wp:code
    but means Mermaid.
  • column_setvariant: [plain, cards, table, list] declared, and flipped
    to used: true. It was the most important composite in the design while
    marked unused, and a client had no way to ask what kind of grid it had.

Adding one block here is deliberately not a one-line change. The repo demanded,
and this supplies: a renderer in the WP pattern, email and ASCII
clients; text · BBS · SMS · RCS · Apple Business Chat reductions; a mock in
the Ritual Blocks catalog; and the count tripwire bumped 32 → 33.
wcag.py report again shows all 33 blocks reducing to every channel.

The client boundary is now real

forgeblock_pattern takes a client and no longer imports one — brand-agnostic
in fact, not just intent. build_surfaces looks it up per brand, so a brand
without one gets the universal rendering. Verified: no toilville block leaks
into the other five brands.

render_sequence gives the client first refusal at every depth. A choice
that only held at the top level meant the Contact form — which lives inside a
column — silently reverted to the universal rendering.

forgeblock_toilville is now two levels:

render_block(block)     one universal block, rendered richer
                        code            -> toilville/terminal
                        column_set{cards} -> toilville/card-grid + card
render_run(blocks, i)   composites that are not any single vocabulary block
                        (a hero is context + header + section + actions),
                        still keyed on TYPES

Converter

<article> joins the container tags — the design puts the grid on the
article for news rows, so those never registered as a column_set at all.
column_set now records its variant and its chrome, so the client doesn't
guess. Card fields are read in order (a short run before the heading is the
mono slot; prose after it is the body) — reading purely by type put the tag in
the body and left the real body on the floor.

Guards worth keeping

The card renderer refuses a column carrying inputs, a terminal, a nested grid,
or a long repeated run. Without them the Contact form rendered as a card, and
the ritual demo's step list and signed history were dropped past the first
paragraph. Both were caught by measuring rendered output, not by reading code.

Result

98% of design copy survives to rendered HTML — landing and rituals 100%,
spelwork 98%, news 94%, contact 92% (the remainder is a handful of
contractions). Every page emits toilville blocks. Contact renders the real
wish-form with Name, Email, Organization, Interested in, and the custom
message label.

just check green, 270 tests pass.

Still not wired

toilville/ritual-demo and toilville/intent-console exist (toilville_rituals#368)
but nothing emits them. Doing that properly means annotating the design source
so the demo's steps become status blocks and its history becomes
lineage_entry — the converter can't produce lineage_entry at all today. That
is the vocabulary-first path and it is the next piece, rather than another
shape-matcher.

## Reworked: the vocabulary decides, the client renders Peter's steer — *ritual blocks are universal, clients like Gutenberg decide how to render them* — meant the adapter approach in this branch was backwards. It pattern-matched runs of ForgeBlocks and bypassed the vocabulary entirely. Rewritten. ### Vocabulary (`udg/config/forgeblock.yml`) - **`code`** — a monospaced literal. `of: [text]`, `enums: kind: [terminal, source]`. There was no way to say "terminal transcript"; `diagram` renders to `wp:code` but means Mermaid. - **`column_set`** — `variant: [plain, cards, table, list]` declared, and flipped to `used: true`. It was the most important composite in the design while marked unused, and a client had no way to ask what *kind* of grid it had. Adding one block here is deliberately not a one-line change. The repo demanded, and this supplies: a renderer in the **WP pattern**, **email** and **ASCII** clients; **text · BBS · SMS · RCS · Apple Business Chat** reductions; a mock in the Ritual Blocks catalog; and the count tripwire bumped 32 → 33. `wcag.py report` again shows **all 33 blocks reducing to every channel**. ### The client boundary is now real `forgeblock_pattern` takes a `client` and no longer imports one — brand-agnostic in fact, not just intent. `build_surfaces` looks it up per brand, so a brand without one gets the universal rendering. **Verified: no toilville block leaks into the other five brands.** `render_sequence` gives the client first refusal at **every depth**. A choice that only held at the top level meant the Contact form — which lives inside a column — silently reverted to the universal rendering. `forgeblock_toilville` is now two levels: ``` render_block(block) one universal block, rendered richer code -> toilville/terminal column_set{cards} -> toilville/card-grid + card render_run(blocks, i) composites that are not any single vocabulary block (a hero is context + header + section + actions), still keyed on TYPES ``` ### Converter `<article>` joins the container tags — the design puts the grid *on* the article for news rows, so those never registered as a `column_set` at all. `column_set` now records its variant **and** its chrome, so the client doesn't guess. Card fields are read in **order** (a short run before the heading is the mono slot; prose after it is the body) — reading purely by type put the tag in the body and left the real body on the floor. ### Guards worth keeping The card renderer refuses a column carrying inputs, a terminal, a nested grid, or a long repeated run. Without them the **Contact form rendered as a card**, and the **ritual demo's step list and signed history were dropped** past the first paragraph. Both were caught by measuring rendered output, not by reading code. ### Result **98% of design copy survives to rendered HTML** — landing and rituals 100%, spelwork 98%, news 94%, contact 92% (the remainder is a handful of contractions). Every page emits toilville blocks. Contact renders the real `wish-form` with Name, Email, Organization, Interested in, and the custom message label. `just check` green, **270 tests pass**. ### Still not wired `toilville/ritual-demo` and `toilville/intent-console` exist (toilville_rituals#368) but nothing emits them. Doing that *properly* means annotating the design source so the demo's steps become `status` blocks and its history becomes `lineage_entry` — the converter can't produce `lineage_entry` at all today. That is the vocabulary-first path and it is the next piece, rather than another shape-matcher.
Sign in to join this conversation.
No description provided.