feat(udg): code primitive + client boundary (fixes toilville blocks leaking into 4 brands) #111

Merged
peterswimm merged 1 commit from fix/toilville-source-regressions into main 2026-09-10 14:49:48 +00:00
Owner

Re-opens the second half of #110. That PR merged at its first commit and
this one never landed. Clean merge, one commit.

It fixes a live defect on main

Before the client boundary existed, forgeblock_pattern imported the toilville
client directly and applied it to every brand. On main today:

spelwork      1 hero-home  1 joinband-cta
pardner       1 hero-home  1 joinband-cta
tctd          1 hero-home  1 joinband-cta
fux-capacity  1 hero-home  1 joinband-cta

Four brands ship patterns referencing toilville/* blocks from a plugin their
sites may not run — tctd serves truechiptilldeath.com on a theme with no
Toilville plugin at all. It's generated preview output rather than a deployed
site, so it's wrong output, not an outage. This PR makes it clean.

Vocabulary

  • 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: renderers 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 shows all 33 blocks reducing to every channel.

The client boundary

forgeblock_pattern now takes a client and no longer imports one.
build_surfaces looks it up per brand, so a brand without one gets the
universal rendering. 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.

forgeblock_toilville becomes two levels: render_block (one universal block
rendered richer — code → terminal, column_set{cards} → card-grid) and
render_run (composites that are not any single vocabulary block, 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. column_set records
its variant and chrome so the client doesn't guess. Card fields are read in
order — 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 steps and signed history were
dropped
. Both were caught by measuring rendered output, not by reading code.

Verification

  • just check green; 270 tests pass
  • 98% of design copy survives to rendered HTML (landing and rituals 100%,
    spelwork 98%, news 94%, contact 92% — remainder is a few contractions)
  • No wp:toilville/ in any other brand's patterns

https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78

**Re-opens the second half of #110.** That PR merged at its first commit and this one never landed. Clean merge, one commit. ## It fixes a live defect on main Before the client boundary existed, `forgeblock_pattern` imported the toilville client directly and applied it to **every** brand. On `main` today: ``` spelwork 1 hero-home 1 joinband-cta pardner 1 hero-home 1 joinband-cta tctd 1 hero-home 1 joinband-cta fux-capacity 1 hero-home 1 joinband-cta ``` Four brands ship patterns referencing `toilville/*` blocks from a plugin their sites may not run — `tctd` serves truechiptilldeath.com on a theme with no Toilville plugin at all. It's generated preview output rather than a deployed site, so it's wrong output, not an outage. This PR makes it clean. ## Vocabulary - **`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: renderers 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` shows **all 33 blocks reducing to every channel**. ## The client boundary `forgeblock_pattern` now takes a `client` and no longer imports one. `build_surfaces` looks it up per brand, so a brand without one gets the universal rendering. `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. `forgeblock_toilville` becomes two levels: `render_block` (one universal block rendered richer — `code` → terminal, `column_set{cards}` → card-grid) and `render_run` (composites that are not any single vocabulary block, 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`. `column_set` records its variant **and** chrome so the client doesn't guess. Card fields are read in **order** — 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 steps and signed history were dropped**. Both were caught by measuring rendered output, not by reading code. ## Verification - `just check` green; **270 tests pass** - **98% of design copy survives to rendered HTML** (landing and rituals 100%, spelwork 98%, news 94%, contact 92% — remainder is a few contractions) - **No `wp:toilville/` in any other brand's patterns** https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78
feat(udg): add the code primitive; clients decide how ForgeBlocks render
Some checks failed
PR policy check / policy-gate (pull_request) Failing after 1s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 23s
Test Generator / lint-workflows (pull_request) Successful in 25s
Security Scans / Gitleaks secret scan (pull_request) Successful in 26s
Test Generator / lint-prose (pull_request) Successful in 29s
Security Scans / Trivy filesystem scan (pull_request) Successful in 30s
Test Generator / test (pull_request) Successful in 3m20s
6b35fddfff
Ritual blocks are universal; a client decides how they look. This makes that
true in the pipeline instead of asserted in a comment.

VOCABULARY (udg/config/forgeblock.yml)

  code — a monospaced literal, of: [text], enums kind: [terminal, source].
  There was no way to say "this is a terminal transcript"; `diagram` renders to
  wp:code but means Mermaid.

  column_set — variant: [plain, cards, table, list] declared, and 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 was looking at.

Adding one block to the vocabulary is not a one-line change here, by design.
The repo demanded, and this commit supplies:
  - a renderer in the WordPress pattern client
  - a renderer in the email client
  - a renderer in the ASCII client
  - text · BBS · SMS · RCS · Apple Business Chat reductions in artifact.py
  - a card mock in the Ritual Blocks catalog
  - the deliberate count tripwire in test_block_coverage bumped 32 -> 33
`wcag.py report` again shows all 33 blocks reducing to every channel.

CLIENT BOUNDARY

forgeblock_pattern is now brand-agnostic in fact, not just intent: it takes a
`client` and no longer imports one. build_surfaces looks the client 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 rewritten from bespoke run-matchers into 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 a context, a header, a section and an
                           actions row), still keyed on TYPES

CONVERTER

  - `code` is produced from `data-term` or a mono container with several lines.
  - column_set records its variant AND its chrome (a 1px gap over a hairline
    parent is the design's seam idiom) so the client does not have to guess.
  - <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.
  - card fields are read in ORDER, not purely by type: a short run BEFORE the
    heading is the mono slot, prose after it is the body. Reading by type put
    the tag in the body and left the real body on the floor. Multiple paragraphs
    now concatenate instead of the first one winning.
  - a fact_set inside a card becomes the card's feature list, not a flattened
    body string.

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.

RESULT

98% of design copy survives all the way 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.

Claude-Session: https://claude.ai/code/session_01Kbzfscp4w5enUrZxMnDt78
peterswimm deleted branch fix/toilville-source-regressions 2026-09-10 14:49:48 +00:00
Sign in to join this conversation.
No description provided.