banner_studio/PROJECT_STRUCTURE.md
Simeon Schecter ccbdb47162 Day 5+6 of the vertical slice: multi-size + per-row strips
Expands the slice from a single 300x250 banner to four IAB sizes
(300x600, 300x250, 728x90, 160x600) driven by a designer-authored
TypeSystem and a per-row strip review surface.

Layout engine
- TypeSystem with role-based typography (headline/subheadline/cta/legal)
  and piecewise size-class derivation: half_page / rectangle /
  leaderboard / skyscraper / mobile_banner.
- resolveLayout now derives per-size font/leading from the role +
  artboard size, then clamps to a legibility floor and emits a
  constraint_signal when copy does not fit at the floor.
- Four reference templates with character constraints per size.

AI pipeline (Shape B)
- One extract + one generate per feed row; generate returns per-size
  copy keyed by artboard_id plus a shared rationale block.
- Constraint-signal retry: orchestrator tightens per-(artboard, field)
  limits and re-calls generate before giving up.
- orchestrateRow returns specs[] + rationale + constraint_signals.

Review UI
- /review renders one strip per feed row, all four sizes side-by-side
  at true pixel dimensions, synced on a single GSAP master timeline.
- AiReasoningDrawer shows a per-size copy table, shared rationale, and
  any constraint signals that fired.
- /api/generate response grouped by row; /api/export accepts the same
  shape and writes exports/row-N/artboard_id.zip.

Render worker
- render-to-zip / render-many accept optional subdir + filename
  overrides so multi-size exports can be grouped by feed row.

Docs
- VERTICAL_SLICE and BUILD_SEQUENCE updated for the multi-size scope.
- RESOLVED_FEED.md documents the V1 Resolved Creative Feed proposal.
- SLICE_DEVIATIONS.md records where the slice diverges from V1.

Tests: 56 pass (28 layout-engine + 14 api-lib + 14 render-worker).
Web app: tsc clean, next build succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 14:22:26 -04:00

13 KiB

PROJECT_STRUCTURE.md

Scaffolding guide for the agentic banner platform monorepo. Use this as the literal target when initializing the project.


Top-level layout

banner-studio/
├── apps/
│   ├── web/                    Next.js 14 (App Router) — UI
│   ├── api/                    tRPC server + service layer
│   └── render-worker/          Playwright + BullMQ consumer
├── packages/
│   ├── types/                  Shared TypeScript contract
│   ├── layout-engine/          Dropflow WASM wrapper
│   ├── ad-server-profiles/     Click tag + weight rules per server
│   ├── qa-gates/               Programmatic QA checks
│   ├── prompts/                Versioned prompt templates
│   └── db/                     Drizzle schema + migrations
├── infra/
│   ├── docker/
│   │   └── render-worker/      Fonts baked, sub-pixel disabled
│   └── compose/                Local dev (postgres, redis, minio)
├── prompts-vault/              Read-only canonical prompts (mirrored to packages/prompts at build)
├── turbo.json
├── package.json
├── tsconfig.base.json
├── CLAUDE.md
├── ARCHITECTURE.md
├── PROJECT_STRUCTURE.md
├── PHASE_1_BRIEF.md
├── BUILD_SEQUENCE.md
└── RESEARCH.md

packages/types

The single source of truth. Every other package and app imports from here. Build this first.

packages/types/
├── src/
│   ├── template.ts           Template, Artboard, GlobalConstraints
│   ├── layers.ts             BaseLayer, TextLayer, SmartAssetLayer, GroupLayer
│   ├── text-behavior.ts      TextBehaviorRules, PushSiblingRule, CharacterConstraints
│   ├── variants.ts           VariantGroup, AssetVariant, VariantMetadata, SelectionRule
│   ├── assets.ts             Asset, AssetRights, AssetMetadata, ApprovedCrop
│   ├── campaign.ts           Campaign, CampaignStatus, Brief, CopyConstraints
│   ├── versioning.ts         CampaignVersion, HumanOverride, Conflict, ConflictReport
│   ├── banner-spec.ts        BannerSpec, ArtboardSpec, ResolvedLayer  ← the contract
│   ├── ad-server.ts          AdServerProfile, ClickDestination
│   ├── qa.ts                 QAResult, QAGateDefinition
│   ├── export.ts             ExportPackage, TraffickingSheetRow
│   └── index.ts              Barrel export
├── package.json
└── tsconfig.json

Exact shapes are in Part 4 of ARCHITECTURE.md. Do not modify them — they are the contract. If a shape needs to change, change it here and propagate.


packages/layout-engine

The keystone package. Same code runs in browser (Konva preview) and Node (render worker).

packages/layout-engine/
├── src/
│   ├── dropflow-wrapper.ts   WASM init, text measurement primitives
│   ├── shrink-to-fit.ts      Recursive font reduction algorithm
│   ├── push-siblings.ts      Cascade logic for flex_vertical groups
│   ├── type-scale.ts         Designer-authored TypeSystem → per-size TypographySpec
│   ├── resolve-layout.ts     Top-level: spec → ResolvedLayer[]
│   ├── layout-log.ts         Build the debug trace
│   └── index.ts
├── test/                     Unit tests are mandatory here
└── package.json

Rules:

  • Pure functions. No DOM access. No fetch. No console.
  • Deterministic — same input always produces same output.
  • The resolve-layout entry point takes a BannerSpec and a GeneratedCopy map, returns ResolvedLayer[] with full layout_log.

packages/prompts

Prompts are first-class. Each file exports:

  • A system prompt
  • A user message template with typed slots
  • A JSON Schema for valid output
  • Few-shot examples (valid + invalid with explanations)
  • A semver version string
packages/prompts/
├── src/
│   ├── extract-context/
│   │   ├── v1.0.0.ts         Initial version
│   │   ├── v1.1.0.ts         If iterated
│   │   └── current.ts        Re-exports the active version
│   ├── generate-copy/
│   │   ├── v1.0.0.ts
│   │   └── current.ts
│   ├── assemble-spec/
│   │   ├── v1.0.0.ts
│   │   └── current.ts
│   ├── shared/
│   │   ├── output-schemas.ts JSON Schema definitions
│   │   └── examples.ts       Reusable few-shot examples
│   └── index.ts
└── package.json

The current.ts indirection lets you roll back a prompt by changing one export, not by editing call sites.


packages/db

Drizzle ORM. Schema mirrors Part 7 of ARCHITECTURE.md exactly.

packages/db/
├── src/
│   ├── schema/
│   │   ├── campaigns.ts
│   │   ├── campaign-versions.ts
│   │   ├── human-overrides.ts
│   │   ├── version-conflicts.ts
│   │   ├── templates.ts
│   │   ├── assets.ts
│   │   ├── figma-sync.ts        Tables exist, empty in V1
│   │   └── index.ts
│   ├── client.ts                Postgres connection
│   └── index.ts
├── drizzle.config.ts
├── migrations/
└── package.json

packages/ad-server-profiles

packages/ad-server-profiles/
├── src/
│   ├── profiles/
│   │   ├── iab-standard.ts
│   │   ├── cm360.ts
│   │   ├── amazon-dsp.ts      Defined, not wired in V1
│   │   └── xandr.ts           Defined, not wired in V1
│   ├── click-tag/
│   │   ├── iab.ts             window.clickTag pattern
│   │   ├── cm360.ts           Same but with the CM360 init quirk
│   │   ├── amazon.ts
│   │   └── xandr.ts           APPNEXUS.getClickTag()
│   ├── weight-rules.ts
│   └── index.ts
└── package.json

packages/qa-gates

packages/qa-gates/
├── src/
│   ├── gates/
│   │   ├── weight-initial.ts
│   │   ├── weight-total.ts
│   │   ├── animation-duration.ts
│   │   ├── character-counts.ts
│   │   ├── click-tag-present.ts
│   │   ├── asset-rights.ts
│   │   ├── backup-png.ts
│   │   ├── cta-approved.ts
│   │   └── color-contrast.ts
│   ├── runner.ts              Executes all applicable gates
│   └── index.ts
└── package.json

Each gate is a pure function: (spec, profile, files, campaign) => QAResult.


apps/api

apps/api/
├── src/
│   ├── routers/                tRPC routers, one per service
│   │   ├── template.ts
│   │   ├── asset.ts
│   │   ├── brief.ts
│   │   ├── campaign.ts
│   │   ├── version.ts
│   │   ├── ai.ts               Triggers orchestration, returns job id
│   │   ├── render.ts           Triggers render, returns job id
│   │   ├── qa.ts
│   │   ├── export.ts
│   │   └── index.ts
│   ├── services/               Business logic, called by routers
│   │   ├── template-service.ts
│   │   ├── asset-service.ts
│   │   ├── brief-service.ts
│   │   ├── version-service.ts    Houses override-merge algorithm
│   │   ├── ai-orchestration/
│   │   │   ├── extract-agent.ts
│   │   │   ├── generate-agent.ts
│   │   │   ├── route-node.ts     Deterministic — no AI
│   │   │   ├── assemble-agent.ts
│   │   │   ├── validator.ts      Programmatic post-AI checks
│   │   │   └── orchestrator.ts   Wires the four stages
│   │   ├── render-service.ts     Enqueues, polls
│   │   ├── qa-service.ts
│   │   └── export-service.ts
│   ├── lib/
│   │   ├── claude-client.ts      Wraps Anthropic SDK
│   │   ├── feed-validator.ts     Schema validation, DLQ routing
│   │   └── ...
│   ├── queue/
│   │   ├── render-queue.ts       BullMQ producer
│   │   └── dlq.ts                Dead letter handlers
│   ├── trpc.ts                   Router root, auth context
│   └── server.ts
└── package.json

apps/render-worker

apps/render-worker/
├── src/
│   ├── worker.ts               BullMQ consumer entry point
│   ├── render/
│   │   ├── render-html5.ts     Spec → HTML5 zip
│   │   ├── render-image.ts     Spec → PNG/JPEG/WebP via Playwright
│   │   ├── render-backup-png.ts
│   │   └── compose-zip.ts      Per-ad-server zip structure
│   ├── runtime/
│   │   ├── runtime-template.html   The HTML5 banner shell
│   │   ├── gsap-init.ts            GSAP timeline assembly from spec
│   │   └── click-tag-injector.ts
│   └── playwright/
│       ├── browser-pool.ts     Persistent browser, isolated contexts
│       └── screenshot.ts
├── Dockerfile                  Fonts baked in here
└── package.json

The render worker imports @banner-studio/layout-engine — same WASM module that runs in the browser. This is non-negotiable.


apps/web

apps/web/
├── src/
│   ├── app/                    Next.js App Router
│   │   ├── templates/
│   │   │   ├── page.tsx        List
│   │   │   ├── new/page.tsx
│   │   │   └── [id]/
│   │   │       ├── page.tsx    Template builder
│   │   │       └── confirm/page.tsx  (V2 Figma import landing)
│   │   ├── assets/
│   │   │   ├── page.tsx        Asset library
│   │   │   └── [id]/page.tsx   Asset detail + crop tool
│   │   ├── campaigns/
│   │   │   ├── page.tsx        List
│   │   │   ├── new/page.tsx    Brief or feed intake
│   │   │   └── [id]/
│   │   │       ├── page.tsx    Review grid
│   │   │       ├── history/page.tsx     Version timeline
│   │   │       └── export/page.tsx      Export + trafficking sheet
│   │   └── layout.tsx
│   ├── features/               Feature folders, co-located logic + UI
│   │   ├── template-builder/
│   │   │   ├── canvas/         Konva components
│   │   │   ├── inspectors/     Layer property panels
│   │   │   ├── character-simulator/
│   │   │   ├── push-siblings-editor/
│   │   │   └── store.ts        Zustand slice
│   │   ├── review-grid/
│   │   │   ├── grid.tsx
│   │   │   ├── inline-edit.tsx
│   │   │   ├── conflict-resolution.tsx
│   │   │   ├── ai-reasoning-panel.tsx
│   │   │   └── store.ts
│   │   ├── asset-library/
│   │   │   ├── upload.tsx
│   │   │   ├── tagger.tsx      Structured metadata form
│   │   │   └── crop-tool.tsx
│   │   └── version-history/
│   ├── lib/
│   │   ├── trpc-client.ts
│   │   ├── konva-helpers/      Snap-to-guide, hit-test wrappers
│   │   └── layout-bridge.ts    Calls @banner-studio/layout-engine
│   └── components/             Generic UI primitives
└── package.json

infra/docker/render-worker/Dockerfile

The font story is critical. Copy fonts into the image at build time. Disable sub-pixel rendering at the Chromium flag level. Verify font loading before any screenshot is taken.

Outline:

FROM mcr.microsoft.com/playwright:v1.x-jammy
RUN apt-get update && apt-get install -y fontconfig
COPY ./fonts/ /usr/share/fonts/banner-studio/
RUN fc-cache -fv
COPY package.json ./
RUN npm ci
COPY dist/ ./dist/
# Chromium flags applied at launch in src/playwright/browser-pool.ts:
#   --disable-font-subpixel-positioning
#   --disable-lcd-text
#   --font-render-hinting=none
CMD ["node", "dist/worker.js"]

prompts-vault/

Read-only canonical prompts. Source-controlled but separate from packages/prompts so prompt edits can be reviewed independently from code. Build step mirrors approved versions into packages/prompts.

This separation enables:

  • Prompt PRs reviewed by writers/CDs, not just engineers
  • Audit trail of prompt changes independent of feature branches
  • A/B testing two prompt versions without touching app code

For V1, can be a single directory of markdown files. The mirroring tool can come later.


Initialization order

When scaffolding from zero:

  1. pnpm init at root. Add turbo.json, tsconfig.base.json.
  2. Create packages/types first. Get all interfaces from Part 4 of the architecture doc into it. Compile clean.
  3. Create packages/db. Drizzle schema mirroring Part 7. Migrate against local Postgres.
  4. Create packages/layout-engine. Get Dropflow WASM loading. Write tests for shrink-to-fit and push-siblings with no UI in sight.
  5. Create packages/prompts. Scaffolding only — actual prompts come in Phase 8.
  6. Create apps/api with tRPC and one router (templates) end-to-end against the database. Prove the type-safety chain works.
  7. Then begin Phase 4 from BUILD_SEQUENCE.md (Konva canvas).

Do not create apps/render-worker until Phase 11. It is the last major component because it depends on everything else being stable.