<!--
AI Compliance / Brand-Safety Gate Playbook

Part of the free Claude Code template library from BILT AI — https://biltai.io/claude-templates

These are the actual playbooks used to build and ship production SaaS apps with
Claude Code. They're written to be handed to an agent as-is: point Claude at one
and it has the full pattern, the schema, the gotchas, and the "don't do this"
list. Paths, project refs, and org names have been replaced with placeholders
(<your-app>, <supabase-project-ref>, <your-github-org>) — search for "<" and
fill in your own before running.

Free to use, modify, and ship. No attribution required.
-->

# AI Compliance / Brand-Safety Gate Playbook

**Portable spec for a brand-safety + compliance gate that sits between an AI generator and the user-facing output.**

Applies only to apps that generate AI content (Kompozy-class). If the app doesn't ship LLM/image/video output to end users, skip this playbook. Built from the BKE / Kompozy implementation. Reference repo: `reference app`.

---

## 0. Use this spec

**Inputs:**
- The app's list of output formats (text, image, video, blog, newsletter, carousel, etc.)
- A `Persona Brief` for the workspace (operator persona, audience, voice principles, anti-patterns, proof anchors)
- A workspace brand-identity record (name, description, website, palette, wordmark policy)
- Banned-word list (start with the 50+ default; allow per-workspace overrides)

**Outputs:**
- `lib/compliance/defaults.ts` — declarative compliance matrix (per-format × per-phase × per-block flags)
- `lib/compliance/load.ts` — DB-backed loader with fall-open-to-defaults
- `lib/compliance/image-prompt.ts` — `buildCompliantImagePrompt()` composer for image steps
- `lib/compliance/lint.ts` — banned-word scanner + anti-AI-tell scorer
- `complianceUsed` audit field on every generated row

---

## 1. Why this matters

Four classes of failure that an AI content app ships by default without this gate:

1. **AI tells leaking through** — "delve", "leverage", "tapestry", "navigate the complexities of", "in the ever-evolving landscape". Audiences pattern-match these in milliseconds and discount everything else you say.
2. **Brand-voice drift** — the LLM defaults to a confident-helper-essay voice. Your operator persona is anything but that. Without an enforced Persona Brief, every output regresses to the mean.
3. **Legal exposure** — competitor naming with motive claims, unverified statistics, "doctors say", anything that crosses from opinion into a defamation/false-advertising vector.
4. **Prompt double-injection at the image step** — LLM format prompts are writing briefs ("Generate a 5-slide carousel. SLIDE 1 HOOK. SLIDE 5 CTA."). Piped into an image model, they get drawn literally — five slides inside each slide, brand wordmark on every panel. (BKE lab note 2026-04-30, carousel "5 inside 5".)

The compliance gate is the layer that catches all four before the output ships.

---

## 2. The Persona Brief (foundation)

Every other block in the matrix is downstream of this one. Cross-references the **TOPICAL-AUTHORITY-PLAYBOOK** and the free **Persona Brief Builder** tool — the brief is the long-lived workspace document; everything else reads from it.

Required fields:
- **Operator persona** — first-person identity, role, credibility anchors (years in, deal count, dollars moved). Voice POV (first-person / third-person / observer).
- **Audience** — who the post is FOR. One sentence. Avoid plural-of-everyone ("entrepreneurs", "business owners").
- **Voice principles** — 3-7 lines. Direct claims; no hedge words; specific over abstract; concrete examples over theory; numbers over adjectives.
- **Anti-patterns** — exact phrases to never emit. "Let's dive in", "in today's fast-paced world", "the importance of X cannot be overstated".
- **Proof anchors** — verifiable facts the model can reach for. Specific results, named tools, named deals.

Reference shape: `src/lib/prompt-blocks.ts::buildPersonaBriefBlock`.

---

## 3. Compliance matrix (per-format flags)

Declarative table — one row per output format, one column per compliance block. Stored as a TS constant with a DB override row keyed by `(workspace_id, format, phase)`.

Reference: `src/lib/compliance/defaults.ts`.

```ts
export type ComplianceBlocks = {
  brandIdentity: boolean;     // workspace name/desc/palette — pixel-relevant
  personaBrief: boolean;      // operator identity + voice principles
  voicePrinciples: boolean;   // tone / POV / personality
  precisionKnobs: boolean;    // complexity, formatting, prohibitions, emoji policy
  formatPrompt: boolean;      // user-set per-format instruction
  antiAiTell: boolean;        // banned-word + anti-tell constraints
  legalGuardrails: boolean;   // competitor-naming + claim rules
  scriptScaffold: boolean;    // video script structure (video formats only)
};

export type Phase = 'generate' | 'regenerate';
```

Block ON/OFF policy per format:
- **All formats:** `brandIdentity`, `personaBrief`, `voicePrinciples`, `antiAiTell`, `legalGuardrails` ON.
- **Video formats only:** `scriptScaffold` ON.
- **`formatPrompt`:** ON for formats whose user-set format prompt reads like an image hint ("warm tones, iPhone POV"). OFF (default) for Carousel, Blog, Newsletter — see §5.

The matrix is per-phase because regenerations sometimes want a slightly leaner block set (e.g., skip topic-pool, since the regen is acting on a fixed slot).

---

## 4. `buildCompliantImagePrompt()` — the composer

One function. Composes the right blocks for the (format, phase) tuple. Called from every image-producing worker AND every text-producing worker that emits a downstream image_prompt.

Reference: `src/lib/compliance/image-prompt.ts`.

```ts
const { prompt, fingerprint } = await buildCompliantImagePrompt({
  formatString: 'Photo Post',
  phase: 'generate',
  basePrompt: llmEmittedScene,
  workspace,
  profile,
  includeFormatPrompt: true,   // safe for image-shaped formatPrompts
});
```

**The `includeFormatPrompt: false` rule.** For Carousel, Blog, and any format whose user-set `formatPrompt` reads like a writing brief, pass `false`. Otherwise the LLM-shaped contract gets piped into the image model and drawn literally. (BKE lab notes 2026-04-30 carousel, 2026-05-05 blog.)

Section ordering inside the composed prompt — identity → format wrapper → vibe hints → constraints → SCENE last. Image models weight the tail of the prompt more heavily, so the scene description always lives at the bottom under a `## SCENE` header.

---

## 5. The double-injection footgun

The single most common compliance bug. Worth its own section.

**Two distinct AI steps in every render flow:**
1. **LLM step** — Claude/OpenAI emits the copy AND the per-image scene descriptions (e.g., for Carousel slides). The compliance blocks are baked in here via the upstream `buildPrompt`.
2. **Image step** — DALL-E / gpt-image / Gemini takes the LLM-emitted scene description and renders pixels.

If you wrap the image step with the same compliance blocks the LLM step already applied, the image model **double-applies** them. Symptoms:
- Carousel slides each contain a 5-panel carousel collage drawn inside them.
- Every slide has the brand wordmark drawn into the corner.
- Blog hero image renders a literal "5-section blog article with FAQ" composition.

**Hard rule.** `buildCompliantImagePromptSync` with `includeFormatPrompt: true` is safe ONLY when the format's `formatPrompt` is image-shaped. Audit the format prompts before adding a new format to the wrapper. The blocks that stay safe to pipe into the image step regardless: `brandIdentity`, `personaBrief`, `antiAiTell`. The block that's almost never safe: `formatPrompt` for writing-brief formats.

---

## 6. Banned-word lint

Static keyword scan with word-boundary regex. Same pattern as `src/lib/glossary-autolink.ts` — `\b` prefix/suffix to avoid `\bdelve\b` matching inside `redelver`.

Default list (50+) — non-exhaustive sample:

```
delve, delves, delving, leverage, leverages, leveraging, tapestry,
navigate the complexities, in the ever-evolving, in today's fast-paced,
unlock, unlocks, unlocking, harness, harnessing, dive in, deep dive,
the importance of X cannot be overstated, it's worth noting,
elevate your, paradigm shift, game-changer, revolutionize,
seamlessly, seamless integration, robust, cutting-edge,
unprecedented, intricate, multifaceted, comprehensive,
underscore, underscores, underscoring, foster, fostering,
testament to, embark, embarking, journey of discovery,
let's explore, in conclusion, to summarize, all in all,
furthermore, moreover, however it's important to note,
it goes without saying, needless to say, at the end of the day,
holistic, holistically, transformative, empower, empowering
```

Per-workspace overrides merge on top — workspaces can add domain-specific tells ("real estate professional", "guru", competitor names) or whitelist a default they actually want to use.

Lint runs at two points:
1. **Pre-render audit** for video/blog scripts (see §9).
2. **Post-LLM, pre-image** — if a forbidden phrase made it into the LLM output, either auto-regenerate once or flag the row for review. Never silently ship.

---

## 7. Anti-AI-tells matrix

Five archetypes from the Kompozy AI Tells Index. Each archetype has its own wordlist; the scanner returns counts per archetype plus an aggregate score.

| Archetype | Examples |
|---|---|
| **Vocabulary tells** | delve, leverage, tapestry, robust, seamless, holistic |
| **Hedge tells** | "may", "might", "could potentially", "it's worth noting", "it could be argued" |
| **Transition tells** | "furthermore", "moreover", "additionally", "in addition", "however, it's important" |
| **Structure tells** | rule-of-three lists in every paragraph, perfect parallel sentence structure, "not just X but Y" |
| **Closing tells** | "in conclusion", "to summarize", "at the end of the day", "all in all", "ultimately" |

**Score formula:**

```ts
const tells = countAllTellOccurrences(text);
const words = countWords(text);
const score = Math.max(0, 100 - (tells / words) * 600);
```

Score floors at 0. Show the score in the row's review pane. Threshold for auto-block vs warn is per-workspace — start with `< 60 = block, 60-80 = warn, 80+ = ship`.

---

## 8. Length defense

Image model prompt caps are non-negotiable and silent until you hit them. BKE 2026-05-05: Blog hero prompts hit ~35.9k chars vs gpt-image-2's 32k cap, 400'd every time, four failed renders before diagnosis.

**Hard-cap at the API boundary, not at the caller.** Defensive truncation in the provider wrapper means every caller is protected even if a future commit re-introduces the wrapper drift.

Reference pattern: `src/lib/image-gen.ts::callOpenAIImage`.

```ts
const MAX_PROMPT_CHARS = 31900;
let finalPrompt = prompt;
if (finalPrompt.length > MAX_PROMPT_CHARS) {
  // Keep the TAIL — SCENE block lives at the end, and image models
  // weight tail more heavily than head.
  finalPrompt = finalPrompt.slice(finalPrompt.length - MAX_PROMPT_CHARS);
  console.warn('[image-gen] prompt truncated', { original: prompt.length, kept: MAX_PROMPT_CHARS });
}
```

Two non-obvious choices:
1. **Slice keeps the tail, not the head.** The SCENE block is at the bottom of every compliance-composed prompt; lose the brand identity preamble before you lose the scene description.
2. **Always log truncation.** Silent truncation is a future bug.

---

## 9. Pre-render audit (video/blog scripts)

Before TTS / video render / blog publish, audit the draft script for these failure modes. Show the user a diagnosis + proposed revision; wait for "go" before render. BKE memory: `feedback_demo_script_audit.md`.

Checklist:

- **Apologetic post-hook lines** — "I know this sounds simple but…", "bear with me for a second". Kills retention in the second slot.
- **Anti-feature framing** — "AI does it for you, you don't even need to think". Reads as cope, kills conversion. (Specific to AI content tools.)
- **Vague claims** — "guru", "experts say", "studies show", unnamed competitor references.
- **Repetition** — the same idea phrased three times across the script. Compress.
- **Legal exposure** — competitor named with a motive claim ("X does this because they want to mislead you"). Either remove the motive or remove the competitor.
- **Weak hook** — first 3 seconds: no specific number, no specific outcome, no specific contrarian take. Rewrite.

The audit is cheap compared to a full TTS+render roundtrip; never skip it.

---

## 10. Auditable trail (`complianceUsed`)

Every `generated_content` row gets a `metadata.complianceUsed` field stamped at render time. Shape:

```ts
type ComplianceFingerprint = {
  slug: OutputTypeSlug;
  phase: 'generate' | 'regenerate';
  flagsBitmask: number;        // stable bit order, append-only
  blocksApplied: string[];     // human-readable, e.g. ['brandIdentity','personaBrief','antiAiTell']
  timestamp: string;
};
```

`blocksApplied` is the load-bearing field for debugging "why did this output look wrong six weeks ago." It records what was ACTUALLY injected into this render, not just what the matrix said. (Some blocks are skipped at composition time if the underlying data is empty — `personaBrief: true` with no brief yields no injection.)

Reference: `src/lib/compliance/image-prompt.ts::ComplianceFingerprint`.

---

## 11. Common pitfalls

**Don't pipe LLM-shaped format prompts into image models.** They're contracts, not scene descriptions. Image models obey literally. Audit every new format's `formatPrompt` shape before deciding `includeFormatPrompt`. (BKE 2026-04-30 carousel + 2026-05-05 blog.)

**Don't compose compliance blocks once and ship to both LLM + image steps.** The LLM step bakes them into the scene description it emits; the image step would double-apply. Compose twice with different block sets, or compose once for LLM and use a leaner image-side composer (current BKE pattern).

**Don't use `buildCompliantImagePromptSync` on formats whose `formatPrompt` is a writing brief.** Only image-shaped prompts ("warm tones, iPhone POV", "candid lighting") are safe with `includeFormatPrompt: true`.

**Don't trust providers to enforce length caps.** Hard-cap at our API boundary with logging. Provider 400s are silent failures at the user level — they see "render failed" with no actionable detail.

**Don't run TTS on unaudited scripts.** TTS + video render burns minutes and credits. The pre-render audit catches retention killers cheaply; skipping it ships unwatchable output. (BKE memory: feedback_demo_script_audit.md.)

**Don't ship anti-feature framing.** "AI does it for you, you don't even need to think" reads as cope and kills conversion. The audit checklist has a specific entry for this.

**Don't silently strip a banned word.** Auto-regenerate once or flag for review. Silent edits cause the user to lose trust in the output mid-review.

**Don't let `complianceUsed` drift to optional.** Stamp it on every row. The day you need to debug a six-week-old "why does this look weird" complaint, you'll thank past-you.

---

## 12. Kickoff prompt

> Execute the AI Compliance Gate Playbook at `<workspace>/templates/c-tier/AI-COMPLIANCE-GATE-PLAYBOOK.md` for this app. Mirror the BKE implementation in `<reference-app>/src/lib/compliance/`.
>
> Inputs:
> - Output formats: [list — e.g., text-post, photo-post, carousel, blog, persona-short]
> - Persona Brief source: [DB-backed workspace doc OR free Persona Brief Builder export]
> - Banned-word list: [use defaults / merge with: <list>]
> - Image providers wired: [dall-e / gpt-image-2 / gemini / heygen / kie / piapi]
>
> Pause after `defaults.ts` + `image-prompt.ts` ship for review. Audit each format's `formatPrompt` shape before deciding `includeFormatPrompt`.

---

**Reference files in BKE:**
- `src/lib/compliance/defaults.ts` — matrix + per-format defaults
- `src/lib/compliance/image-prompt.ts` — `buildCompliantImagePrompt()` composer
- `src/lib/compliance/load.ts` — DB-backed loader, falls open to defaults
- `src/lib/prompt-blocks.ts` — `buildPersonaBriefBlock`, `buildAntiAiTellBlock`
- `src/lib/glossary-autolink.ts` — `\b`-boundary wordlist scanner pattern
- `src/lib/image-gen.ts` — defensive length-cap at the API boundary
- `src/trigger/render-carousel.ts` — canonical `includeFormatPrompt: false` example
- `src/trigger/render-blog.ts` — same pattern, blog edition
