# Phase 7 — v0.2 rearchitecture (agent-native simplification)

**Date:** 2026-04-08
**Reviews:** Claude + Codex (gpt-5.4 xhigh) + Gemini (3.1-pro-preview) ran in parallel on the same brief. All three converged; the v0.2 plan is the three-way synthesis.

**Raw review outputs preserved at:**
- `~/.claude/subagent-results/codex-output-1775664888-rearch.md`
- `~/.claude/subagent-results/gemini-output-1775664888-rearch.md`
- `~/.claude/subagent-results/claude-analysis-1775664888-rearch.md`
- `~/.claude/subagent-results/rearch-brief-1775664888.md` (the brief all three answered)

## Thesis

The v0.1 template system and segment DSL were designed for a "blind author" — an agent that can't preview output, can't iterate, needs the CLI to catch every footgun upfront. **That assumption is wrong.** Agents in Claude Code have Read/Write/Edit/Bash and can iterate on preview loops.

v0.2 commits fully to agent-native design: strip safety nets that exist because humans can't easily preview, drop subsystems whose job is to hide complexity the agent is fine with, minimize dependencies. Ship an integrated preview command as the core iteration primitive.

Target: **~9500 LoC → ~5000 LoC, 23 crates → 14 crates**, 6 of the 20 lint rules survive.

Zero production users means no backwards-compatibility shims needed. v0.2.0 is a breaking release.

## What gets deleted

1. **MJML pipeline** — `mrml`, `css-inline`, `html2text`, `serde_yaml`, the YAML frontmatter parser, the 20-rule lint, `template edit`, `template guidelines`, `template lint` (as a separate command; folded into preview), `assets/template-authoring.md`.
2. **Handlebars** — replaced by a hand-rolled `{{ var }}` + `{{#if}}` substituter with HTML escaping and a triple-brace allowlist.
3. **PEST segment filter DSL** — `src/segment/parser.rs` + `grammar.pest`. Segments are already stored as JSON AST internally; PEST is just an authoring façade. Agents pass `--filter-json <json>` directly.
4. **Webhook listener** — `webhook listen` and `webhook test` commands, `src/webhook/listener.rs`, `src/webhook/signature.rs`, `tiny_http` dep. Keep `webhook poll` / `event poll`.
5. **`template edit`** — interactive $EDITOR guard; violates AGENTS.md doctrine. Agents use Write/Edit directly.

## What gets added

1. **`src/template/subst.rs`** — ~80-line hand-rolled substituter. Supports `{{ var }}` (HTML-escaped), `{{{ var }}}` (raw, allowlist only), `{{#if var}}...{{/if}}`. Scans for unresolved placeholders, returns the list.
2. **`template preview <name> [--with-data <file>] [--out-dir <path>] [--open]`** — deterministic preview output at `$MLC_CACHE_DIR/preview/<name>/{index.html,plain.txt}`. `--open` launches `open`/`xdg-open`/`start`.
3. **Migration 0003** — rename `template.mjml_source` → `template.html_source`, drop `template.schema_json` column. Existing rows get migrated to empty (zero production users).
4. **Hard-fail on unresolved placeholders at send time** — the broadcast pipeline runs the substituter in strict mode; any `{{ typo }}` left in the output aborts the send with a specific error pointing at the placeholder.

## The 6 surviving lint rules

1. `UnsubscribeLinkMissing` — CAN-SPAM hard requirement
2. `PhysicalAddressFooterMissing` — CAN-SPAM hard requirement
3. `HtmlSizeError` at 102 KB — Gmail clip / compliance edge case
4. `ForbiddenTag` (`<script>`, `<form>`, `<iframe>`, `<object>`, `<embed>`) — security
5. `UnresolvedPlaceholder` — warn at preview, **hard-fail at send**
6. `ForbiddenRawInjection` — names outside the triple-brace allowlist cannot use raw HTML substitution (XSS prevention)

Dropped: everything MJML-related, everything schema-related, everything preview-catchable (`DangerousCss`, `SubjectTooLong/Empty`, `ImageMissingAlt`, `ButtonMissingHref`, `EmptyPlainText`, etc.).

## Phases

### Phase 1 — Kill the deadwood

**Low risk, high LoC reduction. Ship before touching templates.**

- Delete `src/webhook/listener.rs` + `src/webhook/signature.rs`
- Delete `src/segment/parser.rs` + `src/segment/grammar.pest`
- Delete `TemplateAction::{Edit,Guidelines}` + `WebhookAction::{Listen,Test}` from cli.rs
- Delete handlers in `src/commands/template.rs` (edit, guidelines) and `src/commands/webhook.rs` (listen, test)
- Update `src/commands/segment.rs::parse_and_store` → directly deserialize JSON
- Update `src/commands/contact.rs::list_contacts` → directly deserialize JSON filter
- Update `src/segment/compiler.rs` tests to construct AST directly (no `parser::parse`)
- Remove `pest`, `pest_derive`, `tiny_http`, `tempfile` from Cargo.toml
- Convert all integration tests using `--filter "DSL"` → `--filter '{json}'`
- Build + test + commit

### Phase 2 — Template rearchitecture

**The big one. Single commit or broken into sub-steps.**

- Add migration 0003 (rename column, drop schema_json)
- Create `src/template/subst.rs` — hand-rolled substituter
- Rewrite `src/template/compile.rs` as `src/template/render.rs` — substituter + 6 inline lint rules + placeholder injection for send pipeline
- Delete `src/template/frontmatter.rs` and `src/template/lint.rs`
- Delete `assets/template-authoring.md`
- Rewrite `src/commands/template.rs` — drop edit/guidelines, add preview, change create to take `--subject "..."`
- Update `src/broadcast/pipeline.rs` to use the new substituter and hard-fail on unresolved placeholders
- Remove `mrml`, `handlebars`, `css-inline`, `html2text`, `serde_yaml` from Cargo.toml
- Update the scaffold HTML to be self-documenting (it's now the entire docs)
- Add tests for substituter + preview command
- Build + test + commit

### Phase 3 — Polish + ship

- Update `src/commands/agent_info.rs` with the new command surface
- Bump `Cargo.toml` version to 0.2.0
- Update README badge to v0.2.0
- Full sweep: fmt + clippy + test
- Commit + tag v0.2.0 + push

## Dependency diff

```
REMOVE (9):
  mrml           (MJML compiler, ~500 KB)
  handlebars     (templating, ~100 KB)
  css-inline     (CSS inliner, ~80 KB)
  html2text      (plain-text alt, ~40 KB)
  serde_yaml     (frontmatter parser, ~200 KB)
  pest           (parser generator runtime, ~200 KB)
  pest_derive    (parser codegen macros, ~300 KB)
  tiny_http      (HTTP server, ~80 KB)
  tempfile       (template edit tempdirs, ~20 KB)

KEEP (14):
  anyhow, thiserror (errors)
  chrono            (timestamps)
  clap              (CLI framework)
  rusqlite          (SQLite)
  serde, serde_json (JSON)
  toml              (config.toml parser)
  dirs              (XDG paths)
  csv               (contact import)
  base64, hmac, sha2, subtle (HMAC unsubscribe tokens / RFC 8058)

NET: 23 → 14 crates (-9, -39%)
```

## Test count projection

| | Current (v0.1.3) | v0.2.0 projected |
|---|---|---|
| Unit tests | 111 | ~85 |
| Integration tests | 62 | ~55 |
| **Total** | **173** | **~140** |

Delete: 20 lint tests, 14 segment parser tests, 3 frontmatter tests, 2 webhook signature tests, some compile tests tied to MJML.
Add: ~6 substituter tests, ~5 new lint tests (on the 6 surviving rules), ~4 preview command integration tests.

## Risk register

| Risk | Severity | Mitigation |
|---|---|---|
| Outlook 2013-2016 layout breakage (5.67% of opens per Litmus Feb 2026) | Medium | Document in scaffold; operators who care pre-compile via `npx mjml` before `template create --from-file` |
| Agents write uglier HTML without MJML guardrails | Low | Preview loop catches visible issues; scaffold must be excellent |
| Unresolved-placeholder detection must be airtight at send time | High | New lint rule is an error, not a warning, at send time; pre-flight check in `broadcast send` |
| Dropping html2text loses auto plain-text alt | Medium | Dumb regex-strip fallback in `render.rs`; or require agent to provide plain-text via optional sidecar |
| Dropping css-inline means `<style>` blocks don't inline | Low | Document "use inline styles" in scaffold; Outlook 365 understands `<style>` blocks |
| Deleting `template guidelines` leaves agents without docs | Medium | Scaffold becomes the documentation — must be self-explanatory |
| Breaking v0.1.x templates and segments | None | Zero production users per handoff |

## Out of scope for v0.2

- Template versioning / `template history` / `template restore` (Gap #2 from v0.1.3 Codex review — defer indefinitely or add in v0.3)
- Real DMARC/SPF/DKIM checks in `report deliverability`
- Long-running poll daemon
- Webhook backfill
- Per-contact engagement score cache

## Acceptance criteria

- `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, `cargo test -- --test-threads=1` all clean
- `template preview welcome --open` writes deterministic files and opens them
- `broadcast send 1` on a template with `{{ typo }}` fails with a clear "unresolved placeholder" error before any Resend call
- `segment create vips --filter-json '{"kind":"atom","atom":{"type":"tag","pred":{"kind":"has","name":"vip"}}}'` works
- `webhook listen` returns "command not found"; `webhook poll` still works
- `cargo tree | wc -l` shows a meaningful reduction
- v0.2.0 tag pushed, CI green
