Durable intent for AI-assisted codebases.
AI assistance makes the first weeks fast and the later months expensive. An agent adds the module that already existed, rewrites the unit that only needed a tweak, lets intent and code drift — each change locally reasonable, the sum globally bad. The cause isn't model capability; it's that nothing tells the agent what this unit is for before it edits this unit.
Trellis pairs every unit of code with a sidecar that states its intent — an invitation for humans and agents alike to understand a unit before they change it.
One contract, beside the code.
A .trellis sidecar states a unit's contract: what it provides, what it consumes, what must always be true, and what is explicitly not its job — identical in structure from language to language. Idiomatic source on the left; its sidecar on the right, highlighted by the same grammar that ships in the editor extension, not faked for the page.
# frozen_string_literal: true
# Creates a user subscription and performs the initial charge.
# Idempotent on (user, plan_id, idempotency_key).
class CreateSubscription
class PaymentError < StandardError; end
def initialize(gateway: PaymentGateway.new)
@gateway = gateway
end
# @return [Subscription]
# @raise [PaymentError] when the gateway declines the charge
def call(user:, plan_id:, idempotency_key:)
plan = Plan.find(plan_id)
charge = @gateway.charge(
token: user.payment_token,
amount: plan.price_cents,
idempotency_key: idempotency_key,
)
raise PaymentError, charge.error unless charge.ok?
subscription = Subscription.create!(user: user, plan: plan, status: :active)
Events.publish("subscription.created", subscription_id: subscription.id)
subscription
end
end
@owner: BillingTeam
@stability: stable
@composition: [Authenticatable, Payable]
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Create Subscription
"Handles the idempotent creation of a user subscription and initial billing."
Provides:
- Subscription.create(user, plan_id) -> Subscription | raises PaymentError
- Event: subscription.created
Consumes:
- PaymentGateway.charge(token, amount) -> ChargeResult
- UserRecord (must respond to: id, email, payment_token)
Invariants:
- A user MUST NOT have two active 'pro' subscriptions
- Charges SHALL be idempotent on (user_id, plan_id, idempotency_key)
OutOfScope:
- Refunds (handled by RefundService)
- Plan upgrades (handled by ChangeSubscriptionPlan)
Scenario (happy-path): Successful checkout
Given a User with a valid stripe_token
When the create method is called with plan_id
Then a Subscription record is created
And the PaymentGateway receives a charge request
And the User status becomes active
Scenario (negative): Expired card
Given a User with an expired card
When the create method is called
Then it MUST raise PaymentError
And no Subscription record is created
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
// Package billing computes prorated charges for mid-cycle plan changes.
package billing
import (
"errors"
"time"
)
// ErrInvalidPeriod is returned when a period's End precedes its Start.
var ErrInvalidPeriod = errors.New("billing: period end precedes start")
// Money is a count of minor units (e.g. cents) in a single currency.
type Money int64
// Period is the billing window a proration is computed against.
type Period struct {
Start time.Time
End time.Time
}
// Proration computes the amount owed when a subscription changes plan
// partway through a billing period. Time enters through now so the unit
// stays pure and testable.
type Proration struct {
now func() time.Time
}
// Calculate returns the amount owed for the remainder of period when
// moving from oldPlan to newPlan. A negative result is a credit.
func (p Proration) Calculate(period Period, oldPlan, newPlan Money) (Money, error) {
if period.End.Before(period.Start) {
return 0, ErrInvalidPeriod
}
elapsed := p.now().Sub(period.Start).Seconds()
total := period.End.Sub(period.Start).Seconds()
remaining := 1 - elapsed/total
delta := float64(newPlan-oldPlan) * remaining
return Money(roundHalfUp(delta)), nil
}
@owner: BillingTeam
@stability: stable
@layer: domain
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Proration
"Computes the credit or charge owed when a subscription changes plan mid-period."
Provides:
- billing.Proration.Calculate @source("symbol:Calculate") -> Money | error
Consumes:
- billing.Period (must carry: Start, End)
- billing.roundHalfUp @source("symbol:roundHalfUp") -> int64
Invariants:
- The result MUST be denominated in the period's currency
- A downgrade with time remaining MUST yield a credit (negative Money)
- Calculate MUST NOT read the wall clock directly; time enters through the injected now()
OutOfScope:
- Applying the proration to an invoice (handled by billing.Invoice.Apply)
- Currency conversion (handled by fx.Convert)
Scenario (happy-path): Upgrade halfway through the period
Given a monthly period that is 50% elapsed
When Calculate is called with a higher newPlan
Then it returns a positive charge for the remaining half
Scenario (negative): Inverted period bounds
Given a Period whose End precedes its Start
When Calculate is called
Then it MUST return ErrInvalidPeriod
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
import type { ApiClient } from "../platform/api-client";
import { publish } from "../platform/events";
export interface InvoiceDTO {
id: string;
customerId: string;
amountDue: number; // minor units
currency: string;
status: "open" | "paid" | "void";
}
/** Creates open invoices for customers and announces them. */
export class InvoiceService {
constructor(private readonly api: ApiClient) {}
async createInvoice(
customerId: string,
amountDue: number,
currency: string,
): Promise<InvoiceDTO> {
if (amountDue <= 0) {
throw new RangeError("amountDue must be positive minor units");
}
const invoice = await this.api.post<InvoiceDTO>("/invoices", {
customerId,
amountDue,
currency,
status: "open",
});
publish("invoice.created", { invoiceId: invoice.id, customerId });
return invoice;
}
}
@owner: BillingTeam
@stability: stable
@layer: application
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Invoice Service
"Creates open invoices for customers and announces them to the rest of the system."
Provides:
- billing.InvoiceService.createInvoice @source("symbol:createInvoice") -> InvoiceDTO
- Type: Billing.InvoiceDTO @source("symbol:InvoiceDTO")
- Event: invoice.created
Consumes:
- ApiClient: Platform.post -> InvoiceDTO
- billing.events.publish @source("symbol:publish")
Invariants:
- amountDue MUST be positive minor units
- Every created invoice MUST start in the 'open' status
- invoice.created MUST be published only after the invoice persists
OutOfScope:
- Charging the customer (handled by billing.PaymentService.charge)
- Dunning and retries (handled by billing.DunningJob)
Scenario (happy-path): Create an open invoice
Given a customer and a positive amountDue
When createInvoice is called
Then it persists an 'open' invoice
And it publishes invoice.created with the new invoice id
Scenario (negative): Non-positive amount
Given an amountDue of 0
When createInvoice is called
Then it MUST raise a RangeError
And it MUST NOT publish invoice.created
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
-- analytics.monthly_revenue
-- Recognized revenue per calendar month, in each invoice's settlement currency.
CREATE OR REPLACE VIEW analytics.monthly_revenue AS
SELECT
date_trunc('month', p.captured_at AT TIME ZONE 'UTC') AS month,
i.currency,
SUM(p.amount_minor) AS revenue_minor
FROM billing.payments AS p
JOIN billing.invoices AS i
ON i.id = p.invoice_id
WHERE p.status = 'captured'
AND i.status = 'paid'
GROUP BY 1, 2
ORDER BY 1, 2;
@owner: AnalyticsTeam
@stability: stable
@layer: reporting
@since: 2025-11-04
@reviewed: 2026-03-15
Feature: Monthly Revenue
"Recognized revenue per calendar month, in each invoice's settlement currency."
Provides:
- View: analytics.monthly_revenue @source("symbol:monthly_revenue")
Consumes:
- Table: billing.payments (must carry: amount_minor, status, captured_at, invoice_id)
- Table: billing.invoices (must carry: id, currency, status)
Invariants:
- Revenue MUST count only 'captured' payments against 'paid' invoices
- Months MUST be bucketed in UTC
- Amounts MUST stay in minor units; the view MUST NOT convert currencies
OutOfScope:
- Refund netting (handled by View: analytics.net_revenue)
- Conversion to a single reporting currency (handled by View: analytics.revenue_usd)
Scenario (happy-path): A captured payment on a paid invoice
Given a captured payment of 1000 minor units on a paid USD invoice in March
When monthly_revenue is queried
Then March / USD revenue includes those 1000 minor units
Scenario (edge): A pending payment
Given a payment with status 'pending'
When monthly_revenue is queried
Then that payment MUST NOT contribute to any month's revenue
left the code ·
right its .trellis sidecar — the intent both you and an agent read before touching the code.
Core Thesis
"Tests verify behavior. Code embodies behavior. Trellis preserves intent — as a reviewable, version-controlled artifact that both humans and agents can reason over before a change is made."
Read further.
Whitepaper
The full case
The problem, the format, the tooling model, and the argument for Trellis as an architectural substrate.
FAQ
The strongest objections
The hardest arguments against Trellis, stated in their strongest form, with the most honest responses.
Get Started
Try it on real code
Install the CLI, wire up your editor, add the agent skill, and write your first sidecar on one real change.