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>
9.5 KiB
PHASE_1_BRIEF.md
Status (May 2026): Superseded by
VERTICAL_SLICE.mdfor the short-timeline build path. The slice is scaffolded through Day 6 (four IAB sizes, four-agent AI pipeline, multi-size review grid, per-row HTML5 export) with known reviewer-surface gaps documented inBUILD_SEQUENCE.md"Slice gaps" andSLICE_DEVIATIONS.md§12–13. The architectural pieces this brief was designed to land — types, layout engine, end-to-end type safety — did land. This brief is preserved as historical record of the original zero-state plan. For the V1 continuation, start atBUILD_SEQUENCE.mdPhase 2.
This is the unblock-the-first-week brief. The full 14-phase plan is in BUILD_SEQUENCE.md. This document is what you hand Claude Code at the start to get out of zero-state cleanly.
The architecture document is explicit: do not start with the canvas. Start with the data layer. Reasons:
- The
BannerSpectype is the contract that touches every other component. If you build the canvas first, you back into the type from UI needs and end up with a shape the render worker and AI orchestration can't cleanly consume. - The layout engine has zero UI dependencies and is the highest-risk technical area. Building it in isolation, with unit tests, before any canvas exists, means you can prove the math before you build the interface that depends on it.
- The version service's override-preservation algorithm is the hardest single piece of logic in the product. Get it tested and stable before any UI is wired to it.
Phase 1 goals
- Monorepo scaffolded, all packages compiling, types shared end-to-end.
- Database up locally with full V1 schema (and the V2 Figma tables, empty).
- Layout engine passing unit tests for shrink-to-fit and push-siblings.
- A trivial tRPC route that fetches a template by ID, end-to-end type-safe from DB to a stub web client.
No UI work, no AI calls, no rendering. The goal is a foundation that everything else slots into without rework.
Step-by-step prompts for Claude Code
Each numbered step is one focused session. Don't combine them — small batches make debugging cheap.
Step 1.1 — Scaffold the monorepo
Set up a Turborepo monorepo at the project root following the structure in
PROJECT_STRUCTURE.md. Use pnpm workspaces. Create emptyapps/web,apps/api,apps/render-workerand packagestypes,layout-engine,ad-server-profiles,qa-gates,prompts,db. Each package has its ownpackage.json,tsconfig.jsonextendingtsconfig.base.json, and an emptysrc/index.ts. Verify withturbo run build— everything should build (producing nothing) without errors.
Acceptance: turbo run build is green. No TypeScript errors. No app code yet.
Step 1.2 — Build packages/types from the architecture document
Read Part 4 of
ARCHITECTURE.md. Implement every interface in that section inpackages/types/src/, organized into the files listed inPROJECT_STRUCTURE.md. Do not invent additional fields. Do not rename anything. Include the Figma V2 optional fields exactly as specified. Export everything throughsrc/index.ts. Verify withtsc --noEmit.
Acceptance: Every type in Part 4 exists. import { BannerSpec, Template, Campaign } from '@banner-studio/types' works from a sibling package. No deviations from the spec — flag any ambiguity before guessing.
Step 1.3 — Build packages/db with Drizzle
Read Part 7 of
ARCHITECTURE.md. Implement the database schema using Drizzle ORM inpackages/db/src/schema/. Create one schema file per table group. Include all V1 tables AND all V2 Figma tables (they exist empty in V1). Add the indexes listed at the bottom of Part 7. Configure Drizzle to generate a migration. Set up a local Postgres viainfra/compose/docker-compose.yml(Postgres + Redis + Minio for S3 emulation).
Acceptance: pnpm db:migrate runs cleanly against the local Postgres. All tables exist. The Drizzle types compile and align with the types in packages/types for fields that overlap (campaigns.brief is Brief, campaign_versions.spec is BannerSpec | null, etc.).
Step 1.4 — Layout engine: Dropflow WASM bootstrap
Initialize
packages/layout-enginewith Dropflow as a dependency. Writesrc/dropflow-wrapper.tsexposing a single functionmeasureText(text: string, typography: TypographySpec, maxWidth: number): { width: number, height: number, lines: number }. Add Vitest. Write three unit tests: short text fits on one line, long text wraps, very long text exceeds reasonable max height. Run them in Node — no browser environment.
Acceptance: All three tests pass. No browser globals referenced. The wrapper is pure (no module-level state besides the WASM instance).
Step 1.5 — Layout engine: shrink-to-fit
Implement
src/shrink-to-fit.ts. Function signature:shrinkToFit(text: string, typography: TypographySpec, container: { width: number, height: number }, behavior: TextBehaviorRules): { fontSize: number, fits: boolean, requiredHeight: number }. The algorithm: measure with current font size, if it fits return it, otherwise reduce 1px and try again, down tomin_font_size. Below that, returnfits: falseand the required height at min_font_size. Write four tests: fits at default, fits after shrinking, hits min_font_size and overflows, hits min_font_size and fits exactly.
Acceptance: All four tests pass. Behavior matches the algorithm in Part 5 of the architecture document.
Step 1.6 — Layout engine: push-siblings cascade
Implement
src/push-siblings.ts. Function signature:applyPushSiblings(layers: ResolvedLayer[], heightDelta: number, sourceLayerId: string, rules: PushSiblingRule[]): { layers: ResolvedLayer[], maxPushExceeded: boolean }. Walk the rules, move each target byheightDeltain the rule's direction, preserve themaintain_gap, and check themax_pushceiling. If any target would exceedmax_push, returnmaxPushExceeded: true. Write tests for: single sibling pushed cleanly, cascading push (sibling A pushes B which pushes C), push exceeds max, push respects maintain_gap.
Acceptance: All tests pass. The cascade order matches Part 5 step-by-step trace.
Step 1.7 — Layout engine: top-level resolveLayout
Implement
src/resolve-layout.ts. This is the entry point the render worker and the canvas both call. Function signature:resolveLayout(spec: BannerSpec, copy: Record<string, string>): { resolved: ResolvedLayer[], anyOverflow: boolean }. For each layer in each artboard: if it's a TextLayer, run shrink-to-fit; if it overflowed at min_font_size, expand the container and call push-siblings; record everything inlayout_logper theResolvedLayertype. Write an integration test using a fixtureBannerSpecwith a text group, simulating short copy (no movement) and long copy (cascade fires).
Acceptance: The fixture test passes both cases. layout_log contains accurate trace data. The function is pure (no side effects).
Step 1.8 — apps/api: minimum viable tRPC
Scaffold
apps/apiwith tRPC. Create one router:templateRouterwith a single proceduretemplate.getByIdthat takes an ID, queries the database viapackages/db, and returns aTemplatetyped frompackages/types. Set up the tRPC root, an Express or Fastify server, and a basic auth context stub (no Clerk yet). Verify by hitting the endpoint with curl and seeing a typed response.
Acceptance: A real query against the database returns a Template. The type at the call site matches the type in packages/types exactly (verify by importing and assigning).
Step 1.9 — apps/web: minimum viable client
Scaffold
apps/webwith Next.js 14 App Router. Add the tRPC client wired toapps/api. Create a single route/templates/[id]that callstemplate.getByIdand renders the JSON. No styling, no Konva, no Zustand. The goal is to prove the type chain works: editing a field inpackages/types/src/template.tsshould cause a TypeScript error inapps/webif the field is referenced there.
Acceptance: Editing the Template interface causes errors to propagate to the web app on next type check. End-to-end type safety verified.
Phase 1 exit criteria
When all 9 steps are green:
- The monorepo builds clean.
- A template can be inserted into the database and fetched through tRPC with full type safety.
- The layout engine resolves a fixture BannerSpec with text group cascade behavior and produces accurate
layout_logdata. - No UI work has been done. No AI calls have been made. No rendering has happened.
This is the moment to commit, tag v0.1.0-foundation, and begin Phase 2.
What goes wrong here, and what to do about it
Dropflow WASM bundling. WASM modules need a loader pattern that works in both Node (render worker) and the browser (Konva preview). Verify both environments in Step 1.4. If one breaks, fix the bundling before continuing — it will haunt every later phase.
Drizzle and JSONB. The brief, spec, and delta columns are JSONB. Drizzle's typing for JSONB is unknown by default. Use Drizzle's $type<Brief>() annotation to recover the typed shape. Confirm at compile time, not at runtime.
Type drift between layers. It is tempting to "just add a field" in the API layer or the canvas state. Do not. Every type lives in packages/types. If it needs a new field, it goes there first, then propagates. This discipline is what makes the contract work.
Premature optimization on the layout engine. It will not be fast initially. That is fine. Correctness first, then speed. Optimization comes after the canvas is wired in Phase 5 and you have real workloads to profile.