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.

§1

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.

§2

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.

§3

The comparison, term by term

TermGherkin / CucumberCase-law tests
Artifacts per ruleThree: .feature, step glue, real codeOne: the test, against the clause
Can it drift from behavior?Yes — prose and regex bind looselyNo — it fails the build if it lies
Type safetyNone; steps parse stringsFull; facts are the Definitions
Who maintains itIn practice, developers anyway¹Developers, honestly
RefactoringRegex glue breaks silentlyRename tooling just works
Stakeholder readabilityHigh — proseHigh, if the naming discipline holds — that is what §4 enforces
Where ambiguity goesHides in the glue layerNowhere 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.

§4

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.

  1. One file per clause. case_law/test_clause_4_refunds.py pins Clause 4 and nothing else. The docket mirrors the contract's table of contents.
  2. 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.)
  3. 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.
  4. Sue at the highest court. Case law calls only top-level clauses, never the internals (refundable_amount et al.). Internals stay redraftable as long as rulings stand. (Lintable: an import allowlist per case-law file.)
  5. 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.
  6. 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.
  7. 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.

§5

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:

ScheduleThe terms declareThe pure ruling
C · Cachingkey, TTL, graceis this evidence still admissible?
D · Deferred performancequeue, attempts owed, backoff, deadlineretry, escalate, or give up?
F · Forms & screensfields, rules, actions, payload mappingwhich grievances stand?
P · Permissionswhich party may invoke which clauseallowed, or denied with reason?
R · Rate limitswindow, quota, scopeis the quota exhausted?
G · Feature gatesaudience, conditions, sunset dateis the gate open for this party, now?
I · Idempotencykey, replay window, on-replay behaviorserve the prior receipt, or reject?
T · Retention & privacywhat data, kept how long, then whatkeep, anonymize, or purge?
S · Recurring performancecadence, grace, missed-run policyis it due? what does a miss owe?
O · Observabilityevents that must be recorded; thresholdsis a threshold breached?
L · Copy & localeevery user-facing sentence, keyedpure lookup; a missing key fails the build
M · Recordshow each Definition persists: table, columns, indexesdoes 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.

§6

The dispute test, made interactive

The manifesto's four questions, extended with two for case law. Click each to see the standard it applies.

HOLDING — Gherkin externalizes the specification into prose and pays for it with a glue layer and drift. Code as Contract internalizes it: the operative clause is the readable spec, the case law is the executable scenarios, and the docket is the generated stakeholder view. Adopt convention CaC/CL-1; do not adopt Gherkin.
§7

Adoption order

  1. Move existing pure tests into case_law/, one file per clause; rename to claim sentences.
  2. Add the Facts / Claim / Ruling comments; delete any mock from a case-law file by purifying the term it was covering.
  3. Ship the lint plugin for rules 1–5 and the CI diff check for rule 6.
  4. 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.
  5. 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.