banner_studio/ANTI_PATTERNS.md
Simeon Schecter 988a47c797 Initial commit: Day 1 + Day 2 of the vertical slice
Day 1 (monorepo + Node layout engine):
- Turborepo + pnpm workspaces with apps/web, apps/render-worker, and
  packages for types, layout-engine, prompts, api-lib.
- @banner-studio/types: BannerSpec contract, every layer kind, ResolvedLayer,
  zod schemas mirroring each interface.
- @banner-studio/layout-engine: Dropflow WASM wrapper, text measurement,
  shrink-to-fit, push_siblings, resolveLayout. Snapshot-tested.

Day 2 (browser parity + AI pipeline):
- Layout engine ./browser subpath: same resolveLayout in the browser via
  Dropflow WASM build. Quarantined wasm-locator import (dropflow 0.5.1
  exports gap).
- Cross-group push_siblings bug fix: deltas now thread through group
  recursion via a shared accumulator; regression test added.
- DEMO_TEMPLATE_300x250 promoted to packages/layout-engine/src/templates/.
- @banner-studio/prompts: versioned extract + generate prompts with
  zod-defined tool schemas (claude-sonnet-4-6, forced tool-use).
- @banner-studio/api-lib: CSV feed loader, extract/generate/route-node/
  assemble agents, orchestrator returning fully-resolved BannerSpec.
  Generate agent retries on character-limit overflow.
- apps/web (Next.js 14 App Router): /api/generate route, /parity diff page,
  promise-singleton browser engine init.
- feeds/demo.csv with five hand-authored rows of varied length.
- SLICE_DEVIATIONS.md documents the five intentional gaps from
  ARCHITECTURE.md with V1 reversal paths.

Verified end-to-end: POST /api/generate against the live Claude API
returns three resolved BannerSpecs and two honestly-skipped rows
(overflow after two attempts). 26 unit + integration tests passing.
2026-05-15 10:25:21 -04:00

15 KiB
Raw Permalink Blame History

ANTI_PATTERNS.md

Patterns this tool explicitly rejects, the reasons they are rejected, and what we do instead. These are non-negotiable constraints on the design — a developer who implements an anti-pattern is fixing a bug, not making a choice.

Organized by surface:

  • Template builder canvas
  • Review grid interactions
  • AI generation UX
  • Export UX
  • Conflict resolution UX (V2 — captured now)
  • Asset selection UX

Format:

  • Anti-pattern — the pattern being rejected
  • Why rejected — the reason it fails
  • What we do instead — the alternative that ships

Template builder canvas

1. Modal dialogs for property editing

  • Why rejected — Breaks flow. The designer has to context-switch away from the canvas to change a value, then back to verify.
  • What we do instead — Inline property panel anchored to the side of the canvas, always visible for the selected layer. Properties update live as values change. No modals.

2. Floating "unsaved changes" state

  • Why rejected — "Unsaved changes" is anxiety-inducing for creative work. Designers lose work in tools that don't auto-save and never forgive it.
  • What we do instead — Auto-save every state change. Version history is the safety net. No save button in the template builder. No dirty indicator.

3. Numeric input as the only way to position

  • Why rejected — Designers think spatially, not numerically. Typing "x: 47" to position an element concedes to the computer.
  • What we do instead — Drag is primary. Numeric input is available in the property panel for precision but is never the only way.

4. Fixed zoom levels

  • Why rejected — Designers zoom constantly and fluidly. Snap-to-50, snap-to-100 interrupts the spatial relationship.
  • What we do instead — Continuous zoom on Cmd+scroll. Cmd+0 fits all artboards. Cmd+1 is 100%. Everything else is fluid.

5. Separate "preview mode"

  • Why rejected — Implies the editing view is not trustworthy. If you need a separate mode to see the real result, WYSIWYG has failed.
  • What we do instead — The canvas is the preview. Animation plays in place on a scrubber. What you see is what exports — guaranteed by Dropflow parity between browser and render worker.

6. Right-click menus as primary access

  • Why rejected — Discoverability is poor; new users don't know to right-click; experienced users shouldn't have to.
  • What we do instead — Every action is accessible through a visible UI element or a keyboard shortcut documented in INTERACTION_STANDARDS.md. Right-click exists as a shortcut for experienced users, never as the only path.

7. Locked artboard order

  • Why rejected — Designers reorganize constantly. Locking artboard order imposes the tool's mental model.
  • What we do instead — Drag to reorder artboards. Order is cosmetic, not structural. Generation, export, and rendering work the same regardless of order.

8. Confirmation dialogs for destructive actions

  • Why rejected — Confirmation dialogs are an admission that undo doesn't work. They train users to dismiss reflexively.
  • What we do instead — Undo is the safety net. No "are you sure?" for layer deletion, artboard deletion, or anything the user can recover from. Confirmations only exist for irreversible cross-system actions (publish, archive, export to ad server).

9. Snapping you can't override mid-gesture

  • Why rejected — Snapping that betrays the user even 20% of the time is worse than no snapping.
  • What we do instead — Snap targets are visible before they engage. Holding a modifier (Cmd) during a drag disables snapping for that gesture. Snap distance is fixed and small (4px).

10. Hidden text overflow

  • Why rejected — If overflow is silent, it ships. The defining failure mode of the category.
  • What we do instead — Overflow is a first-class state. ResolvedLayer.layout_log.overflow_triggered propagates to the UI; affected artboards render with a persistent overflow badge and a red border that cannot be dismissed without resolving the overflow.

Review grid interactions

11. Tiny thumbnails

  • Why rejected — Reviewing at thumbnail scale is data entry. The CD sees postage stamps and disengages.
  • What we do instead — Banners render at their native pixel size by default, with the grid scaling to fit available width. The review surface is designed for judgment ("Feels like a lightbox" — source document, section 3).

12. Inline edits that don't show what was edited

  • Why rejected — Producer edits a headline, regenerates a week later, can't tell what was AI vs. human, can't decide what to keep.
  • What we do instead — Every overridden field renders with a persistent override pip. Hovering shows author and timestamp. The pip survives regeneration. The state lives in human_overrides and is rendered, not inferred.

13. Regeneration as a fire-and-forget button

  • Why rejected — Producer clicks Regenerate, has no idea what will happen to their edits. Fear of losing work prevents iteration.
  • What we do instead — The Regenerate button shows a pre-flight preview: "You have 3 overrides. These will be preserved. 1 may conflict with new copy." Conflicts surface inline after regeneration, not in a separate panel.

14. Conflict resolution as a separate screen

  • Why rejected — Switching screens to resolve a conflict loses the visual context that the conflict is about.
  • What we do instead — Conflicts resolve inline on the affected banner: human value and new value shown side-by-side, choose with one click. Status badge on the banner updates immediately.

15. Per-banner approve buttons buried in menus

  • Why rejected — Approval is the most common action in the review grid; making it require two clicks per banner is friction × N banners.
  • What we do instead — Approve / Reject / Regenerate are single keystrokes (see INTERACTION_STANDARDS.md). Cursor-driven equivalents are visible on the banner card, not behind a menu.

16. Treating regeneration as the only fix

  • Why rejected — When the producer just wants to change one word, regenerating is overkill and risks destabilizing the rest.
  • What we do instead — Inline text edit on any field in the review grid. Single-field edits create an override; they do not trigger regeneration. The regeneration path is reserved for "I want a different take."

AI generation UX

17. AI generation as a blocking operation

  • Why rejected — Staring at a spinner for 60 seconds is anxiety-inducing and feels broken even when it isn't.
  • What we do instead — Generation is async. The user can continue working on other artboards or campaigns. Banners populate in the grid as they complete. Progress is specific per banner ("Generating copy…", "Validating character counts…", "Resolving layout…").

18. Generic progress ("loading…")

  • Why rejected — A generic loader gives no information about whether the run is healthy or stuck. The user can't decide whether to wait, retry, or escalate.
  • What we do instead — Per-banner status reflects the actual pipeline stage (extract → generate → route → assemble) and the most recent AI call. If a stage fails, the UI names the stage and offers a single-click retry that does not re-run successful stages.

19. AI reasoning behind a toggle

  • Why rejected — If "why this copy?" requires a click, half the users never ask. They either trust blindly or distrust uniformly.
  • What we do insteadai_reasoning is rendered as a persistent side panel on banner selection in the review UI. Copy rationale, asset rationale, variant rationale, animation rationale — all visible by default. The panel can be collapsed but is not hidden.

20. AI making asset decisions creatively

  • Why rejected — Verbatim from the stakeholder meeting and CLAUDE.md: "AI manages the process, not the design." Letting an LLM pick a logo or hero shot creatively produces surprises the designer must clean up and undermines brand control.
  • What we do instead — Asset selection is a deterministic metadata query against the variant group's SelectionRule[]. Variant metadata (region, market, language, campaign type, partner brands) is matched against the brief. Highest-priority match wins. If no match, fallback_variant_id is used. The AI orchestration service is given the resolved variant; it never chooses among assets.

21. AI rewriting layout

  • Why rejected — If AI can move elements, the template stops being the contract. Brand guidelines (especially for high-fashion clients with explicit scaling rules — flagged in the stakeholder meeting) become unenforceable.
  • What we do instead — AI generates copy and routes data. Layout is computed by the deterministic Dropflow + push_siblings engine. The text group system encodes brand scaling rules; the AI cannot bypass them.

22. Monolithic AI prompts

  • Why rejected — A single prompt that does extraction + generation + assembly is opaque, hard to validate, and impossible to A/B test.
  • What we do instead — Four discrete agents (extract, generate, route, assemble) with typed interfaces between them. Each prompt lives in /prompts/ and is versioned. Validation is programmatic between stages. If a stage's output fails its schema, the stage retries once before flagging.

23. Trusting AI character counts

  • Why rejected — LLMs miscount characters. Trusting the model's claim that copy fits will ship overflow.
  • What we do instead — Character counts are validated programmatically after each generation. If over limit, the generate agent retries once with the violation named. Second failure flags the banner for human attention. The AI's self-reported counts are never authoritative.

Export UX

24. Multi-step export wizard

  • Why rejected — Export is the most repeated path in the trafficker's workflow. A wizard adds friction every time.
  • What we do instead — Export is one button. Profile, naming, packaging — all configured at campaign setup. Pressing Export produces the zip(s) for the approved version. Failures surface a single message naming the gate that failed and how to fix it.

25. Loud "checking…" UI for QA gates

  • Why rejected — QA gates run on every export. A modal that demands attention every time trains users to dismiss it.
  • What we do instead — QA runs silently as part of export. If gates pass, the zip is produced and the user is notified of success. If a blocking gate fails, the export halts and the failure is shown inline on the campaign — not as a popover. Advisory warnings appear in the trafficking sheet, not as interrupts.

26. Surprising output (wrong filenames, wrong structure)

  • Why rejected — Trafficking sheets and ad server uploads depend on exact filenames. A surprise renames breaks downstream automation.
  • What we do instead — File naming is set per campaign and shown in the export confirmation. The zip structure matches the ad server profile (flat_zip_structure or nested). The trafficking sheet is generated against the same names. No silent renaming.

27. Export against the wrong version

  • Why rejected — The active version isn't always the approved version. Exporting the wrong one ships rejected creative.
  • What we do instead — Export defaults to the approved version (approved_version_id). If the user is on a non-approved version, the Export button surfaces this state and requires an explicit override. The override is logged.

Conflict resolution UX (V2 — captured now)

The slice does not include conflict resolution (no human edits → no overrides → no conflicts). These anti-patterns govern V1 onward. ARCHITECTURE.md Part 7 defines the data model; the UX is open.

28. Conflict list as a separate screen

  • Why rejected — Removing the user from the visual context of the banner makes resolution harder, not easier.
  • What we do instead — Conflicts are surfaced inline on the affected banner card with a distinct state (yellow border + conflict count). Resolution is a side-by-side: human value vs. new generated value, with one-click resolution and an optional "lock this field" toggle. The data model (Conflict.resolution) is unchanged.

29. Auto-resolving conflicts silently

  • Why rejected — If the tool picks a winner silently, the user discovers it at export and trust collapses.
  • What we do instead — Unresolved conflicts block export. They never auto-resolve. The merge algorithm in ARCHITECTURE.md Part 7 explicitly leaves conflicts unresolved and surfaces them.

30. Treating "lock" as the only way to protect an edit

  • Why rejected — Asking the user to anticipate every field they want to protect is too much cognitive overhead.
  • What we do instead — Overrides are preserved by default whenever the generated value didn't change beneath them (the safe-apply branch in the merge algorithm). Lock is reserved for the "client said this exact line" case. The default state is preservation; lock is the escalation.

Asset selection UX

31. AI suggests assets

  • Why rejected — Same reason as #20. Stakeholder positioning is explicit: AI does not creatively choose assets.
  • What we do instead — The asset library exposes variant groups with SelectionRule[]. The brief or feed row provides the inputs (region, market, campaign type). The Route node evaluates the rules deterministically. AI never sees a choice of assets — it sees the one that was selected.

32. Free-form asset upload during generation

  • Why rejected — Letting users upload images mid-generation bypasses the rights/metadata pipeline, leading to unrights-cleared assets shipping.
  • What we do instead — All assets enter through the asset library, with AssetRights and AssetMetadata populated. The Route node selects only from approved variants. Mid-generation upload is not a path.

33. Asset previews that lie about crop

  • Why rejected — Asset library shows the full image; banner uses a crop; designer can't tell what will actually appear.
  • What we do instead — Approved crops are stored per template-artboard pair (Asset.approved_crops). The library preview shows the crop that will be used for the currently-selected target. No surprises at render time.

Conflicts to resolve before build

  • Inline review-grid editing vs. slice scope. Anti-pattern #16 commits to inline text edit in the review grid; VERTICAL_SLICE.md says "No editing, no version control, no conflict resolution" in the slice. Resolution: the anti-pattern governs V1; the slice deliberately defers inline editing. Both can be true. Confirm with the next phase brief.
  • Approve/Reject in the slice. Anti-pattern #15 commits to single-keystroke approve/reject in the review grid. VERTICAL_SLICE.md excludes approval workflow from the slice. The keystrokes are reserved for V1; in the slice, the corresponding keys can be no-ops or simply not exercised. Confirm.
  • Auto-save in the slice. Anti-pattern #2 mandates auto-save in the template builder. The slice has no template builder UI (template is a TypeScript constant). Anti-pattern #2 is dormant in the slice and activates with the template builder in V1.
  • AI reasoning panel structure. Anti-pattern #19 commits to four reasoning categories (copy, asset, variant, animation) per BannerSpec.ai_reasoning. The slice has no variant groups (one asset only), so variant_selection will be empty or "n/a." Not a conflict — a slice simplification — but flag so the UI doesn't render an empty field as if it were a failure.