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>
366 lines
13 KiB
Markdown
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.
|