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>
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-layoutentry point takes aBannerSpecand aGeneratedCopymap, returnsResolvedLayer[]with fulllayout_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:
pnpm initat root. Addturbo.json,tsconfig.base.json.- Create
packages/typesfirst. Get all interfaces from Part 4 of the architecture doc into it. Compile clean. - Create
packages/db. Drizzle schema mirroring Part 7. Migrate against local Postgres. - Create
packages/layout-engine. Get Dropflow WASM loading. Write tests for shrink-to-fit and push-siblings with no UI in sight. - Create
packages/prompts. Scaffolding only — actual prompts come in Phase 8. - Create
apps/apiwith tRPC and one router (templates) end-to-end against the database. Prove the type-safety chain works. - 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.