Skill v1.0.1
currentAutomated scan100/100+8 new
name: rulec description: Turn a table-shaped business rule into proved, dependency-free Python, TypeScript, JavaScript, Rust, Ruby, PHP, Go, Swift, Java, SQL and Wasm with rulec. Use when a shipping tariff, fee schedule, discount or coupon policy, eligibility test, period classification, or any rule that is already written as a table has to become code; when writing, editing or reviewing a .rule file; when a rulec diagnostic (E001-E076, E101-E140, W105, W110, W111, W114-W132, W901) has to be fixed; or when a change to such a rule has to be shown to a person before it ships. compatibility: Requires the ritsu binary on PATH (cargo install --git https://github.com/i2y/ritsu --locked ritsu); run rulec as ritsu rulec <command>, or as rulec <command> through a link to ritsu named for it. license: MIT OR Apache-2.0 metadata: version: "1.0.1"
When this applies
The rule is a table, or could be written as one — a table plus, where it needs it, a little arithmetic over the inputs. Some are already tables: a tariff, a rate card, a fee schedule. Others are prose or folklore and become a table once written down: when a discount applies, whether a return is accepted, which period a date falls in, what rank a set of scores earns. The source of truth may be a spreadsheet, a published policy, a wiki page, a legacy implementation, or a person, and the job is to turn it into code someone can approve.
A process with state applies one call at a time: the table decides a step, and the caller keeps the state. It does not apply to running a workflow, to judgements about a collection ("any line is refrigerated", "three or more items"), to pattern matching on strings, or to scoring and optimisation. Flatten collection facts at the boundary and pass the scalar in; keep iteration in the caller.
Every command here is rulec's: run it as ritsu rulec <command> (through a link to ritsu named rulec, rulec <command> is the same). Whatever gen writes is held to the reference evaluator byte for byte by test. The files bundled with this skill are listed under §7: read them on demand. Every diagnostic is ritsu rulec explain <CODE>, always current, so none are bundled.
Working with rulec
You are the first user of this tool. It exists so that a business rule — a shipping tariff, a coupon policy, an eligibility test — can be written as one table, proved correct before anyone runs it, and turned into ordinary Python, TypeScript, JavaScript, Rust, Ruby, PHP, Go, Swift, Java, SQL and Wasm — or, for a host that decides whole columns at once, into NumPy: the rule as data beside one fixed evaluator.
Your job is the middle of that: take a source of truth (a published policy, a spreadsheet, a legacy implementation) and produce a .rule that passes rulec check, then generate the code and show a person what changed.
Two roles stay with people, and neither is yours:
- someone approves the table. Amounts, rounding directions, and which of two conflicting
readings is correct are business decisions. When you cannot derive an answer from the source, you ask — §6 says how.
- someone owns the application the generated function is called from.
Everything you need is reachable from the command line. rulec --help lists the commands, rulec <cmd> --help explains one, rulec explain <CODE> explains a diagnostic, and --format json gives you any of it as data. There is no step where you have to read rulec's source. Where you have no shell, rulec mcp serves the same commands as MCP tools over stdio — one tool per command, one argument per flag, the exit code at the end of every result — and this document and the references as resources.
1. The loop
write → fmt → check → (fix, repeat) → examples → gen → test│CI: fmt --check, check --diff-base, gen --check, coverage, test│legacy implementation? → verify past records? → replay, diff neither? → diff│people read and check it → doc
Write
The grammar is reference.md, complete. The shortest useful summary: declare inputs and outputs with their units, write one table per decision, and let examples state a few cases you know the answer to.
Two declarations are mandatory and are where most first drafts fail:
- `range` on every numeric input and every `derive`. It is the universe the completeness
proof quantifies over, and the entry guard of the generated code. A rate may leave it out and is then 0% to 100%, guard included; a rate that can go above 100% declares its range.
- `round` on every numeric output, an optional one (
money[JPY]?) too. Without it the
generated code would settle fractions silently.
A few shapes are worth knowing before the first draft:
- Tables stack. A table's output column is a column of any later table, to any depth, and
one table may produce several output columns. That, plus a derive used as a column, is how a rule with interlocking conditions gets written — not by putting more into a cell.
- An output returns the binding of its own name — a
defineor a table output column
called fee is what the output fee returns. result is sugar for the first output only: naming a later one is E015, and a second result line is E016.
- An output may be `T?`: a row answers
nonefor no value, the others a value ofT. - An output may write a `range` after its `round`; one that leaves out an answer is E140.
- Combinations that cannot happen are said once.
constraint <input> <= <input>states a
relation the caller guarantees. Completeness then demands no row for what it excludes, every witness becomes a case somebody could really send, and the generated code refuses a violating input at the door (§6.1 of the grammar).
- A case may carry a sequence. Where the number of things is not fixed — the rows of a tariff
sheet, the candidates a filter left — elements declares what one element carries and fold says what each verdict does next: next, stop with <value>, take_unique, take_first, keep_max <value> by <key>, plus the required empty and exhausted. The table that judges one element is checked exactly as any other table is, and an example names a sequence rather than holding one in a cell (§6.2). Every target but SQL and NumPy generates it.
- A `string` column takes a prefix and nothing else.
starts_with "CH-", "CL-"is the
one test it accepts (E110 for any other): the prefixes a column's own cells name cut the strings into finitely many classes, which is what keeps completeness decidable. Bytes, no case folding. Where the values can be listed, an enum says more.
- Counting the walk instead of folding it. `count <name>(<alias>) over <sequence> where
<column> = <value> ends the walk with a number rather than with the answer, and the rule goes on as usual — so what turns "how many matched" into a class is an ordinary table, and its boundaries are checked like any other (§6.3). The range is required: it is the universe the completeness check quantifies over **and** the cap on the sequence. sum <name>(<alias>) over <sequence> of <column> ends it with a total instead, over a column that cannot go negative, and max/min with the largest or the smallest. An average divides inside a rounding: half_up(total / max(n, 1), 1JPY)`. Folds and these mix freely, over any number of sequences; one item reading the elements of two sequences is E031.
- Handing one amount out over several lines. `allocate(<amount>, <running total>,
<whole>) is <amount> × <running total> ÷ <whole> rounded down. Write one line's share as the share up to it minus the share up to the line before: the parts then add up to the amount exactly, odd yen included. Three names with declared ranges, nothing negative, a positive whole, and a constraint` that the running total never passes it — else E117.
- A process that goes on is decided one call at a time.
machine <name>(<alias>) over <table>
says which output the caller passes back as which input (carry), which inputs a case holds (held), where it starts and ends, and what no sequence of calls may do; check proves that over every sequence (E124–E127) with the shortest breaking one in witness.trace, and scenario is an example several calls long (§6.4); more carry lines carry numbers, flags or enums, held to always (E072, E134, E135). diff of two versions names the states a case could strand in.
- A main rule and its special case are two tables, or a table and a clause. Several tables may
define the same output, each transcribed from its own source, and the one that takes precedence says so with overrides <table> right after policy (overrides base:r3 for one labelled row). A row is labelled by a word before its first bar (r3 | … |), which is also how the trace names it. A proviso whose conditions do not line up as columns is a clause: when <column> <cell> and … (or when always), then <value>, and overrides — one row written as prose, checked and generated as a one-row table. The definitions of one output are checked together — completeness over their union, every overlap either ordered by an overrides line or reported (E105) — and doc says which is the exception (§7 of the grammar).
- A source is declared, cited and pinned. `source stamp_act = law "342AC0000000023" asof
2026-04-01 names a law on e-Gov and law ecfr "29 CFR 1910" one in the US eCFR. @stamp_act 別表第一 at the end of a table, clause, row, derive or define line says what it transcribes. rulec source fetch puts a copy of each cited fragment beside the rule and rulec source pin writes its digest; check then holds the rule to the copies (E037–E039, W119) without reading the network, and doc quotes the fragment under the definition. Any other document is source japanpost = file "…" sha256:…, with a url "…" so that fetch can bring it again and outdated can ask whether it moved on. **A document's fragments are its tables**: @japanpost table1 cites the first table in document order (a sheet, in a workbook), pinned, checked and quoted as a law's article is. The formats read are csv, md, xlsx and docx; for a PDF or a scan, fetch --via <cmd> runs an extractor as a child process and pins what it produced (rulec adapter <file.rule> --template docling` prints the shape). The rows are then held to that copy: an amount the copy does not show is E116, a number it states that no row uses is W120, and a boundary it puts on the other side of itself is E119 — the only checks that look outside the rule at all.
- A provision applied mutatis mutandis is an `apply`. `apply retirement = "retirement_pay.rule"
meaning:… uses another rule with every input bound (years = tenure, enums mapped with with term_end -> retirement_age), definitions left out with except reduction, and outputs taken under a name (allowance -> part_time_allowance). The callee is expanded into the rule under retirement:…, held to its meaning, comments aside (E040, rulec source pin writes it), and what is passed has to stay inside its ranges (E043). check and doc` show the applied tables as the callee wrote them.
- An input may say where the caller's object holds it. `shape order = jsonschema
"order.json" "#/$defs/Order" borrows the contract the object is already described by, and from says where the value stands in it: from order.shipping.zone, from any order.lines where chilled = true (or all, count), from sum order.lines of amount_usd, from max order.lines of amount_usd empty 0USD; elements lines from order.lines` takes a whole sequence. It changes no check of the table; what it buys is the glue generated, a renamed field caught as E121, and a value the contract's validation lets through but the input refuses caught as E122 (§3.3 of the grammar).
- An input may be drawn.
roll : number range >=0 <=99 randomsays the caller draws it
uniformly, and a chances table claims how likely each answer is (1/1000, 5%, <=1/100); check counts it exactly and E133 says where a claim fails. take_weighted <value> by <weight> in a fold picks one element by weight (§6.6).
- A rule may leave places for other files to add to. `open stackable : money[USD] combine sum
range >=0USD <=100USD (or product, max, min), an open enum, table or clause; a contribution file adds terms, values and rows with into, extend and overrides, pinned by signature:. check` runs on the rule alone and merged with the files it is given (§6.7).
- An enum may belong to somebody else.
import proto "<file>" <Enum> -> <enum>(or `import
jsonschema "<file>" "<pointer>" -> <enum>, JSON Schema or OpenAPI in JSON) holds the rule's enum to a service contract on every check: a value on one side only is E032, and a value no row names and no default` marks is E033, not W111 — it arrived as a change nobody has read yet.
- So may the service:
implements proto "<file>" <Service>takes a contract's numbers (E076, §3.4). - A place is an enum rulec has.
import std/<country>/<kind>brings in a country's first-level
divisions (std/us/states, std/jp/prefectures, … in reference.md); import std/都道府県 spells Japan's in Japanese.
- Enums can be ordered, labelled, shared and given prerequisites.
enum tier = basic < silver
takes >=silver, max and range; national "Rest of the country" labels a value; an enums file holds enums rules import enum … from; prerequisites <name> over <enum> lists start and a requires line per value, refusing cycles, unreachable values and a broken ordered by (E136-E138).
- A date input can range over koyomi's days.
range from koyomi "<file.cal>" date <name>, checked by
ritsu rulec check; ritsu check then holds a workflow's calls to the rule's constraints (ritsu's E201).
When the source is a spreadsheet, rulec import xlsx <file.xlsx> writes a first draft from the workbook as it is — no export step, --sheet <name> to pick the sheet, and the first one by default; rulec import csv <file.csv> does the same from a CSV. The columns become inputs, the last column the output, the values an enum or a range, and each guess is marked # guess — yours to correct: a numeric column is copied as equalities and is usually meant as thresholds, and no range, rounding or unit in it is decided. A date, a percentage and a unit from the cell's number format are read back out of it; a word that cannot be a name, or that a target language cannot take, is named for now (c2, v1, Zone(zone)), the word kept in its note. It saves the typing, not the reading.
One habit pays for itself at the first revision:
- Write down where each table came from. A comment at the end of the
tableline names the
source — the document, its edition or date, the page — and a row taken from somewhere else (a later notice, a correction, an answer from a person) carries its own comment at the end of the row. rulec doc shows both to its readers, so a review becomes "compare this row with that cell" rather than "read the whole table again", and when the source is revised, the rows to re-read are the ones that cite it. The description line stays a one-line summary of what the rule decides.
rulec fmt <file>
Run it before check, every time. It aligns the columns and rewrites → ・ 、 ≦ to ASCII, so your diffs stay about the rule and not about whitespace.
rulec check <file> --format json
The centre of the loop. One JSON object per finding (formats.md). Read these fields:
| field | what to do with it | |
|---|---|---|
code | decides the fix. rulec explain <code> if you do not recognise it | |
where.table, where.row | which table and row, without parsing the sentence | |
witness.inputs | a concrete input that exhibits the problem. This is the thing to reason about | |
rows | every row that takes part | |
fix.kind, fix.text | the rewritten form, ready to paste — see the warning in §3 | |
notes | prose, including the caveats that fix.text cannot carry |
--terse cuts each finding to heading, position and witness when you are scanning many.
Exit codes: 0 no errors, 1 errors, 2 bad arguments or an unreadable file. Read the exit code — not whether the output looks empty.
Fixing, by code
Every code is in rulec explain --all. The ones you will meet while transcribing:
- E101 completeness gap — some input matches no row, and the witness names it. Add a row
that covers it, or mark the value default in the enum to let it fall through to a catch-all. One row per run; run again for the next gap.
- E105 overlap / W105 shadowing — two rows match the same input. Under `policy
unique that is an error; under policy first` the earlier row wins and you are being asked whether that is intended.
- E102 unreachable row — a row nothing can reach: earlier rows cover it, the upstream table
never produces its value, or a derive or a define (a boolean one too) never takes what it asks.
- E103 unit mismatch — you are adding or comparing values of different units, currencies
or tax flags, or a heading's type is not its name's. The fix is a table, not a cast.
- E104 no rounding / E106 off the grid — see §3.
- E107 example does not hold — the rule and your expected value disagree. **Decide which
one is wrong from the source**, not from which is easier to change.
- E112 derived range too narrow — the message states the interval to widen to.
- E014 expression in an output cell — a cell to the right of
->holds one value or one
name. Give the calculation a name on a define line and put that name in the table.
- E116 an amount not in the copy / W120 a value of the copy no row uses / **E119 a
boundary on the other side of itself** — the row and the table it cites disagree. A mistyped digit raises the first two; Under 18 transcribed as <=18 raises the third, and nothing else catches that one. A value from elsewhere loses its citation and gains a comment saying so.
- E114 value off the column’s step —
0.5%in arate[step 1%]column has no runtime
representation. Write a value on the step, or declare a finer step (rate[step 0.1%]).
- E022 / E023 / E024 a fold with a hole —
emptyandexhaustedare both required, and
every verdict the table can produce needs an arm. The three are the completeness argument, applied to the walk.
- E028 / E029 / E030 a count that cannot be counted — the shape is `count <name> over
<sequence> where <column> = <value>`; the column has to be one an element has, with values that are a closed set; and the range is required. E030's message says why it is two things at once.
- W111 unused declaration — see §3.
examples
Add them as soon as the tables check clean. They are the only thing that can catch an error the reference evaluator and every generated language share — which happened once, with rounding for multiple outputs missing in all of them. Take the cases from the source: a published tariff's own worked examples are ideal.
They ride in the vector suite. gen writes each out as a case of its own, so rulec test runs them through every generated language and rulec coverage counts what they reach. A row goes in only when it names every input.
rulec gen <file> --out generated/ --format json
Writes Python, TypeScript, JavaScript, Rust, Ruby, PHP, Go, Swift, Java, SQL, Wasm, NumPy, and the vectors. It refuses to generate from a rule that does not pass check. rulec api <file> tells you how to call the result — signatures, parameters with units and ranges, enum member spellings, errors — so you never have to read the generated code to integrate it (generated-code.md carries a heading for each). Beside each function is a twin with _traced on its name that also returns the rows that matched, one per table in order — the row numbers rulec doc prints, and what an answer to "why this amount" needs; rulec test holds those rows to the reference evaluator as well as the values. A third, _record, turns one call into one line of the fixtures format, so what replay and diff need comes out of the generated code itself. A fourth file beside the module, <alias>_mcp.py, serves the rule as one MCP tool for an agent that will call it: the arguments are the wire form, the answer is the record line, and --record <file.jsonl> keeps every call. It speaks stdio, and with --http <port> MCP's Streamable HTTP — put TLS and authentication in front of that one; where the host renders MCP Apps it also offers the page for people (<alias>_page.html) as the tool's view. proto/ and <alias>_service.py are the same rule as a Connect service, for the caller that is another team's code. The wasm/ directory holds the rule as one module for any host, behind call: func(input: string) -> string in the canonical ABI, with a .wit that makes a component of it. The sql/ directory holds two doors on one query: <alias>.sql over a relation of inputs, and <alias>_function.sql, the same query as a PostgreSQL function asked one case at a time — an RPC endpoint the moment it sits in a schema PostgREST or Supabase exposes.
Beside the Rust module, <alias>_proof.rs holds proof harnesses for the Kani model checker, behind #[cfg(kani)] so rustc never reads them: over every input in the declared domain rather than over the vectors, no table falls through, no contradiction guard fires, nothing overflows an i64, and each unique table's rows cover the domain exactly once. rulec test --proofs runs them where kani is installed (generated-code.md).
For a target none of the eleven covers — another language, a workflow engine's expression language, a spreadsheet formula — you need no backend and give up no comparison: generate from rulec api, wrap it in the adapter protocol, and hold it to the rule with rulec verify (backends.md runs that loop end to end).
rulec certificate <file.rule>
The evidence behind all five proofs, as one JSON object something other than rulec can re-check: the tree tiling the input space, the axis each pair of rows of a unique table parts on, a point reaching each row with its values, every computed value's interval and type, each output's range once rounded, and where each cell stands in the file. tools/recheck.py re-checks it with no dependencies; proofs/ proves in Lean 4 that those checks imply the claims and runs them. --rule <file> ties it to that text, cell by cell (formats.md).
rulec test generated/ --format json
Runs every generated language over the vectors and compares them with the reference evaluator, byte for byte. This is the only step that reaches outside: it wants python3, node, rustc, ruby, php, go, swiftc and a JDK, plus the wasm32-unknown-unknown target for the Wasm side, numpy under python3 for the NumPy one, a psql that reaches a PostgreSQL for the function beside the query, and, under --proofs, kani for the proof harnesses gen writes beside the rustc module (that pass is not vectors: it holds the generated code over every input in the declared domain, and costs seconds per rule where the vectors cost milliseconds, which is why it is asked for), and skips-and-reports the side whose toolchain is missing. A skipped language narrows what the run proved, so the summary says how many were skipped and `--require-all` fails when any was — that is the form for CI, where green has to mean the agreement held across all of them.
For one occasion each: CI, rulec verify, rulec diff, rulec doc
Four more steps are for one occasion each: the gate in CI, a legacy implementation to compare with (rulec verify), two versions of a rule (rulec diff), and the pages for people and for customers (rulec doc). They are in more-of-the-loop.md; read it when one of them is the task.
2. Language
Output is English by default. --lang ja (or RULEC_LANG=ja) switches every surface — diagnostics, reports, the rendered document, the prose inside generated code — to Japanese. Names and cell values come from the rule and are unchanged either way.
Use the default for yourself. Use --lang ja for anything a Japanese-reading person will read: the doc rendering and the diff comment on a PR.
JSON keys never move with the language, and neither do witness or fix.text. Only fields whose name says prose (title, notes, what, hint, text in a note) change.
3. What not to do
Do not edit generated code. The header says DO NOT EDIT and gen --check will fail on it in CI. Anything missing belongs in the .rule or on the calling side.
Do not decide a rounding yourself. round down(1JPY) will make E104 go away, and fix.text will hand it to you, but which direction and which grid moves real money. If the source says, use what it says. If it does not, ask — and write the answer's provenance in a # comment next to the declaration, because that comment is the only place the people who read the rule will see that the rounding was assumed.
Do not widen a range to make a check pass. Widening a derive range to the interval E112 names is correct: that interval is what the value can actually reach. Widening an input range so a hole disappears is not — it changes the contract the generated guard enforces, and it hides the hole instead of closing it.
Do not silence a warning to get to green. default on an enum value declares "this value intentionally has no row of its own"; contract_only on an input declares "this only ever guards a range". Both are statements about the business. Using either because a warning was in the way makes the next reader believe something untrue.
Do not add a row with a made-up amount. fix.text for E101 gives you the row's shape, with output values copied from the first row so that it parses. The amount has to come from the source. If you do not have it, that is a question for a person (§6), not a value to invent.
Do not paper over E107 by changing the expected value. Half the time the table is wrong. Decide from the source which side is.
Do not reach for `policy first` to make an overlap go away. first says "the order of these rows is meaningful", which is a claim about the business. If the rows genuinely should not overlap, fix the rows.
4. What rulec will not do, and why that is the point
It has no loops in an expression, no recursion, no state of its own (a machine's is kept by the caller), no nested objects in a cell, and no date arithmetic; only fold, count, sum, max and min walk a sequence, twice when an item needs what the first walk found. Do not look for a way around these. They are the price of the checks terminating: because a cell is a unary test on its own column, a row is a box, and completeness and overlap are exactly decidable. Flatten nested data at the boundary; keep the rest of the iteration in the caller.
It will not print a green result it cannot prove. When a check runs out of budget (E109) or cannot decide an overlap (W114), it says so rather than approximating.
5. Reading a diagnostic as data
A worked example. Ask for JSON, act on the fields, never on the sentence:
$ rulec check rules/parcel.rule --format json | jq -c 'select(.code=="E101") | {table:.where.table, witness:.witness.inputs, fix:.fix}'{"table":"base_rate","witness":{"dest":"overseas","size":"small","weight":1},"fix":{"kind":"add_row","text":"| overseas | small | 1lb | 6USD |"}}
The prose in title and notes is allowed to improve between versions; the codes and the JSON shape are a stable API. Anything you build that parses a sentence will break, and it will break silently.
6. Asking a person
The checks turn vague gaps into specific questions, and a specific question is the one a person can answer in a sentence. Convert the structured finding, not the prose.
| what you have | what to ask | |
|---|---|---|
E101, witness.inputs = {dest: overseas, size: small} | "What is the fee for an overseas parcel of the small size?" | |
E104 on output fee, notes saying the spread is 9 yen | "Which way should the fee be rounded? Rounding up or down changes it by up to 9 yen. Do the terms say?" | |
| E105 between rows 3 and 7 with different outputs | "Is this input 1,200 yen or 800 yen? It meets the conditions of both." | |
| W114 | "Can an order that meets both of these conditions at once exist?" | |
E126, witness.trace = cancel → pay → ship | "What happens to an order when a payment notice arrives after the cancellation? As the table stands it goes back to paid and on to shipped." | |
| A rounding you assumed | "This rounding has no basis in the terms. I have assumed rounding down for now. Is there a source?" |
Three things make such a question answerable: a concrete case (the witness), what turns on the answer (the amount that moves), and what you assumed in the meantime, so that silence does not read as agreement. Keep that assumption in the file — a # comment where the declaration is, or, for an amount taken from outside the table's own source, at the end of its row. rulec doc shows those comments to its readers, the one place a person who can overrule them will see them.
7. Where to look
| reference.md | the complete grammar | |
| examples.md | complete rules that all pass check, generate, and agree across every implementation | |
| formats.md | every machine-readable format: --format json, vectors, fixtures, the manifest, the adapter protocol | |
| generated-code.md | the shape and guarantees of the generated code in each language, and how to call it | |
| backends.md | targeting a language rulec does not generate, without losing the comparison | |
ritsu rulec explain <CODE> | one diagnostic: when it appears, how to fix it, a runnable reproduction. --all for every one, --format json for data | |
| <https://github.com/i2y/ritsu/tree/main/crates/rulec> | the source and the design document, in the ritsu repository | |
| <https://i2y.github.io/ritsu/rulec/> | the documentation site: the tour and the worked rules written for a person — English, and Japanese under /ja/ |