This is a comparison paper, not the manifesto. Read the manifesto.
A working paper · drafted for the author of the concept
Code as Contract does not need Gherkin.
It needs case law.
Gherkin promises “specifications a stakeholder can read” and delivers a second language, a glue layer, and a spec that can silently drift from the code. Code as Contract already contains a stronger answer: the specification is the top-level clause, and the scenarios are typed tests named as claims — case-law tests. This paper compares the two and hardens the idea into a convention.
The recital — what Code as Contract holds
From the manifesto (CODE_AS_A_CONTRACT.md): code is a contract between many parties
that a machine happens to execute. Definitions are declared before use; every business rule has a
name; the operative clause reads top-to-bottom with near-zero cyclomatic complexity; terms are
pure and execution lives at the edges; tests are case law; the principle applies
recursively without abuse; changes are amendments. Three principles complete the seed so that
entire programs — not just decision functions — can be drafted this way: execution follows the
succession of events (gather → consult → perform); operational demands are
schedules — caching, queuing, forms declared as frozen data and executed by rule-free
performers; and the docket is generated, never hand-written. Schedules are worked through with
examples in the companion paper, Schedules — the internals.
Principle 5 is the load-bearing one for this paper. If tests are case law, then a separate “readable specification layer” is a second, competing contract — and two contracts over the same subject matter is how disputes start.
Exhibit A — the same scenario, three drafts
One rule from the refund contract: goods damaged on arrival refund in full, and the merchant bears the cost. Flip between the drafts.
Two artifacts, one meaning. The .feature file is prose that cannot execute; the step definitions are regex glue that must be kept honest by hand. Neither is the contract — the real terms live in a third place.
# refunds.feature — cannot execute, can drift Feature: Refunds Scenario: Damaged goods refund in full Given a settled order of $100.00 delivered on 2026-07-01 And the goods were damaged on arrival When the customer requests a refund on 2026-07-05 Then the refund is approved for $100.00 And the merchant bears the cost # steps/refund_steps.py — the glue layer nobody reads aloud @given(r"a settled order of \$(\d+)\.(\d+) delivered on (.+)") def step_order(context, dollars, cents, delivered): ... @given(r"the goods were damaged on arrival") def step_damaged(context): ... @when(r"the customer requests a refund on (.+)") def step_request(context, on): ... @then(r"the refund is approved for \$(\d+)\.(\d+)") def step_approved(context, dollars, cents): ...
One artifact. The test name is the claim, the body cites the Definitions by their real types, and the ruling is asserted against the operative clause. A stakeholder can read it aloud; the type checker and the test runner both enforce it.
# case_law/test_clause_4_refunds.py def test_customer_may_return_damaged_goods_for_a_full_refund(): # Facts — the Definitions, cited by type, not by regex order = Order(total_cents=10_000, settled=True, delivered_on=date(2026, 7, 1), damaged_on_arrival=True) # Claim — settled by the operative clause, nothing lower decision = settle_refund_request( RefundRequest(requested_on=date(2026, 7, 5)), order, RefundPolicy()) # Ruling assert decision.approved assert decision.amount_cents == 10_000 assert decision.paid_by is Party.MERCHANT
What both drafts try to specify. Note the contract already reads like the Gherkin — because the operative clause has no cyclomatic complexity to hide behind. That is why the prose layer is redundant.
def settle_refund_request(request, order, policy) -> RefundDecision: """Clause 4 — Refunds. Pure: facts in, decision out.""" if not order_is_refundable(order): return RefundDecision(approved=False, reason=DenialReason.ORDER_NOT_SETTLED) if not refund_window_is_open(request, order, policy): return RefundDecision(approved=False, reason=DenialReason.WINDOW_CLOSED) return RefundDecision(approved=True, amount_cents=refundable_amount(order, policy), paid_by=liable_party(order))
Exhibit B — the full cycle, endpoint to performance
Gherkin stops at the scenario; a contract must also govern execution. This exhibit runs the whole interaction in the CaC discipline: the endpoint is the performer — it gathers facts, consults the terms, performs the decision, and never re-interprets a single term. Flip through the three layers.
Zero cyclomatic complexity. The endpoint reads like the
succession-of-events section of a contract: gather, consult, perform. It contains no business
rule — not even if decision.approved; whether an approval pays out is itself a term
of performance, so it lives in step 3.
# app/refunds/endpoint.py — the performer. Impure, and proud of it; # it holds no terms, only the succession of events. @app.post("/orders/{order_id}/refund-requests") def handle_refund_request(order_id: OrderId, body: RefundRequestBody) -> RefundOutcomeBody: facts = gather_refund_facts(order_id, body) # step 1 — impure reads decision = settle_refund_request( # step 2 — pure terms (Clause 4) facts.request, facts.order, facts.policy) receipt = perform_refund_decision(order_id, decision) # step 3 — impure writes return RefundOutcomeBody.from_decision(decision, receipt)
Fact-gathering is itself contractual: it returns a named, frozen bundle of Definitions — the evidence file. Translation from the outside world (HTTP body, DB row) into the contract's types happens here and only here, so the terms never see a raw dict or an ORM object.
# app/refunds/facts.py — evidence gathering. Impure reads, no rulings. @dataclass(frozen=True) class RefundFacts: """The evidence file: every Definition Clause 4 will cite.""" request: RefundRequest order: Order policy: RefundPolicy def gather_refund_facts(order_id: OrderId, body: RefundRequestBody) -> RefundFacts: """Translate the outside world into the contract's Definitions. Reads only; makes no decision, applies no rule.""" return RefundFacts( request=body.as_refund_request(), # HTTP → Definitions order=orders_repo.load(order_id), # DB row → Order policy=policies_repo.current(), # config → RefundPolicy )
Performance mirrors the decision it executes. Each arm cites the decision's
own fields — it never reaches back into Order to re-derive anything the terms
already settled. If performance needed more information, that is a defect in the
RefundDecision Definition, not a license to re-interpret.
# app/refunds/perform.py — performance of the contract. Impure writes, # executed exactly as decided; the decision is never second-guessed. def perform_refund_decision(order_id: OrderId, decision: RefundDecision) -> Receipt: """Execute Clause 4's decision at the boundary.""" if decision.approved: receipt = payments.credit(order_id, decision.amount_cents, from_=decision.paid_by) else: receipt = Receipt.nothing_owed(reason=decision.reason) notifications.send_refund_outcome(order_id, decision) audit_log.record(order_id, decision, receipt) return receipt
What the case law covers, per layer: Clause 4 gets the full docket (§4).
gather_refund_facts and perform_refund_decision get thin execution-layer
tests — mocks are permitted there, per rule 5, because those layers exist to touch the
world. The endpoint itself needs at most one wiring test: with three lines and no branches, there
is almost nothing left to get wrong.
The comparison, term by term
| Term | Gherkin / Cucumber | Case-law tests |
|---|---|---|
| Artifacts per rule | Three: .feature, step glue, real code | One: the test, against the clause |
| Can it drift from behavior? | Yes — prose and regex bind loosely | No — it fails the build if it lies |
| Type safety | None; steps parse strings | Full; facts are the Definitions |
| Who maintains it | In practice, developers anyway¹ | Developers, honestly |
| Refactoring | Regex glue breaks silently | Rename tooling just works |
| Stakeholder readability | High — prose | High, if the naming discipline holds — that is what §4 enforces |
| Where ambiguity goes | Hides in the glue layer | Nowhere to hide; the clause is cited directly |
¹ The known failure mode of Cucumber-style BDD: business people rarely write or read the .feature files, so teams pay the tax of the DSL without collecting its one benefit. Even BDD's originators stress the conversation matters, not the tool. Case-law tests keep the conversation (the test name is the agreed claim) and drop the tool.
The proposed convention — Case-Law Testing CaC/CL-1
This is the hardening you asked for: principle 5 of the manifesto, drafted as enforceable convention rather than sentiment. Seven rules, each mechanically checkable.
- One file per clause.
case_law/test_clause_4_refunds.pypins Clause 4 and nothing else. The docket mirrors the contract's table of contents. - The test name is a claim a party could bring, written as a full sentence in
snake_case:
test_customer_may_return_damaged_goods_for_a_full_refund. If it can't be read aloud in a meeting, it fails review. (Lintable: name length ≥ 6 words, contains a party noun.) - Facts / Claim / Ruling structure, in that order, comment-marked. This is Given/When/Then without the DSL: facts are constructed from the Definitions, the claim is one call to the operative clause, the ruling is plain asserts.
- Sue at the highest court. Case law calls only top-level clauses, never the
internals (
refundable_amountet al.). Internals stay redraftable as long as rulings stand. (Lintable: an import allowlist per case-law file.) - No mocks in case law. A mock in a case-law test is proof the terms are not pure — fix the contract, not the test. Mocks are permitted only in the thin execution-layer tests at the boundary.
- Precedent is append-mostly. Amending a clause may change a ruling, and the diff must show both together: the clause edit and the case-law edit in one commit. Deleting a case without amending a clause is overruling without authority — blocked in review.
- The docket is the stakeholder document. Generate the human-readable spec
from the case law (test names, grouped by clause file), never the other way around.
One command, e.g.
pytest --collect-only -q case_law/, prints the contract's current case law as prose. Gherkin's one genuine benefit, recovered at zero drift.
Rules 1, 2, 4 and 5 are enforceable today with a ~40-line flake8/ruff plugin or a conftest collection hook; rule 6 is a CI diff check. That is what makes this a convention and not a vibe.
Schedules and Articles — the rest of the program
Clauses and case law cover the business rules; two more instruments cover everything else. A schedule (principle 9) drafts one operational concern as frozen data, a pure ruling, and a generic performer. The membership test: can the obligation be stated as data, and its judgment as a pure function? The catalog:
| Schedule | The terms declare | The pure ruling |
|---|---|---|
| C · Caching | key, TTL, grace | is this evidence still admissible? |
| D · Deferred performance | queue, attempts owed, backoff, deadline | retry, escalate, or give up? |
| F · Forms & screens | fields, rules, actions, payload mapping | which grievances stand? |
| P · Permissions | which party may invoke which clause | allowed, or denied with reason? |
| R · Rate limits | window, quota, scope | is the quota exhausted? |
| G · Feature gates | audience, conditions, sunset date | is the gate open for this party, now? |
| I · Idempotency | key, replay window, on-replay behavior | serve the prior receipt, or reject? |
| T · Retention & privacy | what data, kept how long, then what | keep, anonymize, or purge? |
| S · Recurring performance | cadence, grace, missed-run policy | is it due? what does a miss owe? |
| O · Observability | events that must be recorded; thresholds | is a threshold breached? |
| L · Copy & locale | every user-facing sentence, keyed | pure lookup; a missing key fails the build |
| M · Records | how each Definition persists: table, columns, indexes | does the stored form drift from the declared terms? |
Above the schedules sit the Articles — architecture as governing law: layering and who may import whom, deployable boundaries and what crosses them, one owner per fact of state. Articles are declared as data like any schedule, their violations are found by a pure ruling over the import graph (ordinary case law, run in CI), and they are amended only through a recorded ADR that names the case law it changes. Both are worked with full code in the companion paper, Schedules — the internals.
The dispute test, made interactive
The manifesto's four questions, extended with two for case law. Click each to see the standard it applies.
Adoption order
- Move existing pure tests into
case_law/, one file per clause; rename to claim sentences. - Add the Facts / Claim / Ruling comments; delete any mock from a case-law file by purifying the term it was covering.
- Ship the lint plugin for rules 1–5 and the CI diff check for rule 6.
- Wire the docket command into CI output so every build prints the current case law — that artifact replaces any .feature file you would have written.
- Extract schedules as you touch them: each scattered TTL, retry decorator, permission check or hand-built form becomes a declared term with a pure ruling (§5). Declare the Articles last, once the layers are visible — then let CI enforce them.