This paper works the schedules and articles. The manifesto is the governing text. Read the manifesto.
Companion paper · Code as Contract, principle 9
Schedules: how the internals are drafted.
A real program is not only business rules. Something must be cached, something must run later on a queue, something must be a form on a screen. Code as Contract handles all of these with one move, borrowed from legal drafting: the schedule — an annex of declared terms that the clauses cite. The schedule is frozen data; the machinery that executes it is a generic performer with no rules of its own; and every judgment the schedule requires is a pure function with case law. Three worked examples follow.
The pattern, stated once
Every internal concern — caching, queuing, retries, rendering, validation — decomposes into the same three parts:
- Terms — a frozen dataclass (or
as constobject) declaring the obligation: what is owed, for how long, under what conditions. It lives next to the Definitions and is cited by name. - Rulings — every judgment the terms require (is this still fresh? should this retry? is this field valid?) is a pure function of the terms and the facts. This is where the case law attaches.
- A generic performer — one piece of machinery per concern (a cache wrapper, a queue dispatcher, a form renderer) that reads the terms and consults the rulings. It is written once, contains zero business decisions, and never needs case law of its own — only execution-layer tests.
The test for whether you have done it right: could you change the obligation by editing only the schedule? Shorten a cache from 15 minutes to 5, add a retry, make a form field optional — each must be a one-line amendment to declared data, visible in the diff as exactly that.
Schedule C — caching evidence freshness
Caching is not an optimization trick; contractually, it is a rule about how old evidence may be and still be admissible. Say the refund policy converts currency using an exchange rate: the schedule declares how long a fetched rate stands.
The schedule is data. Nothing here executes; it only declares. Note the TTL is a term with a name, not a magic number inside a decorator call.
# schedules/caching.py — Schedule C. Declared terms, no machinery. @dataclass(frozen=True) class CacheTerms: """How old a gathered fact may be and still be admissible.""" key: str ttl: timedelta grace: timedelta = timedelta(0) # stale-while-revalidate allowance # Schedule C-1: exchange rates stand for 15 minutes, plus a # 2-minute grace during which a stale rate may be served while # a fresh one is fetched. EXCHANGE_RATE = CacheTerms( key="fx:{base}:{quote}", ttl=timedelta(minutes=15), grace=timedelta(minutes=2), )
This is the part Gherkin-era codebases never test: the freshness judgment itself. Because it is a pure function of timestamps and terms, it gets ordinary case law — no clock mocking, no cache server in the test.
# schedules/caching.py — the ruling. Pure: facts in, admissibility out. class Admissibility(Enum): FRESH = "fresh" # serve it STALE_IN_GRACE = "grace" # serve it, refetch in background EXPIRED = "expired" # must refetch before serving def admissibility(fetched_at: datetime, now: datetime, terms: CacheTerms) -> Admissibility: age = now - fetched_at if age <= terms.ttl: return Admissibility.FRESH if age <= terms.ttl + terms.grace: return Admissibility.STALE_IN_GRACE return Admissibility.EXPIRED # case_law/test_schedule_c_caching.py def test_a_rate_older_than_its_ttl_but_in_grace_is_served_while_refetching(): fetched = datetime(2026, 8, 6, 12, 0) now = datetime(2026, 8, 6, 12, 16) # 16 min: past TTL, inside grace assert admissibility(fetched, now, EXCHANGE_RATE) is Admissibility.STALE_IN_GRACE
Written once for the whole program. It reads the terms and consults the ruling — it holds no TTLs, no keys, no policy. Swapping Redis for memcache touches this file and no schedule.
# machinery/cache.py — generic performer. One per program, rule-free. def cached(terms: CacheTerms): """Wrap a fact-gatherer so Schedule C governs its evidence.""" def wrap(gather): def gather_with_terms(**kw): key = terms.key.format(**kw) hit = store.read(key) # impure read match hit and admissibility(hit.fetched_at, clock.now(), terms): case Admissibility.FRESH: return hit.value case Admissibility.STALE_IN_GRACE: tasks.enqueue(refetch, gather, key, kw) # Schedule D! return hit.value case _: return store.fill(key, gather(**kw), clock.now()) return gather_with_terms return wrap # usage, at the fact-gathering layer — the clause never knows: @cached(EXCHANGE_RATE) def fetch_exchange_rate(base: str, quote: str) -> Rate: ...
Schedule D — deferred performance async / queues
A queued task is, contractually, performance that need not be immediate but must be certain. The schedule declares where it runs, how many attempts it is owed, how attempts are spaced, and when non-performance escalates into a breach someone must hear about.
Every deferred obligation in the program is one of these declarations.
Grep DeliveryTerms and you have the complete inventory of what the system owes
later — something no Celery decorator scatter can give you.
# schedules/delivery.py — Schedule D. What is owed later, and how surely. @dataclass(frozen=True) class DeliveryTerms: """Performance need not be immediate, but must be certain.""" queue: str attempts_owed: int backoff_base: timedelta escalate_after: timedelta # breach: page a human # Schedule D-1: refund outcomes reach the customer within the hour # or someone gets paged. Five attempts, exponential spacing. REFUND_OUTCOME_NOTICE = DeliveryTerms( queue="notifications", attempts_owed=5, backoff_base=timedelta(seconds=30), escalate_after=timedelta(hours=1), )
The retry decision is where async bugs live, and it is almost never tested — because it is usually buried in framework config. Here it is a pure ruling with case law. The names of the outcomes are Definitions, so the performer cannot improvise.
# schedules/delivery.py — the ruling. Pure: history in, next step out. @dataclass(frozen=True) class Retry: at: datetime @dataclass(frozen=True) class Escalate: reason: str @dataclass(frozen=True) class GiveUp: pass def after_failed_attempt(attempt_no: int, first_tried: datetime, now: datetime, terms: DeliveryTerms ) -> Retry | Escalate | GiveUp: if now - first_tried >= terms.escalate_after: return Escalate(reason="delivery deadline breached") if attempt_no >= terms.attempts_owed: return Escalate(reason="attempts exhausted") return Retry(at=now + terms.backoff_base * 2 ** (attempt_no - 1)) # case_law/test_schedule_d_delivery.py def test_a_breached_deadline_escalates_even_with_attempts_remaining(): first = datetime(2026, 8, 6, 12, 0) now = datetime(2026, 8, 6, 13, 1) # 61 min, attempt 2 of 5 ruling = after_failed_attempt(2, first, now, REFUND_OUTCOME_NOTICE) assert isinstance(ruling, Escalate)
The dispatcher is the program's only contact with the broker. It consults the ruling after every failure; it decides nothing itself. The performer from Exhibit B defers with one call, citing the schedule by name.
# machinery/tasks.py — generic performer over any broker. def defer(fn, args: tuple, terms: DeliveryTerms) -> None: broker.enqueue(terms.queue, job_of(fn, args, terms)) def on_job_failure(job) -> None: match after_failed_attempt(job.attempt_no, job.first_tried, clock.now(), job.terms): case Retry(at=at): broker.reschedule(job, at) case Escalate(reason=r): pager.page(job, r); broker.park(job) case GiveUp(): broker.park(job) # the refund performer (Exhibit B, step 3) now defers the notice: def perform_refund_decision(order_id, decision) -> Receipt: ... defer(notifications.send_refund_outcome, (order_id, decision), terms=REFUND_OUTCOME_NOTICE) ...
Schedule F — the frontend forms / React
A form is an offer: it states what facts a party must tender, in what shape, and what happens on tender. So the form is a schedule too — fields, validation, buttons, endpoint, payload — declared as one typed object. The React component that renders it is a generic performer: it can draw any form and knows no particular one.
Everything a reviewer would ask about this form — which fields, required or not, validated how, submitting where, sending what — is answered in one declaration. The payload mapping is explicit, so the endpoint's contract is cited, not assumed.
// schedules/refundRequestForm.ts — Schedule F-1. The offer, declared. export const refundRequestForm = { clause: "4", // the clause this form tenders facts to fields: [ { name: "requestedOn", label: "Date of request", kind: "date", required: true, rules: [requestedOnMayNotBeInTheFuture] }, { name: "reason", label: "Why are you returning this?", kind: "select", required: true, options: ["damaged_on_arrival", "changed_mind"], rules: [] }, ], actions: [ { label: "Request refund", kind: "submit", call: { method: "POST", endpoint: "/orders/{orderId}/refund-requests", payload: (v: Values) => ({ requested_on: v.requestedOn, damaged_on_arrival: v.reason === "damaged_on_arrival", }) } }, { label: "Cancel", kind: "dismiss" }, ], } satisfies FormTerms<Values>;
Frontend validation rules are named, pure, and separately testable — exactly like backend clauses. A rule returns a grievance or nothing. Case law runs in vitest with no DOM, no render, no user-event simulation.
// schedules/rules.ts — named rulings. Pure: value in, grievance out. export function requestedOnMayNotBeInTheFuture( value: string, today: string ): Grievance | null { return value > today ? { field: "requestedOn", says: "The request date cannot be in the future." } : null; } // case_law/refundRequestForm.test.ts test("a customer may not date a refund request in the future", () => { const grievance = requestedOnMayNotBeInTheFuture("2026-08-07", "2026-08-06"); expect(grievance?.says).toMatch(/cannot be in the future/); }); test("the form tenders exactly what Clause 4's endpoint is owed", () => { const body = refundRequestForm.actions[0].call.payload({ requestedOn: "2026-08-05", reason: "damaged_on_arrival" }); expect(body).toEqual({ requested_on: "2026-08-05", damaged_on_arrival: true }); });
One renderer for every form in the product. It walks the terms, runs the rules, disables submit while grievances stand, and performs the declared call. Adding a field to the refund form is a diff in Schedule F-1 only — no component edits, no new tests for the renderer.
// machinery/ContractForm.tsx — generic performer. Knows no form. export function ContractForm<V>({ terms }: { terms: FormTerms<V> }) { const [values, setValues] = useState<V>(initialValues(terms)); const grievances = terms.fields.flatMap(f => f.rules.map(rule => rule(values[f.name], today())).filter(Boolean)); return ( <form onSubmit={e => { e.preventDefault(); tender(terms, values); }}> {terms.fields.map(f => <Field key={f.name} field={f} value={values[f.name]} onChange={v => setValues(s => ({ ...s, [f.name]: v }))} grievances={grievances} />)} {terms.actions.map(a => <Action key={a.label} action={a} disabled={a.kind === "submit" && grievances.length > 0} />)} </form> ); } // tender() performs the declared call — gather, consult, perform, // browser edition. It never builds a payload by hand. async function tender<V>(terms: FormTerms<V>, values: V) { const submit = terms.actions.find(a => a.kind === "submit")!; await api.call(submit.call.method, submit.call.endpoint, submit.call.payload(values)); }
The full docket of schedules
Caching, delivery and forms were three worked examples — the pattern covers every operational concern a program has. The test for membership: can the obligation be stated as frozen data, and its judgment as a pure function? If yes, it is a schedule. The recurring catalog:
| Schedule | The terms declare | The pure ruling |
|---|---|---|
| C · Caching | key, TTL, grace | admissibility(fetched_at, now, terms) |
| D · Deferred performance | queue, attempts owed, backoff, deadline | after_failed_attempt(...) → Retry | Escalate |
| F · Forms & screens | fields, rules, actions, payload mapping | field rules → Grievance | null |
| P · Permissions | which party may invoke which clause | may_invoke(actor, clause, terms) → Allowed | Denied(reason) |
| R · Rate limits | window, quota, scope | over_quota(history, now, terms) |
| G · Feature gates | audience, conditions, sunset date | gate_is_open(actor, now, terms) |
| I · Idempotency | key, replay window, on-replay behavior | on_replay(prior_receipt, terms) → ServePrior | Reject |
| T · Retention & privacy | what data, kept how long, then what | disposition(record_age, terms) → Keep | Anonymize | Purge |
| S · Recurring performance | cadence, grace, missed-run policy | is_due(last_run, now, terms), on_missed(...) |
| O · Observability | which events must be recorded; alert thresholds | breach(measurements, terms) → Alert | None |
| L · Copy & locale | every user-facing sentence, keyed and per-locale | (pure lookup — untranslated key fails the build) |
| M · Records | how each Definition persists: table, columns, indexes | undeclared_drift(live_schema, terms) → [] |
Each of the remaining schedules, worked to the same three parts — terms, ruling, case law. Open any of them:
P · Permissions — who is party to the clause
Authorization is a statement about standing: which roles are party to an interaction at all. One declaration per clause; the ruling always answers with a reason.
# schedules/permissions.py — Schedule P. @dataclass(frozen=True) class InvocationTerms: clause: str parties: frozenset[Role] REQUEST_REFUND = InvocationTerms(clause="4", parties=frozenset({Role.CUSTOMER, Role.SUPPORT})) def may_invoke(actor: Actor, terms: InvocationTerms) -> Allowed | Denied: if actor.role in terms.parties: return Allowed() return Denied(reason=f"{actor.role.value} is not party to clause {terms.clause}") # case_law/test_schedule_p_permissions.py def test_a_courier_has_no_standing_to_request_a_refund(): ruling = may_invoke(Actor(role=Role.COURIER), REQUEST_REFUND) assert isinstance(ruling, Denied)
R · Rate limits — the quota is a term, not an nginx line
The quota lives with the clause it protects, not in infrastructure config where no stakeholder will ever find it.
# schedules/quotas.py — Schedule R. @dataclass(frozen=True) class QuotaTerms: scope: str window: timedelta invocations_owed: int REFUND_REQUESTS = QuotaTerms(scope="per_customer", window=timedelta(days=1), invocations_owed=3) def over_quota(history: list[datetime], now: datetime, terms: QuotaTerms) -> bool: return sum(1 for t in history if now - t < terms.window) >= terms.invocations_owed # case_law/test_schedule_r_quotas.py def test_the_fourth_refund_request_in_a_day_is_refused(): noon = datetime(2026, 8, 6, 12) history = [noon - timedelta(hours=h) for h in (1, 2, 3)] assert over_quota(history, noon, REFUND_REQUESTS)
G · Feature gates — every flag carries its own funeral
The sunset date is a required term. A lint rule walks the schedule and fails the build on any gate past its date — flags cannot rot.
# schedules/gates.py — Schedule G. @dataclass(frozen=True) class GateTerms: name: str audience: str # cohort that may pass while the gate stands sunset: date # after this, the gate is closed — and lint flags it INSTANT_REFUNDS = GateTerms("instant_refunds", audience="beta_customers", sunset=date(2026, 10, 1)) def gate_is_open(actor: Actor, today: date, terms: GateTerms) -> bool: return today <= terms.sunset and terms.audience in actor.cohorts # case_law/test_schedule_g_gates.py def test_a_gate_past_its_sunset_is_closed_even_for_its_audience(): beta = Actor(role=Role.CUSTOMER, cohorts=frozenset({"beta_customers"})) assert not gate_is_open(beta, date(2026, 10, 2), INSTANT_REFUNDS)
I · Idempotency — a replay is served its prior receipt
Double-submission is a contractual question: within the window, the same tender is the same tender, and it is owed the original receipt — not a second performance.
# schedules/replays.py — Schedule I. @dataclass(frozen=True) class ReplayTerms: key: str window: timedelta REFUND_SUBMISSION = ReplayTerms(key="refund:{order_id}", window=timedelta(hours=24)) def on_replay(prior: Receipt | None, age: timedelta, terms: ReplayTerms) -> ServePrior | Proceed: if prior is not None and age <= terms.window: return ServePrior(prior) return Proceed() # case_law/test_schedule_i_replays.py def test_resubmitting_within_the_window_returns_the_original_receipt(): prior = Receipt(id="r-1") ruling = on_replay(prior, timedelta(minutes=5), REFUND_SUBMISSION) assert ruling == ServePrior(prior)
T · Retention & privacy — data has a disposition, declared up front
What is kept, for how long, and what happens then — declared when the data is first collected, not negotiated during the audit. The sweeper job (Schedule S) consults this ruling; it decides nothing itself.
# schedules/retention.py — Schedule T. class Disposition(Enum): KEEP = "keep"; ANONYMIZE = "anonymize"; PURGE = "purge" @dataclass(frozen=True) class RetentionTerms: record: str keep_for: timedelta then: Disposition REFUND_DECISIONS = RetentionTerms("refund_decisions", keep_for=timedelta(days=7 * 365), then=Disposition.ANONYMIZE) def disposition(age: timedelta, terms: RetentionTerms) -> Disposition: return terms.then if age > terms.keep_for else Disposition.KEEP # case_law/test_schedule_t_retention.py def test_a_decision_older_than_seven_years_is_anonymized_not_purged(): assert disposition(timedelta(days=7 * 365 + 1), REFUND_DECISIONS) is Disposition.ANONYMIZE
S · Recurring performance — what a missed run owes
Cron expressions say when; they never say what lateness means. The schedule declares the grace and the obligation a miss creates — so "the job silently didn't run for a week" becomes a breach with a named consequence.
# schedules/cadence.py — Schedule S. @dataclass(frozen=True) class CadenceTerms: task: str every: timedelta grace: timedelta RECONCILE_LEDGER = CadenceTerms("reconcile_ledger", every=timedelta(days=1), grace=timedelta(hours=2)) def standing(last_run: datetime, now: datetime, terms: CadenceTerms) -> Current | Due | Missed: since = now - last_run if since < terms.every: return Current() if since < terms.every + terms.grace: return Due() return Missed(overdue_by=since - terms.every) # case_law/test_schedule_s_cadence.py def test_a_run_later_than_cadence_plus_grace_is_a_breach(): last = datetime(2026, 8, 5, 3, 0) now = datetime(2026, 8, 6, 5, 1) # 26h01m: past the 2h grace assert isinstance(standing(last, now, RECONCILE_LEDGER), Missed)
O · Observability — the duty to notice
Which measurements the program is obliged to keep, and the thresholds whose breach must wake someone. The alerting stack becomes machinery; the threshold stops living in a Grafana panel nobody reviews.
# schedules/vigilance.py — Schedule O. @dataclass(frozen=True) class VigilanceTerms: measure: str threshold: float over: timedelta REFUND_ESCALATIONS = VigilanceTerms(measure="refund_escalations", threshold=5, over=timedelta(hours=1)) def breach(count_in_window: int, terms: VigilanceTerms) -> Alert | None: if count_in_window > terms.threshold: return Alert(says=f"{terms.measure}: {count_in_window} in {terms.over}, " f"threshold {terms.threshold}") return None # case_law/test_schedule_o_vigilance.py def test_six_escalations_in_an_hour_raises_an_alert(): assert breach(6, REFUND_ESCALATIONS) is not None
L · Copy & locale — every sentence is a term
User-facing prose is part of the contract — it is the part the customer actually reads. Keyed terms per locale; the case law is exhaustive over the Definitions, so a new denial reason without copy fails the build in every language at once.
# schedules/copy.py — Schedule L. COPY: dict[str, dict[str, str]] = { "refund.denied.window_closed": { "en": "The return window for this order has closed.", "es": "El plazo de devolución de este pedido ha finalizado.", }, "refund.denied.order_not_settled": { "en": "This order has not settled yet — refunds open once it does.", "es": "Este pedido aún no se ha liquidado; podrás pedir el reembolso después.", }, } def say(key: str, locale: str) -> str: return COPY[key][locale] # a missing key raises — by design # case_law/test_schedule_l_copy.py — exhaustive over the Definitions. def test_every_denial_reason_has_copy_in_every_locale(): for reason in DenialReason: for locale in ("en", "es"): assert say(f"refund.denied.{reason.name.lower()}", locale)
Schedule M — records data models
Data models need a careful distinction, because they live on both sides of the line. The
in-memory shapes — Order, RefundDecision — are not a
schedule: they are the Definitions, the contract itself, and they already have a
home in principle 1. What is a schedule is how a Definition persists:
which table, which columns, which indexes — and how that stored form changes over time. Confuse
the two and you get the classic disease: the ORM class becomes the contract, and business terms
end up coupled to storage choices.
One declaration binds a Definition to its stored form. The Definition stays pure and ORM-free; the record terms are the only place storage vocabulary may appear. The fact-gathering layer (Exhibit B) reads through these terms; the clauses never see them.
# schedules/records.py — Schedule M. How the Definitions persist. @dataclass(frozen=True) class RecordTerms: """Binds one Definition to one stored form. Storage words live here only.""" definition: type table: str columns: dict[str, str] # field → column indexes: tuple[str, ...] owner: str # must agree with Article III (state ownership) ORDER_RECORD = RecordTerms( definition=Order, table="orders", columns={"total_cents": "total_cents", "settled": "is_settled", "delivered_on": "delivered_on", "damaged_on_arrival": "damaged_on_arrival"}, indexes=("delivered_on",), owner="storefront.db", )
Drift is the dispute. Three pure rulings keep the parties honest: the record must cover its Definition, the live schema must match the record, and every record must respect Article III. Schema-scanning is impure fact-gathering in conftest; the rulings stay pure.
# schedules/records.py — the rulings. def uncovered_fields(terms: RecordTerms) -> list[str]: """Every field of the Definition must map to a column.""" return [f.name for f in fields(terms.definition) if f.name not in terms.columns] def undeclared_drift(live: TableSchema, terms: RecordTerms) -> list[str]: """The database may hold nothing the schedule does not declare.""" declared = set(terms.columns.values()) return [col for col in live.columns if col not in declared] # case_law/test_schedule_m_records.py def test_every_field_of_order_has_a_declared_column(): assert uncovered_fields(ORDER_RECORD) == [] def test_the_live_orders_table_holds_nothing_undeclared(): live = scan_schema("orders") # gathered fact (impure, conftest) assert undeclared_drift(live, ORDER_RECORD) == []
A migration is never a standalone SQL file: it is the amendment that reconciles the stored form with an amended Definition, and the diff must show all three together — Definition, record terms, migration. CI rejects any one without the other two.
# One amendment, three parts, one commit: # 1. The Definition amended (the contract changes first) @dataclass(frozen=True) class Order: ... damaged_on_arrival: bool gift_wrapped: bool = False # ← new term # 2. Schedule M amended (the stored form follows) ORDER_RECORD = RecordTerms(..., columns={..., "gift_wrapped": "gift_wrapped"}) # 3. The migration performs the amendment — it decides nothing # migrations/0012_orders_gift_wrapped.sql ALTER TABLE orders ADD COLUMN gift_wrapped boolean NOT NULL DEFAULT false; # and the case law that motivated it, in the same commit: def test_gift_wrapped_orders_refund_without_the_restocking_fee(): ...
The counter-test again, for models: if a field exists in the record terms but no clause ever cites its Definition field, that is data collected without a purpose — a term nobody agreed to. Schedule T (retention) will ask awkward questions about it; better to strike it.
Articles — architecture as governing law
Schedules govern one concern each. Architecture is different in kind: it is the governing-law section — the terms that bind every clause at once. Which layers exist and who may import whom; what is synchronous and what is deferred; where state may live; what is one deployable. In most codebases these choices live in nobody's head and everybody's habits. Under CaC they are Articles: declared, enforced, and amended only with a recorded rationale.
The same move as a schedule — frozen data — but the subject matter is the program's own shape. Each Article states one structural obligation every clause inherits.
# articles/governing_law.py — binds every clause in the program. @dataclass(frozen=True) class LayeringTerms: """Article I — who may cite whom. Lower layers never import upward.""" order: tuple[str, ...] ARTICLE_I = LayeringTerms(order=( "machinery", # generic performers: cache, queue, renderer "schedules", # declared operational terms "clauses", # pure business terms + Definitions "facts", # evidence gathering (impure reads) "performers", # endpoints, jobs, handlers (impure writes) )) @dataclass(frozen=True) class BoundaryTerms: """Article II — what is one deployable, and what crosses between them.""" deployable: str may_call: tuple[str, ...] # other deployables, by contract only crossing: str # "http" | "queue" — never a shared DB ARTICLE_II = ( BoundaryTerms("storefront", may_call=("payments",), crossing="http"), BoundaryTerms("payments", may_call=(), crossing="queue"), ) @dataclass(frozen=True) class StateTerms: """Article III — where truth lives. One owner per fact; all else is cache.""" fact: str owner: str ARTICLE_III = (StateTerms("orders", owner="storefront.db"), StateTerms("ledger", owner="payments.db"))
Articles are enforced, not aspirational. The ruling is pure — an import graph is just data — so the architecture check is ordinary case law run in CI. Tools like import-linter already speak this shape; here the terms stay in your Articles and the tool is mere machinery.
# articles/rulings.py — pure: an import graph in, violations out. def layering_violations(imports: ImportGraph, terms: LayeringTerms) -> list[Violation]: rank = {layer: i for i, layer in enumerate(terms.order)} return [Violation(edge) for edge in imports if rank[edge.importer_layer] < rank[edge.imported_layer]] # case_law/test_article_i_layering.py — architecture case law, in CI. def test_no_clause_may_cite_a_performer(): graph = scan_imports("src/") # gathered fact (impure, in conftest) assert layering_violations(graph, ARTICLE_I) == [] def test_deployables_share_no_database(): bindings = scan_db_bindings("src/") assert ownership_violations(bindings, ARTICLE_III) == []
An ADR under CaC is not a wiki page — it is the amendment record of an Article, in the repo, adjacent to the terms it amends. The diff that changes the Article must carry its recital; CI blocks an Article edit without one.
# articles/amendments/0007_payments_becomes_async.md Amendment 7 — to Article II (boundaries) Recital (why): synchronous capture blocked checkout for the p99 of processor latency; the parties accept eventual receipt in exchange for a bounded checkout time. Operative change: storefront → payments crossing amended "http" → "queue", under Schedule D terms CAPTURE_PAYMENT (attempts_owed=5, escalate_after=15min). Case law affected: - test_checkout_completes_without_awaiting_capture (new) - test_capture_breach_pages_the_merchant (new) - test_checkout_returns_the_captured_receipt (overruled)
The hierarchy, top to bottom: Articles bind everything; clauses bind one interaction; schedules bind one operational concern; machinery binds nothing — it obeys. Anything structural you cannot write as an Article with a pure violation check is a choice you have not actually made yet.
Closing — why this completes the seed
With principles 8–10 added to the manifesto, every kind of code in a program now has a place:
- Business rules → named clauses, pure terms, case law. (The manifesto's original ground.)
- I/O and side effects → performers following gather → consult → perform, with the evidence file as the only translation point. (Principle 8.)
- Operational behavior — the full catalog in §5: caching, queues, forms, permissions, rate limits, gates, idempotency, retention, cron, observability, copy — → schedules + pure rulings + one generic performer per concern. (Principle 9.)
- Data models → the in-memory shapes are Definitions (the contract itself); their persistence is Schedule M, and every migration is a three-part amendment. (§6.)
- Architecture → Articles: governing law declared as data, violations found by pure rulings over the import graph, amended only through recorded ADR amendments. (§7.)
- Documentation → the docket, generated from case law, never drifting. (Principle 10.)
Companion to Code as Contract vs.
Gherkin and CODE_AS_A_CONTRACT.md, principles 8–10.