Sweep across the planning docs so a future session opening any of them sees the same picture of where the slice actually landed and which docs to load before touching a given phase. BUILD_SEQUENCE.md: - Reworded status header from "slice has shipped" to honest framing: scaffolded through Day 6, architectural pieces proven, reviewer surface and hero-image rendering not demo-ready. - Added "How to use this document" explaining the reference-docs block pattern. - Added "What V1 inherits from the slice" inventory mapping shipped work onto Phases 1-3 (foundation), 5 (TypeSystem), 8 (orchestration), 11 (render), 12 (review), 13 (export). Plus "What V1 replaces." - Added "Slice gaps" subsection enumerating the two items that block Phase 12 from inheriting the review page as-is. - Added Reference docs blocks to every phase (1-14), priority-ordered. - Wired ANIMATION_V1.md forward-pointers specifically into Phase 6 (asset selection consults unionBoundingBox), Phase 7 (crop tool must respect required_source_size), Phase 8 (Assemble agent emits AnimationTimeline referencing preset names only), Phase 11 (GSAP via s0.2mdn.net CDN, SplitType bundled, force-prefers-reduced-motion), Phase 13 (G1-G12 supersede general gates for animation checks). - Added validator additions in Phase 8: animation preset whitelist, duration cap, loop cap. - Added Document index at the bottom listing every referenced doc. PHASE_1_BRIEF.md: - Added superseded banner pointing to VERTICAL_SLICE.md and BUILD_SEQUENCE.md Phase 2. Notes that the architectural pieces this brief was designed to land did land; the reviewer-surface gaps are documented in SLICE_DEVIATIONS.md sections 12-13. SLICE_DEVIATIONS.md: - Added section 12: hero-image rendering on Konva unverified across the four IAB sizes. Earlier rounds shipped with broken placeholder URLs; the Unsplash swap fixed the URL but not the full pipeline (load-gating the GSAP timeline, CORS, per-size crop/fit, parity vs. Playwright PNG). V1 reversal: dedicated Konva image-pipeline pass before Phase 12 inherits the review UI. - Added section 13: the review page is a developer scaffold. Empty, loading, and error states, drawer affordance, export feedback, and information hierarchy are not designed. V1 reversal: Phase 12 starts from a design pass over the existing data shape, not from the slice's page shell. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
16 KiB
SLICE_DEVIATIONS.md
Intentional deviations from CLAUDE.md / ARCHITECTURE.md made for the
vertical-slice phase. Each one has a documented reversal path for V1. None
of them break the BannerSpec contract or the layout-engine parity guarantee.
1. Prompts package lives at packages/prompts, not /prompts/ at repo root
CLAUDE.md says: "Every prompt is versioned in /prompts/, never hardcoded."
Slice does: Workspace package @banner-studio/prompts under
packages/prompts/. Each prompt file (extract.ts, generate.ts) exports
a version string, the system prompt, a userTemplate(input) function,
and the tool/schema definition.
Why: Importing from '@banner-studio/prompts' is cleaner than from
'../../../prompts/...' paths, and pnpm workspace deps give us proper
TypeScript types for free. The intent of the rule — prompts as
versioned, first-class, non-embedded artifacts — is preserved.
V1 reversal: Either move to /prompts/ at repo root (rename only,
no code changes), or keep the workspace package and update CLAUDE.md.
Decision deferred.
2. AI orchestration is packages/api-lib, not apps/api/services/ai-orchestration/
VERTICAL_SLICE.md / ARCHITECTURE.md say: AI orchestration is one of
eight services, owned by apps/api.
Slice does: Single workspace package @banner-studio/api-lib
containing the Claude client, feed loader, agents (extract, generate,
route-node, assemble), and the orchestrator. Consumed by an in-process
Next.js API route at apps/web/app/api/generate/route.ts. There is no
separate apps/api process for the slice.
Why: The slice has one user, one machine, one feed file. Splitting the API into its own service buys us nothing and costs us deploy complexity. The library boundary is correct; the process boundary is deferred.
V1 reversal: Move packages/api-lib under apps/api/services/
(or keep it as a package consumed by an apps/api tRPC server). The
public functions — loadFeed, orchestrateRow, callClaudeWithTool —
are designed to be process-boundary-callable already.
3. SmartAssetLayer.direct_url field
ARCHITECTURE.md says: Asset selection is a deterministic metadata query against a variant group's selection rules. There is no direct URL on a layer.
Slice does: Added an optional direct_url?: string to
SmartAssetLayer (commented // SLICE_ONLY: in packages/types/src/index.ts).
The route-node agent writes row.hero_image_url straight into this
field and sets selected_variant_id = 'feed-row' as a synthetic marker.
Why: Variant groups and the asset library are out of scope for the slice. The CSV gives us a URL per row; we need to get it onto the spec without inventing infrastructure.
V1 reversal: When the asset service ships, the route-node agent will
look up a variant group, apply selection rules, and write a real
selected_variant_id. The render worker will resolve the variant to a
URL at render time. Drop the direct_url field from the type. All
callers that read direct_url are isolated to the slice render path.
4. Single model — claude-sonnet-4-6 — instead of the opus/sonnet/haiku tiering
CLAUDE.md says:
- Orchestration: claude-opus-4
- Generation: claude-sonnet-4
- Validation: claude-haiku-4
Slice does: All Claude calls go through claude-sonnet-4-6 via the
DEFAULT_MODEL constant in packages/api-lib/src/claude-client.ts.
Why: The slice has no real orchestration choices (the pipeline is linear and fixed) and no validation step that benefits from a cheaper model (validation is programmatic — char counts, zod schemas). Sonnet is the right tool for the generation step, which is the only step doing non-trivial work.
V1 reversal: Add per-call model overrides at callClaudeWithTool
call sites. Extract agent → haiku, generate agent → sonnet, orchestrator
escalations → opus. The model parameter is already plumbed through.
5. dropflow wasm-locator subpath imported by file path
The constraint: dropflow 0.5.1's package.json exports field does
not include ./wasm-locator.js or ./dist/src/api-wasm-locator-browser.js,
even though dropflow's own runtime error suggests importing it.
Slice does: A single quarantined file — packages/layout-engine/src/browser/wasm-locator.ts —
imports dropflow/dist/src/api-wasm-locator-browser.js directly by deep
file path. Pinned dropflow to exact 0.5.1 (no caret). A webpack alias
in apps/web/next.config.mjs resolves the subpath at build time.
Why: The browser path of the layout engine cannot initialize without
the wasm locator. Upstream dropflow has not exported a stable entry for
it. Going through dist/src/... is brittle but isolated.
V1 reversal: File a dropflow issue requesting ./wasm-locator.js in
the exports map. When fixed, replace the quarantined import with the
public subpath import, drop the next.config.mjs alias, allow the
dropflow caret range again. Single-file change.
6. Render worker is packages/render-worker, not apps/render-worker
ARCHITECTURE.md says: Render is its own service (Playwright + BullMQ
in Docker), conceptually apps/render-worker.
Slice does: Workspace package @banner-studio/render-worker
consumed in-process by apps/web/app/api/export/route.ts. The route
dynamic-imports renderMany, which launches a single headless Chromium
and renders specs through isolated browser contexts (concurrency capped
at 3). No queue, no Docker, no IPC.
Why: The slice exports a handful of banners synchronously from a
single user action; a queue + dedicated worker container buys nothing.
The library shape — renderMany({ specs, outputDir }) — is the same
function the V1 worker will call from its BullMQ consumer.
V1 reversal: Move the package to apps/render-worker, wrap
renderMany in a BullMQ consumer, swap the in-process import for an
HTTP/queue dispatch. The pure logic (buildRuntimeHtml, renderToZip,
safeSlug) moves unchanged.
7. Rendered zips reference the hero image by remote URL
IAB / production trafficking expects: Self-contained zips — every
asset URL inside index.html must resolve to a file inside the zip.
Slice does: The runtime HTML emits the hero image's direct_url
verbatim (<img src="https://…">). Fonts and GSAP are inlined; the hero
is not.
Why: Asset bundling needs the same Sharp-based pipeline that the asset service will eventually own (fetch, content-hash, optimise, re-write). That pipeline is squarely out of slice scope.
V1 reversal: Add a bundleAssets({ spec }) step before
buildRuntimeHtml: fetch each direct_url over HTTP, write to
assets/<hash>.<ext> inside the zip, rewrite the spec's direct_url
fields to relative paths. Local-only change inside render-to-zip.
8. CM360 click-macro variant deferred — IAB clickTag only
ARCHITECTURE.md says: CM360 is the first non-IAB ad-server profile.
Slice does: ad_server_profile is locked to 'iab_standard'; the
runtime hardcodes var clickTag = "<url>" from
click_destinations[0].url and wires it into a single <a> element.
No clickTagN, no exit URLs, no DCM macro insertion.
Why: The Day-4 gate is "click tag works" — the simplest possible
IAB-compliant click flow. CM360's macro shape (%%CLICK_URL_UNESC%%)
requires either a real CM360 trafficking sheet output or a fake
substitution layer; neither pulls demo weight.
V1 reversal: Add packages/ad-server-profiles/cm360.ts exporting a
buildClickRuntime(profile, destinations) function. Branch the runtime
template on spec.ad_server_profile.
9. Multi-size in the slice (4 artboards instead of 1)
VERTICAL_SLICE.md originally said: "One template, hand-built in code… A single 300x250 artboard… Multi-artboard linked sets (one size only) — out of scope."
Slice now does: One template containing four artboards: 300x600
(reference), 300x250, 728x90, 160x600. Same Template shape from the V1
architecture (artboards: Artboard[] was already an array); we just
populate four entries instead of one.
Why: Single-size obscures the thesis. The product value is in scale — one designed system, many adaptive outputs. A demo with one size reads as "AI rewrites copy." A demo with four sizes reads as "humans design, AI scales," which is the actual claim. The V1 architecture supported multi-artboard from day one; the slice was scoped down for time, not for data-model reasons.
V1 reversal: None needed. Multi-artboard is the V1 shape. The slice restores it. Variant groups, the asset library, and linked-set conflict resolution are still V1 work, layered on top of what the slice ships.
10. TypeSystem — designer-authored, formula-derived
ARCHITECTURE.md says: Each TextLayer carries a TypographySpec
with raw font_size, font_weight, line_height, etc. There is no
shared system across layers or artboards.
Slice does: Adds a TypeSystem interface on Template declaring the
four roles (headline, subheadline, cta, legal) with base size,
weight, line-height, tracking, color, and legibility floor each.
Authored once on the 300x600 reference artboard. A new module —
packages/layout-engine/src/type-scale.ts — exposes
deriveTypeSpec(system, target) which returns per-role TypographySpec
values for any target artboard using piecewise size-class math
(half-page, rectangle, leaderboard, skyscraper) with a width-correction
term.
When resolveLayout runs, it shrinks the derived font down to 85% of
the formula-derived size to fit the actual copy. If still overflowing at
the floor, it emits a constraint_signal on the resolved spec carrying
the max characters that would fit. The orchestrator reads this signal
and re-invokes the Generate agent with a tighter per-size character
limit. One retry, then flag as failed.
Why: Per-size hand-authored typography doesn't scale beyond a handful of templates. Encoding role relationships once and deriving sizes by formula is how a designer actually thinks about a multi-size system. Tying the layout engine's overflow signal back to the AI's copy generation is the lever that turns the four-agent pipeline from "copy rewriter" into "copy adapter" — adaptation against system-emitted constraints, per surface.
V1 reversal: The TypeSystem concept should graduate to V1
architecture, not be removed. It's a slice-originated artifact worth
keeping. Folding it into ARCHITECTURE.md Part 5 (Text Group System) is
the natural V1 step.
11. BannerSpec.ai_reasoning.per_size_decisions — forward-pointer to the resolved feed
ARCHITECTURE.md says: ai_reasoning carries flat strings (asset_selection,
copy_rationale, variant_selection, animation_rationale). Per-size editorial
decisions are not captured as structured data in the spec.
Slice does: Adds an optional per_size_decisions array on
ai_reasoning capturing each per-size editorial decision the AI made,
along with the constraint signal (if any) that drove it. Shape:
per_size_decisions?: Array<{
artboard_id: string;
role: 'headline' | 'subheadline' | 'cta' | 'legal';
reason: 'baseline' | 'constraint_emitted';
constraint_signal?: {
max_chars_at_floor: number;
derived_font_size: number;
floor_font_size: number;
};
decided_at: string;
}>
Why: This is the audit-trail data that V1's Resolved Creative Feed
(see RESOLVED_FEED.md) will carry as per-size cells. The slice has no
persistence, no campaign reruns, no human editing — so the resolved feed
doesn't earn its weight yet. But the editorial decisions are still
real and worth surfacing in the demo reasoning panel. Capturing them as
a flat array on the spec lets the demo show the "AI decided differently
per size, here's why" story, and gives V1 a zero-friction migration path:
each per_size_decisions entry maps one-to-one onto a future resolved-feed
cell.
V1 reversal: Move this data out of ai_reasoning and into the
resolved_cells table (RESOLVED_FEED.md schema). Each entry becomes a
row keyed by (campaign, product, size, field) with cell_source set
to 'constraint_emitted' or 'generated_baseline'. The ai_reasoning
field reverts to flat-strings only, or grows other structured fields as
they're needed.
12. Hero-image rendering on Konva is unverified across sizes
The expectation: Every layer in the resolved spec — including the photographic hero — renders identically in the Konva preview and in the Playwright-rendered HTML5 output. This is the parity guarantee the layout engine exists to enforce.
Slice does: The Konva grid reliably renders text and shape layers.
The hero image, loaded into a Konva Image node via the
SmartAssetLayer.direct_url field (see §3), has not been validated to
render consistently across the four IAB sizes (300x600, 300x250, 728x90,
160x600). Earlier rounds shipped with broken placeholder URLs in
feeds/demo.csv; the Unsplash swap fixed the URL but did not prove the
full Konva image pipeline. Cross-origin loading, async image readiness
before the GSAP timeline starts, and crop/fit behavior at the four
aspect ratios are all unverified.
Why: The slice prioritized proving the layout engine, type system, constraint-signal retry, and multi-size orchestration. Image fidelity on canvas was assumed working from early manual checks; it was never systematically validated, and the placeholder URL bug masked the gap.
V1 reversal: Before Phase 12 inherits the review UI, a dedicated pass on the Konva image pipeline:
useImage(react-konva) or equivalent loader gates the GSAP master timeline until images are ready.- Cross-origin and CORS behavior validated against the Unsplash CDN (and whatever CDN V1's asset service uses).
- Per-size crop/fit confirmed against the resolved
SmartAssetLayerrect. - Visual parity verified against the Playwright PNG output at all four sizes.
13. /review UI is a developer scaffold, not a reviewer surface
The intent: /review is the surface a Creative Director or
Trafficker uses to approve a campaign run. It needs to communicate
clearly: what was generated, what the AI decided, what the constraints
were, and a one-click path to export.
Slice does: A functional but unrefined page — per-row strips, four sizes side-by-side, the AI Reasoning drawer, the export gate — all rendered with developer-grade layout. Empty states, loading states, error states, image-loading flicker, drawer scroll/affordance, export feedback, and the overall information hierarchy have not been designed. The page proves the data flow; it does not yet present it.
Why: Reviewer-facing polish is downstream of "the data is real and correct." Days 3–6 were spent making the data real and correct. The UI pass was deferred so the slice didn't ship with a polished surface hiding broken plumbing.
V1 reversal: Phase 12 in BUILD_SEQUENCE.md (Approval workflow) is
where this lands. It should not start from the slice's /review page
as a base — it should start from a design pass that takes the existing
data shape (per-row groupings, per_size_decisions, constraint
signals, the export gate) and builds the reviewer surface around it.
The Konva grid component and the data hooks are reusable; the page
shell is not.
Things that are NOT deviations (in case it looks like they might be)
- Resolved BannerSpec from orchestrator. VERTICAL_SLICE.md line 161
asks for a typed BannerSpec; running
resolveLayoutas the last orchestration step is the most faithful reading of "fully resolved". - Promoted demo template to
packages/layout-engine/src/templates/. CLAUDE.md doesn't forbid templates inside the layout-engine package; the slice has one template and it lives next to the engine that consumes it. - Browser layout engine subpath export (
@banner-studio/layout-engine/browser). Subpath exports are a standard pnpm pattern and a logical fit for "same engine, two runtimes." - In-process Next.js for
/api/generate. Allowed by the slice; upgrading to a separateapps/apiservice is the V1 task.