banner_studio/PHASE_1_BRIEF.md
Simeon Schecter d6347264f1 Doc hygiene: build sequence + slice-status honesty pass
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>
2026-05-18 20:59:14 -04:00

9.5 KiB
Raw Permalink Blame History

PHASE_1_BRIEF.md

Status (May 2026): Superseded by VERTICAL_SLICE.md for 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 in BUILD_SEQUENCE.md "Slice gaps" and SLICE_DEVIATIONS.md §1213. 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 at BUILD_SEQUENCE.md Phase 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:

  1. The BannerSpec type 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.
  2. 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.
  3. 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

  1. Monorepo scaffolded, all packages compiling, types shared end-to-end.
  2. Database up locally with full V1 schema (and the V2 Figma tables, empty).
  3. Layout engine passing unit tests for shrink-to-fit and push-siblings.
  4. 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 empty apps/web, apps/api, apps/render-worker and packages types, layout-engine, ad-server-profiles, qa-gates, prompts, db. Each package has its own package.json, tsconfig.json extending tsconfig.base.json, and an empty src/index.ts. Verify with turbo 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 in packages/types/src/, organized into the files listed in PROJECT_STRUCTURE.md. Do not invent additional fields. Do not rename anything. Include the Figma V2 optional fields exactly as specified. Export everything through src/index.ts. Verify with tsc --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 in packages/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 via infra/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-engine with Dropflow as a dependency. Write src/dropflow-wrapper.ts exposing a single function measureText(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 to min_font_size. Below that, return fits: false and 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 by heightDelta in the rule's direction, preserve the maintain_gap, and check the max_push ceiling. If any target would exceed max_push, return maxPushExceeded: 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 in layout_log per the ResolvedLayer type. Write an integration test using a fixture BannerSpec with 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/api with tRPC. Create one router: templateRouter with a single procedure template.getById that takes an ID, queries the database via packages/db, and returns a Template typed from packages/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/web with Next.js 14 App Router. Add the tRPC client wired to apps/api. Create a single route /templates/[id] that calls template.getById and renders the JSON. No styling, no Konva, no Zustand. The goal is to prove the type chain works: editing a field in packages/types/src/template.ts should cause a TypeScript error in apps/web if 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_log data.
  • 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.