Draft Specification v0.2

Your spec is the source code.
The LLM is the compiler.

A structured language for defining human-readable codebases that are transpiled and maintained by AI agents. Write what you want. Pin what you care about. Let the machine handle the rest.

IaC Infrastructure as Code  →  CaT Code as Text

The Goal

Traditional codebases are optimized for compilers, not comprehension. LLMs work best with structured natural language. CaT bridges the gap by making the human-readable specification the single source of truth, and letting an AI agent transpile it into a working codebase.

Spec is the Source

The bacon/ folder is the source of truth. Humans author and version .cat files. Code in src/ is a derived artifact.

Progressive Specificity

Write at whatever level of detail you care about — from vague intent ("I need auth") to pinned implementation details. The LLM fills in everything else.

Git-Native Workflow

Transpilation runs on every commit via GitHub Actions. The LLM opens a PR with the generated code. Humans review and merge — like terraform plan.

Off-the-Shelf Components

Use using: to bind features to known providers (Supabase, Stripe, Resend). The LLM uses idiomatic patterns. No reinventing the wheel.

The Spectrum of Control

LevelYou writeLLM decides
Intent"I need auth"Everything: provider, flow, UI, code
Strategyusing: Supabase AuthWiring, components, error handling
BehaviorFlows and stepsImplementation code
PinHard constraintsEverything else
LiteralExact code blocksNothing — hands off

POC Plan

The proof of concept targets a Supabase + Next.js stack to keep scope manageable while demonstrating the full loop: bacon → transpile → PR → deploy.

Repository Structure

my-cat-project/
├── .github/
│   └── workflows/
│       └── transpile.yml       # triggers on bacon/ changes
├── .cat/
│   ├── config.yaml             # stack, model, preferences
│   ├── purr          # transpilation metadata (hashes, outputs, deps)
│   └── providers/              # provider profiles
│       └── supabase.yaml
├── bacon/                      # source of truth (human-edited)
│   ├── system.cat
│   ├── auth.cat
│   ├── models.cat
│   └── features/
│       └── *.cat
├── src/                        # transpiled output (LLM-generated)
│   ├── app/
│   ├── lib/
│   ├── components/
│   └── supabase/
│       ├── migrations/
│       └── functions/
└── README.md

Transpilation Pipeline

bacon/ changed git push GitHub Action CaT CLI reads diff LLM transpiles Opens PR Human reviews Merge & deploy

The Purr File

When the LLM makes implementation choices, it writes them back into the .cat spec files as Pin: blocks — keeping the spec as the single source of truth. The .cat/purr file tracks operational metadata only: content hashes, generated output paths, cross-feature dependencies, and the last transpiled git commit, so incremental runs can git diff against it to find only the .cat files that changed.

# .cat/purr

# Git state — used for incremental transpilation
last_transpiled_commit: a3f7c2e
last_transpiled_at: 2026-04-14T10:30:00Z

# Per-file state
bacon/auth.cat:
  content_hash: a1b2c3d4
  outputs:
    - src/app/(auth)/login/page.tsx
    - src/app/(auth)/signup/page.tsx
    - src/lib/auth.ts

bacon/features/invoices.cat:
  content_hash: e5f6g7h8
  outputs:
    - src/app/invoices/page.tsx
    - src/supabase/migrations/002_invoices.sql
  dependencies:
    - bacon/models.cat # uses shared Invoice model
  migrations:
    - 002_invoices.sql
    depends_on: 001_auth.sql

POC Components

ComponentWhatPriority
GitHub TemplateRepo scaffold with folder structure, workflow, configP0
CaT CLIcat transpile — reads bacon, calls LLM, writes to src/P0
Purr FileTrack content hashes, outputs, dependencies, incremental diffsP0
Supabase ProviderProvider profile + system prompt tuning for Supabase idiomsP1
GitHub ActionCI wrapper around the CLI, PR creationP1
Formal ParserAST parser for .cat files (optional for v0.1 — LLM reads raw text)P2

The CaT Specification Language

.cat files are structured text documents that define a software system in human-readable form. The format is intentionally designed to be readable without tooling — it's valid text that any person can understand and any LLM can parse.

1. File Structure

A CaT project contains a bacon/ directory with .cat files. Each file defines one logical unit: a feature, a set of data models, or the system itself. Files can be organized in subdirectories for larger projects.

File Naming

Files are named in kebab-case with the .cat extension. The name should reflect the content: auth.cat, user-profiles.cat, billing.cat.

One file is required: bacon/system.cat — the root declaration of the system.

bacon/
├── system.cat              # required — system declaration
├── auth.cat                # one file per feature or domain
├── models.cat              # shared data models
└── features/
    ├── invoices.cat
    └── dashboard.cat

2. System Declaration

Every CaT project starts with system.cat. It names the system, provides a high-level description, and declares the stack.

System: FreelanceOS

A simple invoicing app for freelancers. Manage clients,
create invoices, accept payments, and track revenue.

Stack:
  framework:  Next.js (App Router)
  language:   TypeScript
  database:   Supabase
  auth:       Supabase Auth
  styling:    Tailwind + shadcn/ui
  deploy:     Vercel

Syntax Rules

System: is followed by the system name on the same line. The description is free-form text on subsequent lines until the next keyword. Stack: contains indented key-value pairs.


3. Features

A Feature is the primary building block of a CaT spec. It describes a capability of the system. A feature can contain a description, provider binding, data models, flows, views, pins, and constraints.

Feature: Authentication
  using: Supabase Auth

  Users can sign up, log in, and reset their password.
  New users start on the free plan.

  Flow: Sign Up
    ...

  Flow: Password Reset
    ...

  Pin:
    ...

Syntax Rules

Feature: is followed by the feature name. Everything until the next block keyword (Flow:, View:, Model:, Pin:, Constraint:, or a new Feature:) is treated as a free-form description. The description is natural language — the LLM uses it as context for implementation decisions.

At the simplest level, a feature can be a single line:

Feature: Authentication
  Users can sign up and log in.

The LLM has full discretion: it picks the provider, designs the UI, chooses the flows. The user adds detail only when they care.


4. Provider Binding (using:)

The using: keyword binds a feature to one or more providers or libraries. It tells the LLM: "use these tools and their idiomatic patterns." Multiple providers are comma-separated.

Feature: Authentication
  using: Supabase Auth

Feature: Payments
  using: Stripe

Feature: Invoices
  using: Supabase, Resend       # multiple providers

Feature: File Uploads
  using: Supabase Storage

What using: does

The provider name maps to a provider profile in .cat/providers/. This profile contains the provider's capabilities, required environment variables, packages, and idiomatic patterns. The LLM receives this profile as context during transpilation.

If using: is omitted, the LLM selects a provider based on the stack defined in system.cat and coherence with other features. Its choice is recorded in purr.

Provider profiles look like this:

# .cat/providers/supabase-auth.yaml
name: Supabase Auth
category: authentication
capabilities:
  - email-password
  - oauth: [google, github, apple]
  - magic-link
  - phone-otp
defaults:
  session: jwt
  storage: cookie
requires:
  env: [SUPABASE_URL, SUPABASE_ANON_KEY]
  packages: ["@supabase/supabase-js", "@supabase/ssr"]

5. Data Models

A Model defines a data entity. Models can appear inside a feature or in a dedicated models.cat file for shared entities. Models referenced by multiple features should be defined in models.cat rather than inside any one feature — this makes cross-feature dependencies explicit and ensures incremental transpilation can track them correctly.

Model: Invoice
  id:          uuid, primary key
  owner:       reference to Profile
  client_name: text
  client_email: email
  status:      draft | sent | paid | overdue
  due_date:    date
  items:       list of LineItem
  created_at:  timestamp, auto

Model: LineItem
  id:          uuid, primary key
  invoice:     reference to Invoice
  description: text
  quantity:    integer, min 1
  rate:        decimal

Syntax Rules

Model: is followed by the model name in PascalCase. Fields are indented with the format: name: type, ...modifiers.

Supported Types

text, integer, decimal, boolean, date, timestamp, uuid, email, url, json, currency. Enums use the pipe syntax: value_a | value_b | value_c.

Modifiers

primary key, nullable, unique, auto (auto-generated), min N, max N, default VALUE. References use reference to ModelName. Lists use list of ModelName.

Invariants

Business rules about the model can be expressed as Invariant: lines after the fields:

Model: Invoice
  status: draft | sent | paid | overdue
  items:  list of LineItem

  Invariant: items must not be empty when status is "sent"
  Invariant: due_date must be in the future when status changes to "sent"

6. Flows

A Flow describes a user-facing behavior as a sequence of steps. Flows live inside features and define what happens, not how it's implemented.

Flow: Place Order
  1. Customer confirms checkout
  2. System creates Order with status "placed"
  3. System reserves inventory via InventoryService
  4. System charges payment via PaymentGateway

  On failure (step 4):
    Set Order status to "cancelled"
    Release inventory

Syntax Rules

Flow: is followed by the flow name. Steps are numbered lines with natural language descriptions. The LLM translates steps into code.

Error Handling

On failure (step N): defines what happens when a step fails. On failure: without a step reference applies to the entire flow. This keeps specs readable while giving the LLM clear recovery logic.

Conditions

Use If ... then ... or When ... then ... for conditional branching within a flow. Conditions are natural language.

Flow: Send Invoice
  1. User clicks send on a draft invoice
  2. System validates all required fields are filled
  3. System generates PDF of the invoice
  4. System emails PDF to client_email
  5. Status changes to "sent"

  If client has Stripe payment method on file:
    Include a payment link in the email

  On failure (step 4):
    Show error to user: "Failed to send email. Please check the client's email address."
    Status remains "draft"

7. Views

A View describes a page or UI component layout. Unlike a Flow, which is a sequence of user actions, a View describes what the user sees — data layout, filtering, grouping, and visual treatment.

View: Dashboard
  Show tasks grouped by status in a kanban board.
  User can filter by assignee and priority.
  Show overdue tasks highlighted in red.

Syntax Rules

View: is followed by the view name. The body is free-form description of the layout, data displayed, filtering, and visual behavior. The LLM translates this into a page or component.

When to Use View vs. Flow

Use Flow: when describing a sequence of actions with numbered steps ("User does X, System does Y"). Use View: when describing a page layout or data display ("Show X grouped by Y, filter by Z"). If it has numbered steps, it's a Flow. If it describes what appears on screen, it's a View.

Views can include Pin: blocks to constrain specific UI choices:

View: Task Board
  Show tasks in a draggable kanban board with columns: To Do, In Progress, Done.
  Each card shows title, priority badge, and assignee avatar.

  Pin:
    Use @hello-pangea/dnd for drag-and-drop

8. Pinning (pin:)

The Pin: block is the escape hatch. Anything inside a pin is a hard constraint the LLM must not deviate from. Use it when you care about a specific detail.

Feature: Authentication
  using: Supabase Auth

  Users can sign up with email or Google.

  Pin:
    Passwords must be at least 12 characters
    Use zxcvbn for password strength validation
    Rate limit: 5 attempts per minute per IP
    After 10 failed attempts, lock account for 30 minutes

Pins can also appear inside flows to constrain specific steps:

Flow: Generate Invoice PDF
  1. System renders the invoice as a PDF
  2. System stores the PDF in file storage

  Pin:
    Use @react-pdf/renderer for PDF generation
    PDF page size: A4
    Include company logo from Profile.logo_url

Pin vs. Description

Regular description text is guidance — the LLM should follow it but has discretion in how. Pinned text is a requirement — the LLM must implement it exactly as stated. Think of description as "should" and pin as "must."


9. Literal Code (literal:)

When you need the LLM to use exact code, use a Literal: block. The LLM incorporates this code verbatim — no modifications, no "improvements." This is the ultimate override.

Flow: Validate Session

  Literal: typescript
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) {
      redirect('/login')
    }
    if (isExpired(session)) {
      await supabase.auth.refreshSession()
    }

Syntax Rules

Literal: is followed by a language identifier (typescript, sql, css, etc.). The indented code block that follows is treated as verbatim source. The LLM must place this code in the appropriate file without alteration.

When to Use

Use Literal: sparingly. It defeats the purpose of CaT (letting the LLM decide implementation). Good uses: security-critical code, exact SQL queries, vendor-specific API calls, performance-sensitive logic.


10. Constraints

Constraints define non-functional requirements: performance, security, accessibility, and operational rules.

Feature: API

  Constraint: performance
    All API endpoints must respond in under 500ms at p99

  Constraint: security
    All database queries must use Row Level Security
    No raw SQL in application code — use the Supabase client

  Constraint: accessibility
    All pages must score 90+ on Lighthouse accessibility

Scope

Constraints can appear at the feature level (apply to that feature) or in system.cat (apply globally). Feature-level constraints override system-level ones.


11. References Between Specs

Specs can reference models and features from other files by name. The transpiler resolves references across the entire bacon/ directory.

# In bacon/features/invoices.cat

Feature: Invoices

  Model: Invoice
    owner: reference to Profile    # defined in models.cat

  Flow: Send Invoice
    ...
    4. System emails PDF via EmailService  # defined in email.cat

Resolution Rules

Model names (Profile, Invoice) are globally unique across the spec. Feature names are also globally unique. If two files define the same model, the transpiler reports an error.

Forward references are allowed — you can reference a model before it's defined in another file. The transpiler reads all files before resolving.

Shared Models

If a model is referenced by multiple features (e.g., Profile used by both auth.cat and tasks.cat), it should live in a dedicated models.cat file. This avoids ambiguous ownership and ensures the transpiler can correctly track dependencies for incremental transpilation.


12. Config (.cat/config.yaml)

The config file controls how the transpiler operates. It lives outside bacon/ because it's infrastructure, not specification. The stack is defined in system.cat (part of the spec) — the config only holds transpiler settings and provider paths.

# .cat/config.yaml

version: 0.1

transpiler:
  model:     claude-sonnet-4
  strategy:  incremental    # or "full" to regenerate everything
  output:    src/
  retries:   2              # self-correction attempts on build failure

providers:
  path: .cat/providers/

13. Conventions & Style Guide

Naming

Models: PascalCase
Fields: snake_case
Features: Title Case
Files: kebab-case.cat

Indentation

2 spaces for nesting. Blocks are defined by their keyword and indentation level, similar to Python or YAML.

Comments

Lines starting with # are comments. Comments are passed to the LLM as additional context but have no semantic meaning.

Ordering

Within a feature: description first, then using:, then Model:, then Flow:, then View:, then Pin:, then Constraint:.

Keywords Reference

KeywordContextPurpose
System:system.cat onlyDeclares the system name and description
Stack:system.cat onlyDefines the technology stack
Feature:any .cat fileDeclares a feature / capability
using:inside FeatureBinds to one or more providers (comma-separated)
Model:Feature or standaloneDefines a data model / entity
Invariant:inside ModelBusiness rule on a model
Flow:inside FeatureDescribes a user-facing behavior sequence
View:inside FeatureDescribes a page or UI component layout
On failure:inside FlowError handling / recovery logic
If / When:inside FlowConditional branching
Pin:Feature or FlowHard constraint the LLM must follow
Literal:Feature or FlowVerbatim code block
Constraint:Feature or system.catNon-functional requirement

Full Example: FreelanceOS

A complete CaT spec for a simple invoicing app. Three files, fully readable, producing a full-stack Supabase + Next.js application.

bacon/system.cat

System: FreelanceOS

A simple invoicing app for freelancers.
Manage clients, create invoices, track payments.

Stack:
  framework:  Next.js (App Router)
  language:   TypeScript
  database:   Supabase
  auth:       Supabase Auth
  styling:    Tailwind + shadcn/ui
  deploy:     Vercel

Constraint: security
  All database access must use Row Level Security.
  Users can only access their own data.

bacon/auth.cat

Feature: Authentication
  using: Supabase Auth

  Users can sign up with email or Google OAuth.
  After first login, a Profile is created automatically.

  Model: Profile
    id:           uuid, primary key # matches auth.users.id
    display_name: text
    avatar_url:   text, nullable
    created_at:   timestamp, auto

  Flow: Sign Up
    1. User enters email and password, or clicks "Sign in with Google"
    2. System creates auth account
    3. System sends verification email (email signup only)
    4. On first login, system creates a Profile row

  Flow: Sign In
    1. User enters credentials or uses Google
    2. System redirects to /dashboard

  Flow: Password Reset
    1. User enters email on /forgot-password
    2. System sends a reset link
    3. User sets a new password

  Pin:
    Passwords must be at least 10 characters
    Session duration: 7 days
    Refresh token rotation enabled

bacon/features/invoices.cat

Feature: Invoices
  using: Supabase, Resend

  Users can create, edit, send, and track invoices.
  Each invoice belongs to the logged-in user.

  Model: Invoice
    id:           uuid, primary key
    owner:        reference to Profile
    client_name:  text
    client_email: email
    status:       draft | sent | paid | overdue
    due_date:     date
    created_at:   timestamp, auto

    Invariant: owner must equal the authenticated user
    Invariant: must have at least one LineItem when status is "sent"

  Model: LineItem
    id:          uuid, primary key
    invoice:     reference to Invoice
    description: text
    quantity:    integer, min 1
    rate:        decimal

  Flow: Create Invoice
    1. User clicks "New Invoice"
    2. User fills in client name, email, and line items
    3. System calculates totals (subtotal, tax, total)
    4. Invoice is saved as "draft"

  Flow: Send Invoice
    1. User clicks "Send" on a draft invoice
    2. System validates required fields
    3. System generates a PDF
    4. System emails the PDF to client_email
    5. Status changes to "sent"

    On failure (step 4):
      Show error: "Could not send email. Check the address."
      Status remains "draft"

    Pin:
      PDF generation: use @react-pdf/renderer

  Flow: Mark as Paid
    1. User clicks "Mark as Paid" on a sent invoice
    2. Status changes to "paid"
    3. System records payment date

  View: Dashboard
    Show a summary: total outstanding, total paid this month, overdue count.
    List recent invoices with status badges.
    User can filter by status and date range.

  Constraint: performance
    Dashboard must load in under 1 second with up to 1000 invoices

CaT — Code as Text · Draft Specification v0.2 · April 2026