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

366 lines
13 KiB
Markdown

# 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.