Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

Punch turns one brand website and one to six product pages into a grounded,
responsive ecommerce email. It extracts evidence, asks Claude for a semantic
campaign, checks product and claim associations deterministically, then writes
standalone HTML and machine-readable validation artifacts.
campaign, checks product and claim associations deterministically, then renders
it with configurable brand colours and fonts. The result is standalone HTML
and machine-readable validation artifacts.

It is an engine and CLI, not an ESP. Punch does not manage contacts, send
messages or hide unsupported claims behind a confidence score.
Expand All @@ -39,28 +40,40 @@ the useful generative part while making commerce facts inspectable:
- unknown or conflicted critical facts cannot be promoted to truth;
- availability, promotion and selected high-risk claims require source support;
- Claude produces semantic blocks, never raw layout HTML;
- website style roles inform a validated brand theme, with explicit overrides
and readable fallbacks;
- final HTML passes deterministic accessibility, geometry, resource and
compliance-placeholder checks; and
- public fetching rejects local/private networks, unsafe redirects, oversized
responses and credential-bearing URLs.

## Live showcase

<p align="center">
<img src="https://raw.githubusercontent.com/plmn95/punch/main/docs/showcase/northstar-campaign.png" alt="A live Punch campaign for the fictional Northstar Goods brand" width="700" />
</p>

This campaign was generated live with Claude Sonnet 5 from two public, newly
fictional product pages. The `sales` safety policy was combined with a custom
desk-reset brief; both supplied products remained grounded and all ten campaign
and render checks passed.

[Inspect the brief, source commit and validation record](https://github.com/plmn95/punch/blob/main/docs/showcase/README.md).
## One campaign, different looks

Change colours and fonts without changing the products, copy or links, or
making another AI call. These screenshots show the same fictional campaign
rendered with two brand profiles, on desktop and mobile. Click either image
to inspect it at full resolution.

<table>
<tr>
<th>Blue · desktop</th>
<th>Dark · mobile</th>
</tr>
<tr>
<td width="67%" valign="top"><a href="docs/showcase/branding/blue-desktop.jpg"><img src="docs/showcase/branding/blue-desktop.jpg" alt="Soft Orbit campaign with blue accents and Verdana headings on desktop" width="520" /></a></td>
<td width="33%" valign="top"><a href="docs/showcase/branding/dark-mobile.jpg"><img src="docs/showcase/branding/dark-mobile.jpg" alt="The same Soft Orbit campaign with a dark background, lime accents and monospace headings on mobile" width="250" /></a></td>
</tr>
</table>

These are renderer examples, not fresh AI generations or automatic brand-detection
results. [Reproduce them without an API key](docs/showcase/branding/README.md).
For an end-to-end generation with source evidence and a validation record,
see the [recorded Northstar Goods live run](docs/showcase/README.md).

## Quick start

Punch currently ships from source. Node.js 24 or newer and an Anthropic API key
are required.
Punch currently ships from source. Node.js 24 or newer is required; an Anthropic
API key is needed for generation, but not for rendering an existing campaign.

```bash
git clone https://github.com/plmn95/punch.git
Expand Down Expand Up @@ -91,6 +104,51 @@ Add `--trace` for redacted structured stage artifacts or `--json` for exactly
one terminal JSON result on stdout. Run `node dist/cli/bin.js --help` for the
complete explicit interface.

### Guided input and brand settings

Run `node dist/cli/bin.js` in a terminal to start the optional guide. It collects
website/product URLs and the campaign brief, shows detected colours and fonts,
then asks for confirmation **before any AI call**. Keep the detected settings
with Enter, change individual six-digit hex colours or font families, preview
the actual email in a browser, and export when ready.

Complete commands stay prompt-free. Add `--interactive` to request review even
with complete inputs. `--json`, `--no-interactive`, CI and non-TTY input/output
always disable prompting.

```bash
node dist/cli/bin.js generate \
--website "https://example.com" \
--product "https://example.com/products/first-product" \
--goal "sales" \
--primary-colour "#2563EB" \
--heading-font "Verdana" \
--save-brand "./brand.json" \
--output "./campaign"
```

Use `--brand ./brand.json` to reuse a saved profile. Explicit flags override
the profile; supplied settings override website detection. Output paths and
profile filenames must be new; Punch never overwrites an existing profile.

### Restyle without another AI call

The guide's **adjust branding** action re-renders the same campaign without
changing its copy or spending more model tokens. Saved campaigns can also be
restyled without an API key:

```bash
node dist/cli/bin.js render \
--campaign "./campaign/campaign.json" \
--primary-colour "#006644" \
--output "./campaign-green"
```

Render-only output is explicitly labelled `render-only` in its validation
metadata: it checks the HTML, not current product facts or source grounding.
Saved campaign settings are retained unless overridden. See
[brand settings and CLI behaviour](docs/brand-settings.md) for the full contract.

## Custom campaign briefs

Punch has three fixed goal policies and open-ended campaign direction. Keep the
Expand Down Expand Up @@ -186,6 +244,9 @@ artifact is not automatically ready for lawful sending.
## Current limits

- Anthropic is the only supported provider.
- Brand detection is conservative, not a pixel-perfect website clone. Colours
and fonts can be overridden; custom font files are not downloaded or embedded.
- Browser checks are not certification across all email clients.
- Input is one website plus one to six explicit product URLs.
- Punch does not discover products or crawl a catalogue.
- Safe forced directory replacement is unavailable in `0.1.0`; choose a fresh
Expand Down
123 changes: 123 additions & 0 deletions docs/brand-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Brand settings and the CLI

Punch keeps email layout in controlled React blocks. Branding is a separate,
validated input to those blocks, never model-written CSS or HTML.

## Five settings

| Setting | CLI flag | Meaning |
| ------------------ | --------------------- | ----------------------------------------- |
| `primaryColour` | `--primary-colour` | Main action colour and decorative accents |
| `backgroundColour` | `--background-colour` | Main email surface |
| `textColour` | `--text-colour` | Readable content ink |
| `headingFont` | `--heading-font` | One heading font family |
| `bodyFont` | `--body-font` | One body font family |

Colours use `#RRGGBB`. Font names are bounded plain family names, not CSS stacks,
URLs or font files. The renderer supplies fallback stacks. It does not download
or embed custom fonts; naming one does not guarantee the recipient has it.

The renderer derives cards, borders and supporting surfaces. It preserves the
primary colour on buttons and chooses readable button text. Links may use
readable ink instead of a low-contrast brand colour. Explicit text must meet
4.5:1 contrast against the background; an invalid manual combination is refused.
The guide offers a correction that the user must accept. Unreadable detected
text receives a labelled fallback.

## Conservative website detection

The existing bounded fetcher supplies HTML and same-origin CSS. Role extraction
recognises explicit root tokens such as `--primary`, `--color-primary`,
`--background`, `--text`, `--font-heading` and `--font-body`, plus unconditional
body, heading and button rules. It supports opaque hex/integer RGB values and
short local variable chains. Explicit tokens take priority over semantic rules.
Conflicting top-ranked candidates are omitted, not selected by stylesheet order.

This is not a browser-computed cascade or a pixel-perfect website clone. It does
not evaluate JavaScript, follow CSS imports, infer roles from arbitrary class
names, or choose between conditional/hover/dark-mode rules. Missing roles use
defaults. A website that provides only ambiguous signals may retain neutral
styling until the caller supplies overrides.

`result.brand` contains the resolved settings, per-slot `website`/`manual`/
`fallback` origins and warnings. These also live in `campaign.json`, independent
of optional traces. They contain no raw stylesheet or provider payload.

## Reusable profiles

```json
{
"version": "1",
"settings": {
"primaryColour": "#2563EB",
"backgroundColour": "#FFFFFF",
"textColour": "#172033",
"headingFont": "Verdana",
"bodyFont": "Arial"
}
}
```

Profiles may specify a subset of settings. `--brand` reads a bounded, regular
JSON file; symlinks, hardlinks and linked parent paths are refused. `--save-brand`
and the guide's save action require a new filename in an existing real parent
directory. There is no global profile, credential store or automatic saving.
The campaign and an external profile are separate saves: if the latter fails,
the CLI reports that the campaign was saved and leaves it intact.

Precedence is explicit flags → loaded profile → detected website roles →
fallbacks. During restyling, saved campaign settings replace website detection.

## Guided and automated use

Bare `punch` and incomplete `punch generate` invocations guide only when both
stdin and stdout are TTYs, no CI environment is detected, and prompting has not
been disabled. Complete commands bypass the guide unless `--interactive` is
present. `--json` and `--no-interactive` always win over `--interactive`.
Unknown flags and duplicate scalar flags fail before any question.

The guide collects sources and the brief, fetches the pages, then reviews brand
settings before optional voice inference or campaign generation. Source-fetch
resources are released before waiting for input. Ctrl-C, Ctrl-D and a declined
generation confirmation cancel without publishing an output bundle.

After generation: `p` opens a temporary browser preview, `b` adjusts branding,
`s` chooses a profile filename, and Enter exports. Temporary previews are removed
when the session ends. The final HTML remains in the chosen output directory.
Brand-only changes reuse the same semantic campaign and its generation usage.

`punch render --campaign <campaign.json> --output <new-directory>` works without
credentials or AI calls. It accepts a saved Punch campaign document or a canonical
semantic campaign. It does not reuse prior grounding claims: validation is
explicitly `render-only`, with zero model usage. Neither rendering mode sends
email or resolves the caller-owned compliance placeholders.

## TypeScript integration

```ts
import { generateCampaign, restyleCampaign, renderCampaign } from "punch-email";

const result = await generateCampaign(
{ website, products, goal: "sales", brand: { primaryColour: "#2563EB" } },
{ provider },
);

// In-memory restyling preserves the existing generation proof and usage.
const green = await restyleCampaign(result, { primaryColour: "#006644" });

// Independent rendering makes only render-validation claims.
const preview = await renderCampaign(result.campaign, result.brand?.settings);
```

A platform can supply its existing brand settings through the same `brand`
input; no CLI or platform-specific connector is required. An optional
`reviewBrand` callback can return overrides before model work begins. Ordinary
API calls remain non-interactive.

## Verification boundary

Tests cover role selection and ambiguity, strict settings, contrast, concurrent
theme isolation, single/six-product rendering, profile safety, TTY/CI gating,
manual correction, cancellation, preview cleanup and render-only reproducibility.
Browser fixtures check desktop/mobile layout. Browser proof is not certification
across Gmail, Outlook, Apple Mail or every installed font.
4 changes: 4 additions & 0 deletions docs/showcase/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Live Northstar Goods showcase

This is the original recorded generation, captured before brand-aware rendering
was added. Its neutral theme is retained as historical evidence. For the current
configurable renderer, see the [blue and dark brand examples](branding/README.md).

![Generated Northstar Goods campaign](northstar-campaign.png)

This is a real Punch generation from public HTTP input through Claude Sonnet 5,
Expand Down
61 changes: 61 additions & 0 deletions docs/showcase/branding/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# One campaign, two brand profiles

These are real browser captures of Punch's HTML renderer, not image-generated
mockups. Both use the same fictional Soft Orbit campaign. Only the brand
settings and viewport differ; the product, copy, price and destination links
are unchanged.

| Preview | Brand settings | Capture | Maximum README display width |
| -------------------------------- | ---------------------- | ------------- | ---------------------------- |
| [Blue desktop](blue-desktop.jpg) | [blue.json](blue.json) | 820 × 1000 px | 520 px |
| [Dark mobile](dark-mobile.jpg) | [dark.json](dark.json) | 390 × 1000 px | 250 px |

The JPEGs are original browser captures with no extra compression or upscaling.
They show the upper part of each email; the complete HTML includes the closing
CTA and Punch's compliance placeholders. The mobile layout is a real responsive
render, not a resized desktop screenshot. Installed fonts may affect line breaks
on another machine.

## Reproduce the HTML without an API key

From the repository root, after installing dependencies:

```bash
npm run build

node dist/cli/bin.js render \
--campaign docs/showcase/branding/campaign.json \
--brand docs/showcase/branding/blue.json \
--output ./showcase-blue

node dist/cli/bin.js render \
--campaign docs/showcase/branding/campaign.json \
--brand docs/showcase/branding/dark.json \
--output ./showcase-dark
```

Both output directories must be new. Open each `email.html` in a browser, or
add `--interactive` to review settings and open a preview through the CLI.
To try a different accent, add `--primary-colour "#006644"`; explicit flags
override the selected profile.

## Provenance and limits

[`campaign.json`](campaign.json) is the public
[single-product renderer fixture](../../../tests/fixtures/checkpoint-4/single-product.json)
with the placeholder product image omitted. It intentionally demonstrates
Punch's image-free rendering and requires no external image requests. Its
reserved `.example.com` links are fictional, not working shop destinations.

The screenshots were captured from the same content with local fixture links
for browser interaction checks. The committed campaign keeps the original
reserved-domain destinations; this does not change the visible rendering.

The profiles are explicit inputs, not evidence of automatic website detection.
No AI call, fresh source fetch or product-grounding claim is made by this
showcase. The render command produces `render-only` validation and zero model
usage. Browser verification is not certification across all email clients.

The separate [Northstar Goods live-generation record](../README.md) retains the
original end-to-end generation evidence. See [brand settings](../../brand-settings.md)
for detection behaviour, contrast checks and the TypeScript integration.
Binary file added docs/showcase/branding/blue-desktop.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions docs/showcase/branding/blue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": "1",
"settings": {
"primaryColour": "#2563EB",
"backgroundColour": "#FFFFFF",
"textColour": "#172033",
"headingFont": "Verdana",
"bodyFont": "Arial"
}
}
57 changes: 57 additions & 0 deletions docs/showcase/branding/campaign.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"schemaVersion": "0.1.0",
"goal": "product-launch",
"subject": "Meet the Pebble Weekender",
"preheader": "A soft-sided overnight bag with thoughtful space for the essentials.",
"blocks": [
{
"type": "header-standard",
"brandName": "Soft Orbit",
"homeUrl": "https://soft-orbit.example.com/",
"id": "block-01"
},
{
"type": "hero-stacked",
"eyebrow": "New for short escapes",
"heading": "Pack one more good day",
"body": "Meet the Pebble Weekender, arranged for overnight plans and easy unpacking.",
"id": "block-02"
},
{
"type": "product-feature",
"eyebrow": "The new arrival",
"productId": "product-01",
"name": "Pebble Weekender",
"description": "A soft-sided overnight bag with a wide zip opening, two interior pockets, and an adjustable woven strap.",
"price": { "amount": "148.00", "currency": "USD", "display": "$148" },
"cta": {
"label": "Explore the Pebble Weekender",
"href": "https://soft-orbit.example.com/products/pebble-weekender"
},
"id": "block-03"
},
{
"type": "heading",
"level": 2,
"text": "Small details, easier departures",
"id": "block-04"
},
{
"type": "body-paragraph",
"markdown": "A wide zip opening keeps the main compartment easy to scan, while **two interior pockets** separate smaller essentials.",
"id": "block-05"
},
{
"type": "cta-block",
"heading": "Ready for the next overnight?",
"body": "See the complete bag details before you pack.",
"actions": [
{
"label": "View the Pebble Weekender",
"href": "https://soft-orbit.example.com/products/pebble-weekender"
}
],
"id": "block-06"
}
]
}
Binary file added docs/showcase/branding/dark-mobile.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading