Skip to content

How to use it, by role

One tool, but what you have in hand and what you want out decide the path through it. Five readers, five paths. Start from the one closest to you.

You are What you have What you want Read
implementing a public rule (a statute, a published policy, a tariff) the article, or the policy PDF code that does what the article says, and that notices the amendment 1. Implementing an existing public rule
implementing a rule of your own that already exists: an internal policy, your service's terms or tariff, a spreadsheet, and perhaps an implementation that runs today the policy document, the spreadsheet, the running code code that does what the document says and answers like the current implementation 2. Implementing an existing rule of your own
designing a new rule (a new public rule, an internal rule, the terms of an online shop or a service) the conditions, in prose or in your head a table with no gap and no contradiction, and the pages that people read to check it or that get published 3. Designing a new rule
implementing from a finished rule a .rule that passes check code in your own language that answers exactly like the table 4. Implementing from a new rule
holding an API's or a message's contract (a .proto, OpenAPI, a JSON Schema) and wanting it kept in line with the business rule the contract's file and the rule's table a CI run that says so whenever the contract and the rule move apart 5. Holding an API contract to the rule

The middle step is the same on every path: nothing comes out of a table that does not pass rulec check (What it proves). Every command below is one of these:

$ rulec --help

Every step below is one of three kinds: something to hand to an agent, something rulec does, or something a person decides. Transcribing an article or a policy into a table, drafting from a spreadsheet, the one line of an adapter around legacy code, wiring the generated code in: all of that an agent can do. Proving there is no gap and no overlap, pinning a source, holding the table to a legacy implementation or to past records, checking that twelve languages agree: rulec does that, mechanically. What stays with a person is deciding the conditions, checking the table, ruling on which side of a mismatch is wrong, and rereading a source after an amendment or a replacement. Under each heading below is who does that step.


1. Implementing an existing public rule

You have a statute or a published policy and want code that does exactly what it says. The lead role here is the source: which row came from which article, held to a copy of the text, so that an amendment is noticed.

An agent transcribes a statute or a published policy into a table (.rule), citing the article with @. rulec holds the table to the copy, proves it has no gap and no overlap, and generates twelve languages. A person compares the article and the table on the page rulec doc renders. When an amendment comes, rulec source outdated says so and the table is reread An agent transcribes a statute or a published policy into a table (.rule), citing the article with @. rulec holds the table to the copy, proves it has no gap and no overlap, and generates twelve languages. A person compares the article and the table on the page rulec doc renders. When an amendment comes, rulec source outdated says so and the table is reread

1-1. Transcribe, citing the article

Who: the agent

An agent or a person transcribes; what matters is that every table or row ends with @source article, so that where it came from stays with it. For a statute, source names the law's id and the date the text is read as of. The word after law says which database: none for e-Gov, the Japanese government's statute database, and ecfr for the US federal regulations — source osha = law ecfr "29 CFR 1910" asof 2026-01-01, cited as @osha "§1910.157".

source stamp_act = law "342AC0000000023" asof 2026-04-01
source measures_act = law "332AC0000000026" asof 2026-04-01

define reduced : bool = made <= 2027-03-31  @measures_act 第91条

table base  @stamp_act 別表第一
policy unique
         | stated | amount                   | -> tax : money[JPY] |
unstated | false  | -                        | 200JPY              |
r3       | true   | >=10_000JPY <=100_000JPY | 200JPY              |

The whole notation is in Write a rule. A supplementary provision is cited as @stamp_act 附則第3条, an amending law's as @stamp_act 附則(令和七年三月三一日法律第一三号)第3条.

1-2. Fetch the copy and pin it

Who: the agent

check never reads the network. The article's copy lives beside the rule, and its digest is written into the rule. Two commands:

$ rulec source fetch rules/stamp_duty.rule
measures_act: fetched 第91条 (sha256:85faf53f6f6e8196)
$ rulec source pin rules/stamp_duty.rule
measures_act: pinned 1 fragments

pin writes one line under the source: 第91条 sha256:85faf53f6f6e8196. The copies go to sources/law/<law id>@<date>/; commit them.

1-3. Check

Who: rulec. The agent fixes the table until it passes

$ rulec check rules/stamp_duty.rule
ok rules/stamp_duty.rule

When the article's side changes, check stops there. A copy that differs is E038, naming the definitions to reread:

$ rulec check rules/stamp_duty.rule
error[E038]: Fragment `第91条` of source `measures_act` has changed
  --> rules/stamp_duty.rule:7 source measures_act
  |
7 |   第91条 sha256:85faf53f6f6e8196
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pinned: sha256:85faf53f6f6e8196
  |
 The copy now: sha256:625713b9504d23dc
 Definitions to reread: definition reduced, table reduced_rate

A missing pin is E037, a missing copy E039; rulec explain E038 explains any of them.

1-4. Show it to a person

Who: a person, who compares the article with the table. rulec renders the page

Comparing the article with the table is a person's job. rulec doc renders the page, quoting the cited article from the copy.

$ rulec doc rules/stamp_duty.rule > stamp_duty.md

Under the table's heading, the page reads:

Source: measures_act 第91条 (law 332AC0000000026, as of 2026-04-01; in force from 2026-04-01, as amended by Act No. 12 of 2026)

> 第九十一条
> 平成二十六年四月一日から令和九年三月三十一日までの間に作成される…

The reader compares the rows with that quotation and nothing else. How to read the page is in Showing it to people.

1-5. Generate and hold the code to the table

Who: the agent. rulec checks that the twelve languages agree

$ rulec gen rules/stamp_duty.rule --out generated/
$ rulec test generated/
ok    japan_stamp_duty_split (Python) 296 vectors
ok    japan_stamp_duty_split (SQL) 296 vectors
…

The header of every generated file names the article, its date and its digest, so a reader of the code can tell which text it was made from:

# Cites: measures_act = law 332AC0000000026 asof 2026-04-01 (第91条 sha256:85faf53f6f6e8196)

Where an implementation already runs, hold the table to it before anything is replaced, as in 2-3.

1-6. Notice the amendment

Who: rulec, weekly in CI. A person rereads the article and decides

Statutes get amended. check is held to the copy, so this is the one way to learn of an amendment; put it in a weekly CI job.

$ rulec source outdated rules/pension_premium.rule
厚生年金保険法: the amendment enforced on 2027-09-01 changes 第20条; the version in force from that day needs a reread rule
  add `source 厚生年金保険法_20270901 = law "329AC0000000115" asof 2027-09-01` and transcribe the rows in force from that day from it
  第20条: - 六〇五、〇〇〇円以上
  第20条: + 六〇五、〇〇〇円以上六三五、〇〇〇円未満
  第20条: + 第三二級
  第20条: + 六五〇、〇〇〇円
  …

What changes, from when, and the source line to add. The day an amendment takes effect is written as a row condition on a date column (月分 >=2027-09-01), with the old rows and the new rows in the same table; a gap or an overlap between them stops check.

Take the date as an input, and the amendment folds in

An amendment usually says "applies to documents made on or after the day it comes into force; earlier ones follow the old rule". The switch is not the day you run the code but the day the document was made, the wage paid, the month insured. Take that date as an input and write it into the row conditions, and the versions need no separate files.

Putting it in CI is on the install page. outdated exits 1 when something changes, so its output can become an issue as it is.


2. Implementing an existing rule of your own

An internal policy, the terms or the tariff of your own service, a spreadsheet someone keeps, code that already runs. The rule is not public, but it is decided and in force, and you want code that does the same. The lead role here is the comparison: unlike a statute, the document cannot be fetched again, so it is pinned whole by its digest; and where an implementation or past records exist, the table is held to them and every mismatch comes back by row.

An agent transcribes what is at hand - an internal policy, the terms of your own service, a spreadsheet, code that runs today - into a table (.rule): a spreadsheet becomes a draft through rulec import, a document's table is cited with @ and pinned by the digest of the copy. rulec proves no gap and no overlap, holds the table to the legacy implementation and to past records, and returns every mismatch by row, count and amount. A person compares the document and the table on the page rulec doc renders. From a passed table come twelve languages An agent transcribes what is at hand - an internal policy, the terms of your own service, a spreadsheet, code that runs today - into a table (.rule): a spreadsheet becomes a draft through rulec import, a document's table is cited with @ and pinned by the digest of the copy. rulec proves no gap and no overlap, holds the table to the legacy implementation and to past records, and returns every mismatch by row, count and amount. A person compares the document and the table on the page rulec doc renders. From a passed table come twelve languages

2-1. Start from what you have

Who: the agent. A person confirms every line marked guess

A spreadsheet becomes a first draft. Every guess is marked, so only the marked places need a look.

$ rulec import xlsx tariff.xlsx --sheet Base --name parcel_fee > rules/parcel_fee.rule
rule parcel_fee v1
description "A draft that rulec import made from tariff.xlsx (sheet Base). Every line marked guess is for a person to confirm"

enum dest_values = domestic | canada | overseas  # guess: the values seen in this column, as an enum; add what is missing, and rename the aliases
enum weight_values = <=2000g(v1) | >2000g(v2)  # guess: the values seen in this column, as an enum; add what is missing, and rename the aliases

inputs
  dest : dest_values
  weight : weight_values

outputs
  fee : money[USD]  round down(1USD)  # guess: the rounding's direction and grid come from the source; if it has none, write down that this is a placeholder
…

A policy document is transcribed as in 1-1. If all there is is the running code, hand that code to an agent to transcribe, and hold the table to the code in 2-3. The running code is not touched.

2-2. Pin the document as a file

Who: the agent. rulec notices a replaced document

It cannot be fetched again, so the document itself sits beside the rule and its digest, whole, goes into the rule. It is cited as @terms, or — to say which table of the document was transcribed — as @terms table1, the first table of the document in document order (表1 is the same name in Japanese).

source terms = file "shipping-terms.md"

table base_rate  @terms table1
policy unique
| dest               | weight | -> base : money[USD, incl_tax] |
| north_america      | <=10lb | 8USD                           |
| north_america      | >10lb  | 14USD                          |
| not: north_america | <=10lb | 26USD                          |
| not: north_america | >10lb  | 42USD                          |

Citing a table makes rulec source fetch take that table out of the document and write it beside it. A sheet is a table in a workbook (.xlsx), a w:tbl in a Word file (.docx), and what it looks like in Markdown and CSV. A PDF or a scan cannot be read here: hand an extractor (docling and the like) to --via, or cite the document whole as @terms.

$ rulec source fetch rules/shipping_fee.rule
terms: took out table1 (3 rows by 3 columns, sha256:c846fef7727dd6e0)
$ rulec source pin rules/shipping_fee.rule
terms: pinned sha256:d1156fa90a72194c
terms: pinned 1 fragments

From here on, an amount in the table has to be a value the copy shows, which is what catches a mistyped digit.

When the document is replaced, check stops with E038 and names the tables that cite it; whether the file changed is known on the spot, from its digest. With a url "…" on it, rulec source outdated asks where it came from, and says whether a cited table moved or only something this rule does not transcribe.

$ rulec check rules/shipping_fee.rule
error[E038]: The copy of source `terms` has changed
  --> rules/shipping_fee.rule:6 source terms
  |
6 | source terms = file "shipping-terms.md" sha256:a4b42e3e6c346e56
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ pinned: sha256:a4b42e3e6c346e56
  |
 The copy now: sha256:a4b1a4052a111009
 Definitions to reread: table base_rate, table surcharge
 Reread the document; if what was transcribed still holds, rewrite the line as follows to pin the new copy.

A mistyped digit looks like this. 14USD written as 11USD sits on the rounding grid, leaves no gap and overlaps nothing: every other check stays green and these two are what fail.

$ rulec check rules/shipping_fee.rule
error[E116]: The amount of row 4 is not in the copy it cites
  --> rules/shipping_fee.rule:24 table base_rate row 2
   |
24 | | north_america | >10lb | 11USD |
   |                           ^^^^^ not in the copy: 11USD
   |
 The copy cited: terms table1
 An amount is not rewritten as it is transcribed, so either it was mistyped or it came from somewhere else. …

warning[W120]: The copy of table1 states values no row uses
 Stated in the copy, used by no row: 14USD

The two name both halves of the same slip. W120 also catches a row that was never transcribed — the completeness check cannot, because the inputs of a dropped row fall into one of the rows that remain.

The page for people quotes the copy under the table's heading — "Source: terms table1 (shipping-terms.md, sha256:d1156fa90a72194c)", then the table itself — and adds one line to what was verified: Every amount in this table is a value the copy it cites (terms table1) shows (E116). The source table above, the rule's table below, and that line between them.

2-3. Hold it to the code that runs today

Who: the agent, for the adapter's one line and verify. A person rules on each mismatch

Where an implementation already runs, hold the table to it before anything is replaced. The legacy code is wrapped in an adapter of about twenty lines, and the cases built from the table's boundaries go through both. rulec prints the adapter's template; the one line to write is the call into the legacy code. The legacy code itself is not touched. The rule here and in 2-4 is the corpus's parcel tariff, parcel_rate.rule (pounds, inches, USD).

$ rulec adapter rules/parcel_rate.rule --template python > adapter.py
# rulec adapter template (rule parcel_rate).
# It only exchanges JSON Lines over stdin/stdout. Call the legacy implementation from here.
import json, sys

sys.stdin.readline()  # handshake
print(json.dumps({"ok": True, "impl": "legacy@REPLACE_ME"}), flush=True)

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    d = req["in"]  # inputs: weight, girth, dest, signature

    # Call the legacy implementation here.
    got = 0  # TODO: legacy.compute(d)

    print(json.dumps({"id": req["id"], "out": {"fee": got}}, ensure_ascii=False), flush=True)
$ rulec verify rules/parcel_rate.rule --adapter python3 adapter.py
Compared 96 / matched 93 (96.875%)
Counterpart: legacy@2024-03

Affected 3 (3.125%)  amount +6
  table size_of row 2 / table base_rate row 7 / table fuel_rate row 2 / table signature_fee row 1     3 records  difference +2 uniform  total +6
    Example: dest=overseas, girth=23, signature=true, weight=1 → rule fee=47 / legacy fee=45

Mismatches come grouped by the rows that matched. Here only the row for small parcels going overseas disagrees. Whether that is a defect in the legacy code, a transcription error in the table or a rounding convention is decided from the row and its example; a mismatch is not automatically anyone's bug.

2-4. Hold it to past records

Who: the agent. A person rules on each mismatch

Without running code, but with records of past cases (the inputs and the values that came out), the rule is applied to the records. Records are one JSON object per line; their shape is checked first, then they are replayed.

$ rulec fixtures lint records.jsonl rules/parcel_rate.rule
records.jsonl: 96 records (96 observed, 0 filled)
No format problems.
$ rulec replay rules/parcel_rate.rule --fixtures records.jsonl --terse
Compared 96 / matched 93 (96.875%)
Counterpart: records.jsonl

Affected 3 (3.125%)  amount +6
  table size_of row 2 / table base_rate row 7 / table fuel_rate row 2 / table signature_fee row 1     3 records  difference +2 uniform  total +6

Both comparisons are described on Compare and replay.

2-5. Check and generate

Who: a person checks, the agent generates

The person who checks it gets the page rulec doc renders, with the document quoted under each table's heading, as in 1-4. Generating and holding the twelve languages to the table is 4. Implementing from a new rule.


3. Designing a new rule

Shipping fees, coupon conditions, whether a return is accepted, an internal criterion, a new public rule: something still being decided that you want to settle as a table. The lead role here is the check: every gap and every contradiction comes back with a concrete input that shows it, so what you forgot to decide is visible before you decide.

Someone designing a rule (shipping, coupons, returns, an internal criterion) writes it as a table (.rule). rulec check returns every gap and overlap with an input that shows it, until the table passes. From a passed table come the page for people, the customer article and the impact of a revision Someone designing a rule (shipping, coupons, returns, an internal criterion) writes it as a table (.rule). rulec check returns every gap and overlap with an input that shows it, until the table passes. From a passed table come the page for people, the customer article and the impact of a revision

3-1. Write the table first

Who: a person decides the conditions. The agent or the person writes the table

Conditions as columns, the answer as the last column. "Canada and the US, eight dollars up to ten pounds" becomes one row.

rule shipping_fee v1
description "Standard delivery fee"

enum zone = domestic | canada | overseas
group north_america = domestic, canada

inputs
  dest   : zone
  weight : mass[lb]  range >=1lb <=70lb

outputs
  fee : money[USD, incl_tax]  round up(1USD)

table base
policy unique
| dest               | weight | -> fee : money[USD, incl_tax] |
| north_america      | <=10lb | 8USD                          |
| north_america      | >10lb  | 14USD                         |
| not: north_america | <=10lb | 26USD                         |
| not: north_america | >10lb  | 42USD                         |

If the rule already lives in a spreadsheet, a first draft can be made from it. Every guess is marked, so only the marked places need a look.

$ rulec import csv tariff.csv --name parcel_fee > rules/tariff.rule

Whether your rule fits a table at all is settled first on Does your rule fit.

3-2. Check it

Who: rulec. A person answers each witness

$ rulec check rules/tariff.rule

A gap comes back with the input that falls through it, and the shape of the row to add. Only the amount is a person's to decide.

error[E101]: Completeness gap: some input matches no row
  --> rules/tariff.rule:14 table decision
   |
14 | table decision  # source: tariff.csv (guess: replace with the document's name and date)
   |       ^^^^^^^^ the input space is not fully covered
   |
 An input that matches no row: dest = overseas, size = S60
 hint: add a row that matches this input.
 The shape of the row to add: `| overseas | S60 | 8USD |`. Its output values are copied from the first row to give a shape that parses; they are not the right amounts.

A contradiction stops with an input both rows match:

error[E105]: Overlapping rows: the same input matches row 4 and row 9
  --> rules/tariff.rule:25 table decision
 Both rows match: dest = canada, size = S60

Fix until it passes. What you fix is the table, never code. The seven checks are on What it proves.

3-3. Write the examples

Who: the person who decided the answers. The agent may write them into the file

The answers you decided go into examples, and every check runs them. The "for instance" of a spec becomes a test that does not go away.

examples
| dest     | weight | -> fee |
| overseas | 12lb   | 42USD  |
| domestic | 9lb    | 8USD   |

3-4. Render the pages for people and for customers

Who: rulec (rulec doc). The people who check the rule and the customer read

From the same table, one page per reader. For the people who check the rule, the facts the table does not show: what was verified, which row hides which, which rounding is provisional. For the customer, no aliases and no diagnostic codes, and instead the answer on both sides of every threshold.

$ rulec doc rules/shipping_fee.rule > shipping_fee.md
$ rulec doc rules/shipping_fee.rule --audience customer > shipping_fee_article.md

3-5. Know the impact of a revision before it ships

Who: rulec (rulec diff). A person decides whether it ships

When a rule is revised, what it does can be known first — in two ways, and the first needs nothing but the two versions.

With no records, rulec diff answers which inputs get a different answer, as a region in the rule's own columns, plus the claim that there are none outside it.

$ rulec diff parcel_rate.rule parcel_rate_new.rule
rule parcel_rate v3 → v4
210 cells, of which 210 are inputs that can occur: 194 same, 16 differ, 0 unsettled, 0 unrealized

  signature = false  and  dest = north_america  and  weight >=11lb <=70lb  and  girth >=23in <=60in
    fee: 19 → 20
    rows: table size_of row 2, table base_rate row 3, table fuel_rate row 1, table signature_fee row 2
    example: dest=domestic, girth=23, signature=false, weight=11

  signature = true  and  dest = north_america  and  weight >=11lb <=70lb  and  girth >=23in <=60in
    fee: 23 → 24
    rows: table size_of row 2, table base_rate row 3, table fuel_rate row 1, table signature_fee row 1
    example: dest=domestic, girth=23, signature=true, weight=11

outside this region the two versions answer alike.

The revision raised the fuel rate for north_america from 5% to 6%, and yet weight and girth are in there: the fee is rounded up to the dollar, and on every other row the new surcharge comes to the same dollar as the old one, so the rise is rounded away. That comes out of the whole rule, not out of the row that changed.

With past records (one JSON object per line), both versions are applied to the same records and the answer is how many of yours move, by name.

$ rulec fixtures lint records.jsonl rules/parcel_rate.rule
records.jsonl: 96 records (96 observed, 0 filled)
No format problems.
$ rulec diff parcel_rate.rule parcel_rate_new.rule --fixtures records.jsonl --terse
Compared 96 / matched 86 (89.583%)
Counterpart: parcel_rate.rule → parcel_rate_new.rule

Affected 10 (10.417%)  amount +10
  table size_of row 2 / table base_rate row 3 / table fuel_rate row 1 / table signature_fee row 1    10 records  difference +1 uniform  total +10

Run the first before the second: it says what can move, and the second says how many of your records land there. --format json names every record that moved, by its line and tag. Where an implementation already runs, 2-3 holds the table to it before anything is replaced. All of it is on Compare and replay.


4. Implementing from a new rule

You have a .rule that passes check and want it inside your app or your batch, in your language. The lead role here is the generated code; what you write is the caller.

From a table that passed check, rulec gen writes code in twelve languages and rulec test holds each to the reference evaluator. The implementer reads how to call it from rulec api and puts the function, the SQL query, the Wasm module or the MCP server into an app, a batch or an agent. Generated code is never edited; when the table changes, gen --check in CI stops the build From a table that passed check, rulec gen writes code in twelve languages and rulec test holds each to the reference evaluator. The implementer reads how to call it from rulec api and puts the function, the SQL query, the Wasm module or the MCP server into an app, a batch or an agent. Generated code is never edited; when the table changes, gen --check in CI stops the build

4-1. Generate

Who: the agent

$ rulec gen rules/shipping_fee.rule --out generated/

Under generated/, one directory per language. Python, TypeScript, JavaScript, Rust, Ruby, PHP, Go, Swift and Java get a function; SQL gets one query over a relation of inputs and the same query as a PostgreSQL function; Wasm gets one module; NumPy gets the rule as data and one fixed evaluator. No runtime, no dependency.

4-2. Read how to call it

Who: the agent

You do not read the generated code to call it; the inventory says how.

$ rulec api rules/shipping_fee.rule | jq -r .python.signature
def shipping_fee(dest: Prefecture, weight: Gram, total: YenInclTax, member: MemberKind) -> YenInclTax:
$ rulec api rules/shipping_fee.rule | jq -r .go.signature
func ShippingFee(in Input) (YenInclTax, error)

Values are integers in the declared unit (1999 for 1,999 g, a rate as a number of steps) and enum members are spelled as the inventory spells them. The entry checks ranges and enums, so a value outside the declaration is refused rather than computed in silence. The details are on Generate and call.

4-3. Hold every language to the table

Who: rulec (rulec test)

Every generated language is run over the cases built from the table's boundaries and compared with the reference evaluator, byte for byte.

$ rulec test generated/
ok    shipping_fee (Python) 68 vectors
ok    shipping_fee (TypeScript) 68 vectors
ok    shipping_fee (Go) 68 vectors
ok    shipping_fee (SQL) 68 vectors
ok    shipping_fee (Wasm) 68 vectors
…

A toolchain that is not installed is skipped, and the skip is reported.

4-4. Wire it in

Who: the agent

Pick the shape the destination takes.

Destination What to use Where to read
your application's code the generated function Generate and call
a recalculation in the database, a closing batch the SQL query, one statement over the input relation SQL is a query, and the same query as a function
an HTTP endpoint with no server of your own the same query as a PostgreSQL function, behind PostgREST or Supabase SQL is a query, and the same query as a function
a browser, or any host at all the Wasm module Wasm
a place where an agent has to decide the generated MCP server, the rule as one tool The rule as a tool for an agent
a form or an API entry the JSON Schema from rulec schema The input checked from the same table

Generated code is never edited. What you want changed is in the table, and a change to the table changes every language at once.

4-5. Keep it in step when the table changes

Who: CI (rulec gen --check)

Commit the generated code and regenerate in CI with --check. A table that changed while its generated code did not stops the build there.

$ rulec gen rules/ --out generated/ --check

The whole job is on the install page, with the two lines that post the impact of a revision to a pull request.


5. Holding an API contract to the rule

The shape of an API's request or of a message, and the values allowed in it, are usually set by a contract: a .proto with Protovalidate's annotations, OpenAPI, a JSON Schema. The business rule also says which values it takes. The two are written by different people in different files, and when they drift apart nobody notices.

When a contract changes, a tool like buf breaking checks that the change is compatible on the wire. Whether the business decisions it feeds still hold is left out, and by design: buf breaking does not read custom options such as Protovalidate's rules, and adding a value to a request enum is not a breaking change to it or to oasdiff. We have found no tool that checks it. rulec checks that, from the rule's side. The lead role here is the contract, and nothing has to be generated.

An agent binds a rule's table (.rule) to the API contract (a .proto with Protovalidate, OpenAPI or a JSON Schema): shape and from say where in the contract each input comes from, and an import ties an enum to the contract's set of values. Whenever the contract or the rule changes, rulec check in CI holds the two together and returns a field that was renamed, an enum value that was added, and a value the contract lets through that the rule refuses, with that value. Whether the contract or the rule is the side to change is a person's decision An agent binds a rule's table (.rule) to the API contract (a .proto with Protovalidate, OpenAPI or a JSON Schema): shape and from say where in the contract each input comes from, and an import ties an enum to the contract's set of values. Whenever the contract or the rule changes, rulec check in CI holds the two together and returns a field that was renamed, an enum value that was added, and a value the contract lets through that the rule refuses, with that value. Whether the contract or the rule is the side to change is a person's decision

Between a request and the rule's answer there are three joints:

Joint What is checked When By
request and contract that the request passes the contract's validation at run time, at the service's door Protovalidate, the OpenAPI validator
contract and the rule's inputs that every path exists, that the enums' value sets agree, and that every value the contract lets through is one the inputs take in CI rulec check
the rule's inputs and the table's rows that the rows cover the declared inputs exactly once in CI rulec check

rulec takes the last two. The first stays with the validation already in place.

5-1. Bind the inputs to the contract

Who: the agent

Each input says with from where in the contract it comes from, and shape names the contract.

shape shipment = proto "contracts/shipment.proto" shop.v1.CreateShipmentRequest

inputs
  dest    : region  from shipment.destination.region
  fragile : bool    from any shipment.parcels where handling = HANDLING_FRAGILE
  parcels : number  range >=1 <=20  from count shipment.parcels

When the contract holds an enum, import proto "<file>" <Enum> -> <enum of this rule> (import jsonschema for a JSON Schema) ties the rule's enum to the contract's set of values. How to write both is in Write a rule, under Imports and under Inputs taken from the caller's object, and two worked rules with their contracts beside them are in Examples.

5-2. Hold them together

Who: rulec (rulec check)

Every rulec check reads the contract's file and holds it to the rule. What it finds:

Mismatch Code
a field the rule names is not in the contract (renamed, removed) E121
the type the rule takes does not fit the contract's E120
the contract's enum and the rule's hold different values E032
a value that came through the contract has no row and no default E033
a value the contract lets through is one the rule's input refuses E122
a row is reached only by values the contract never lets through W123
a constraint between two inputs is one the contract does not keep E123
a row is reached only by a combination the contract never lets through W124

The one met most is E122. A contract that puts no cap on an order's lines, for example:

$ rulec check rules/order_shipping.rule
error[E122]: The contract lets `order.lines` hold 51, which the rule refuses
  --> rules/order_shipping.rule:13
   |
13 |   lines : number  range >=1 <=50  from count order.lines
   |                                   ^^^^^^^^^^^^^^^^^^^^^^ the contract lets through 1 or more; lines takes >=1 <=50
   |
 A value that passes the contract's validation is still refused at the door of the generated code: an API answers the request with an error, and a Kafka consumer stops or sends the message to the DLQ.
 hint: if that value cannot occur, narrow the contract with "minItems": 1, "maxItems": 50. If it can, widen the rule's range and decide what it answers for that many. Which of the two is a person's decision.

An order of 51 lines passes the contract's validation and is refused by the rule: an API answers it with an error, and even where no generated code runs, the rule has no answer for that many. fix.text is the keyword to add to the contract, here "minItems": 1, "maxItems": 50.

5-3. Decide which side to change

Who: a person

A mismatch can be the contract's error or the rule's. If an order of 51 lines really cannot come, narrow the contract; if it can, widen the rule's range and decide what it answers for that many. rulec offers both; which one is for the people who own the contract and the rule.

5-4. Stop on every change

Who: CI (rulec check)

With the contract's file where the rule can see it, putting rulec check in CI is all there is to it. A change to the contract and a change to the rule are held together by the same job.

$ rulec check rules/ --diff-base origin/main

--diff-base reports only what is new on the branch. A contract kept in another repository is fetched beside the rules before the run.

5-5. Decide whether to generate

Who: a person

No generated code has appeared in any of this. The service's implementation can stay as it is, and the rule can be kept only to check the contract. To hold the implementation to the table as well, rulec verify in 2-3 does that. To generate, rulec gen also writes a function that takes the request in the contract's shape (Generate and call).

The same holds for the contract of a message on a queue such as Kafka. A message that passes the contract's validation and is refused by the rule stops the consumer or goes to the DLQ; CI finds it before any message is sent.


Where next

Write a rule (.rule) What it proves Generate and call Compare and replay