Day 21 — 'stop dumping into the persona; build the library.'
Day 20 closed with the chain orchestration on code-level rails. Day 21 went one architectural layer deeper: stop relying on the model to GENERATE complex components (sliders, navs, heroes — the things small models can scaffold but can't fill). Hand-craft a library of named components; the model picks; the composer substitutes. AO.1 Part 7 decoupled CSS into its own file. Part 7.1 walked back JS-separation (small models can't reliably emit two fenced blocks). Part 8 shipped the snippet library v0.1 + composer stage end-to-end. The slider snippet substituted into a generated landing page with a real image and working JS — the architectural proof. Plus the launch positioning locked: 'I built a sub-optimal AI platform on purpose.'
“stop dumping into the persona; build the library.”
The arc of Day 21
Day 20 proved that chain orchestration belongs in code, not in the model’s tool-call loop. Day 21 carries the lesson one layer deeper: complex-component generation belongs in a hand-crafted library, not in the model’s per-pass output. Two days, two moves from “ask the model harder” to “build the infrastructure that obviates the ask.”
Three concrete shifts shipped:
- Decoupled CSS (AO.1 Part 7) — split styling out of the
single-file HTML artifact into a separate
styles.css. CSS in its own file becomes a filesystem boundary that the “preserve structure verbatim” rule can’t violate by accident anymore. - Snippet library v0.1 (AO.1 Part 8) — 5 hand-crafted
Material-style components at
~/werbos/snippets/with a Material 3 token contract (--w-*custom properties), base CSS, README, authoring guide. The agent picks from this library; humans craft what’s in it. - Composer stage (AO.1 Part 8 cont.) — pure-Python
stage between @front-end and @css-wizard that scans for
<TAG data-snippet="category/name">placeholders in the generated HTML and substitutes the snippet’s HTML + CSS + JS deterministically. Zero model calls, zero ambiguity.
Why decoupling CSS came first (AO.1 Part 7)
Two days of fighting “@css-wizard drops rows from index.html during the styling pass.” Persona rules said “preserve structure verbatim.” Model didn’t comply consistently. The architectural answer wasn’t more persona language; it was make the file the model can’t touch literally a different file.
- @front-end produces
index.html(semantic HTML + inline JS). - @css-wizard produces
styles.css(visual treatment only). - The model literally can’t drop rows from a file that isn’t in
its
expect_files. The “preserve” rule becomes a filesystem boundary, not a persona promise.
Plus: dropped Tailwind CDN entirely. Plain modern CSS with custom
properties handles everything — and @tailwind base/components/ utilities PostCSS directives in CDN mode were their own bug class.
The 7.1 walkback
Part 7 initially proposed splitting JS out too (script.js as a
third file). Day-21-morning testing showed deepseek-coder-v2:16b
cannot reliably emit two fenced blocks in one response. Model
is trained for one artifact per response; asking for two collapses
the second into the first or refuses outright.
Reverted to: HTML with inline <script> (1 file) + styles.css
(separate). CSS-separation is the load-bearing decoupling; JS-
separation was nice-to-have decoration. User’s framing on the
call captured it perfectly: “js is a nice to have… but css is
mandatory.”
The snippet library bet (AO.1 Part 8)
Late-Day-20 conversation: “why cant i make a graph snippet and werbos just grabs and builds?” That instinct turned into the day’s biggest architectural move.
Crafted v0.1 starter pack at ~/werbos/snippets/:
_tokens.css— Material 3 token contract (color roles + elevation tiers + shape family + 4dp spacing + 5-level typography). Every snippet’s CSS usesvar(--w-*)exclusively; hard-coded values are forbidden._base.css— lean reset + layout primitive utilities. No pre-styled components (shadcn/ui-style, not Bootstrap-style).- 5 snippets in
nav/,display/,data/,image/— each withsnippet.html,snippet.css,meta.json, plussnippet.jswhere interactive (slider). README.md— token contract spec + authoring guide + roadmap to ~28 snippets for v1.
Composer stage in build_project.py:
- Loads library at session start (cataloged by
category/namekeys). - Scans @front-end’s HTML for
<TAG data-snippet="category/name"> </TAG>placeholder elements. - Substitutes each match with the snippet’s HTML.
- Concatenates snippet CSS into
styles.cssafter tokens + base. - Injects snippet JS (IIFE-wrapped) into the inline
<script>block. - @css-wizard’s job shrinks to APPENDING a
:rootoverride block matching the spec’s palette. No per-component CSS anymore. Per-component lives in the snippet library, themed via tokens.
The slider moment
11:10pm test: same dashboard brief that’s been the test corpus all week. The model emitted `
` for the carousel slot. **The composer substituted.**What landed in the browser:
- Real iPhone-with-purple-gradient image from Pollinations.ai (the snippet’s default URL did its job)
- Working dots indicator at the bottom of the card
- Card-shaped wrapper with Material elevation shadow
[Slide 1 title]+[Short description of slide 1.]placeholders intact for operator fill-in- Click a dot → slide changes (the snippet’s IIFE-wrapped JS
was injected into the inline
<script>block, picked updata-sliderattribute, wired everything)
This is the first time a hand-crafted snippet from a library composed into a model-generated page with real image + working JS + matching styles. That’s the architectural proof for the whole library bet. Tomorrow morning’s tagline writes itself:
“Yesterday the model couldn’t make a working slider. Tonight it picked a slider from a library, and the library handled the rest.”
The 60% landed; 40% diagnosed
Nav and hero snippets DIDN’T substitute on the same run. The model
wrote its own .navbar / .hero BEM markup instead of using the
data-snippet placeholders. Composer skipped them; their snippet
CSS never landed; the rendered nav stacks vertically with no
styling.
@css-wizard also returned the pre-populated styles.css verbatim
WITHOUT appending the project’s :root override block — so the
artifact uses the default Material blue palette regardless of the
spec.
Both are model-discipline gaps, both fixable with persona- tightening:
- @front-end: “MUST use snippet placeholders for these component types; writing your own nav/hero markup is a failure.”
- @css-wizard: “Append
:root { --w-* }overrides at the BOTTOM of styles.css; returning the file verbatim without overrides is a failure.”
5-min persona work, deferred to Day 22 morning because the architectural proof tonight was the meaningful deliverable.
Tomorrow’s first move: modular CSS files
Late-Day-21 conversation surfaced the next architectural
refinement: instead of the composer concatenating all snippet
CSS into one styles.css, write each snippet’s CSS as its own
file under styles/, with the project’s styles.css
@importing each one.
Operator framing: “the sass approach where index.scss imports
nav.scss and graph.scss and table.scss” — but achievable with
plain native CSS @import (no Sass build step needed in 2026).
Resulting project structure:
project/
├── intent.md
├── spec.md
├── index.html <link rel="stylesheet" href="styles.css">
├── styles.css 6-line orchestrator with @imports
└── styles/
├── tokens.css
├── base.css
├── nav-navbar-horizontal.css
├── data-slider.css
└── theme.css @css-wizard writes ONLY this
@css-wizard’s expect_files shrinks to ["styles/theme.css"] —
the most extreme version of the simplification arc that started
two days ago.
Plus the launch-narrative angle: “Each project is a tree of files an operator can hand-edit. The AI doesn’t produce blob output; it produces a structured project.”
AHAs
- Stop dumping into the persona; build the library. The Day 20 lesson was “move chain orchestration to code.” Day 21’s is the same shape one layer deeper: stop loading more language into the persona to fix what the model can’t do; build the infrastructure that obviates the ask. Both lessons point at the same anti-pattern: trying to compensate for small-model limitations by making the model carry more.
- Modern CSS in 2026 is good enough that Sass-like
modularity needs no build step. Custom properties +
native @import + cascade layers + color-mix() cover almost
everything Sass gave us. The right shape for werbos is
per-snippet
.cssfiles with a thin orchestrator that@imports them. - Material 3’s token system maps to “@css-wizard’s whole job
is a :root block.” When the snippet library does the per-
component CSS heavy lifting, the model’s job at styling time
is generating a
:root { --w-primary: ...; ... }block from the spec’s palette. Trivial for any local model. The 200-line per-component CSS that was the bug-rich surface is now in the library. - Two-fence multi-file output is broken on local 16b models. Tonight’s 7.1 walkback. Models are trained for one artifact per response. Asking for two collapses the second into the first or refuses. CSS-as-separate-file works because it’s written by a DIFFERENT specialist (@css-wizard, separate dispatch). JS-as-separate-file would require @front-end to emit TWO fences, which it can’t reliably do.
- Positioning locked: werbos is INTENTIONALLY sub-optimal in spec so it runs on hardware most people already own. Late conversation thread that closed the launch-narrative question. Hermes/OpenClaw/Claude Max are great IF you have budget for an API key; werbos is for when you don’t. The launch piece headline becomes “I built a sub-optimal AI platform on purpose.”
Quote of the day
“resist the urge to over context dump on my little model.”
Operator-stated end-of-day, after we shipped the snippet library
- composer end-to-end. The whole-day arc condensed into one sentence: don’t make the persona longer to compensate for what the model can’t do; build the infrastructure that lets the model do what it’s actually good at (selection from a curated set) instead of fighting it on what it isn’t (generating complex working components from scratch). The compounding wisdom from [[celebrate-wins-2026-05-19]] applies — small model in a smart architecture beats big model fighting a dumb one.
Carryover
- Persona tightening (tomorrow’s first 15 min) —
@front-end MUST use snippet placeholders for nav/hero/slider/
kpi-card/image-placeholder; @css-wizard MUST append
:rootoverrides. Both 5-min edits. - Modular CSS files refactor (the morning’s main work) —
composer writes per-snippet
.cssfiles understyles/,styles.cssbecomes a thin@importorchestrator, @css-wizard’sexpect_filesshrinks to["styles/theme.css"]. - Add ~10 more snippets — table-basic, chart-bar, chart-line, footer, button-primary, input, badge, empty-state, breadcrumb, pagination. Hand-crafted in parallel to the modular-CSS refactor. Library scope was always ~28; we’re at 5.
- Distribution rails (still deferred from Day 20) — NX.1 npm publish + A.5 day 5 PyPI. The architectural-clarity work has been worth the deferral, but the “stranger can install werbos” bar is still the v1 ship line.
- Artifact extractor fence-header leak — still in the bug bin. Not blocking but worth fixing for demo polish.
- Snippet library file structure on SR8 — composer reads
from
/data/snippets/(WERBOS_DATA_DIR + /snippets). Deployed and live as of tonight.
What we proved tonight (the meaningful bit)
- A hand-crafted snippet can compose into a model-generated page without the model writing the component itself.
- The composer is deterministic (no model calls during substitution).
- The token contract themes the whole library when @css-wizard
overrides
:rootvalues. - The same architecture pattern that solved chain orchestration
(Day 20:
build_projecttool) solves complex-component generation (Day 21: snippet library + composer). - The launch narrative now has a real product story: “operator-curated library, AI picks what fits, themed via tokens.” That beats “AI-generated dashboards that sometimes paint the wrong column.”
Day 22 morning: persona tightenings + modular CSS refactor + add ~10 snippets. Then the wireframe → mockup demo is genuinely shippable.