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.
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.
The bacon/ folder is the source of truth. Humans author
and version .cat files. Code in src/ is a derived artifact.
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.
Transpilation runs on every commit via GitHub Actions. The LLM opens a PR
with the generated code. Humans review and merge — like terraform plan.
Use using: to bind features to known providers (Supabase, Stripe, Resend).
The LLM uses idiomatic patterns. No reinventing the wheel.
| Level | You write | LLM decides |
|---|---|---|
| Intent | "I need auth" | Everything: provider, flow, UI, code |
| Strategy | using: Supabase Auth | Wiring, components, error handling |
| Behavior | Flows and steps | Implementation code |
| Pin | Hard constraints | Everything else |
| Literal | Exact code blocks | Nothing — hands off |
The proof of concept targets a Supabase + Next.js stack to keep scope manageable while demonstrating the full loop: bacon → transpile → PR → deploy.
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
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
| Component | What | Priority |
|---|---|---|
| GitHub Template | Repo scaffold with folder structure, workflow, config | P0 |
| CaT CLI | cat transpile — reads bacon, calls LLM, writes to src/ | P0 |
| Purr File | Track content hashes, outputs, dependencies, incremental diffs | P0 |
| Supabase Provider | Provider profile + system prompt tuning for Supabase idioms | P1 |
| GitHub Action | CI wrapper around the CLI, PR creation | P1 |
| Formal Parser | AST parser for .cat files (optional for v0.1 — LLM reads raw text) | P2 |
.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.
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.
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
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
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.
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:
...
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.
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
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"]
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
Model: is followed by the model name in PascalCase.
Fields are indented with the format: name: type, ...modifiers.
text, integer, decimal, boolean,
date, timestamp, uuid, email,
url, json, currency.
Enums use the pipe syntax: value_a | value_b | value_c.
primary key, nullable, unique,
auto (auto-generated), min N, max N,
default VALUE.
References use reference to ModelName.
Lists use list of ModelName.
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"
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
Flow: is followed by the flow name. Steps are numbered
lines with natural language descriptions. The LLM translates steps
into code.
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.
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"
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.
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.
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
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
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."
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()
}
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.
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.
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
Constraints can appear at the feature level (apply to that feature) or in
system.cat (apply globally). Feature-level constraints override
system-level ones.
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
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.
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.
.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/
Models: PascalCase
Fields: snake_case
Features: Title Case
Files: kebab-case.cat
2 spaces for nesting. Blocks are defined by their keyword and indentation level, similar to Python or YAML.
Lines starting with # are comments. Comments are
passed to the LLM as additional context but have no semantic meaning.
Within a feature: description first, then using:,
then Model:, then Flow:, then View:,
then Pin:, then Constraint:.
| Keyword | Context | Purpose |
|---|---|---|
System: | system.cat only | Declares the system name and description |
Stack: | system.cat only | Defines the technology stack |
Feature: | any .cat file | Declares a feature / capability |
using: | inside Feature | Binds to one or more providers (comma-separated) |
Model: | Feature or standalone | Defines a data model / entity |
Invariant: | inside Model | Business rule on a model |
Flow: | inside Feature | Describes a user-facing behavior sequence |
View: | inside Feature | Describes a page or UI component layout |
On failure: | inside Flow | Error handling / recovery logic |
If / When: | inside Flow | Conditional branching |
Pin: | Feature or Flow | Hard constraint the LLM must follow |
Literal: | Feature or Flow | Verbatim code block |
Constraint: | Feature or system.cat | Non-functional requirement |
A complete CaT spec for a simple invoicing app. Three files, fully readable, producing a full-stack Supabase + Next.js application.
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.
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
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