Generated Computers
Locals: an Apple Silicon engine that runs local LLMs up to 3x faster. A product of Generated Computers.
翻译准备中
Nine small languages, one toolchain. What one checks, the next can build on.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Write the rules a system must follow in small languages, and check that every input gets exactly one answer and that no two rules disagree. From the checked rules, ritsu generates code, pages for people to read, and the inputs, answers and scenarios to test with.
Coding agents now write code faster, and more of it, than anyone can read line by line. That makes one question sharper: what is the code there to carry out, whoever — or whatever — writes it? The contracts between services and inside them. The business rules. The calendars and the deadlines. The ledgers and the bounds on them. The skeleton of a workflow. Who may do what. Where each requirement came from, and what still satisfies it.
ritsu gives each of those a small language of its own, in a file people can read. Each language checks what it says as far as it can be checked — over every input, every day, every path, not a sample — generates code where code is needed, and draws pages for whoever needs to understand it. Because the nine share one toolchain, a proof does not stop at a language's edge: a workflow knows the preconditions of the rules it calls, a rule knows the days a calendar can come to, a ledger knows the amounts a rule can return.
Agents write the glue. ritsu holds what it all has to carry out.
| File | What it is for | What it generates | |
|---|---|---|---|
| rulec | .rule | business rules: tariffs, fees, eligibility, tax | functions in twelve languages |
| dandori | .flow | the skeleton of a workflow | Temporal, Step Functions, Argo and more |
| koyomi | .cal | dates: closing and payment days, business days, legal periods | TypeScript, Python, Go, Rust, SQL, Wasm |
| chobo | .book | ledgers: accounts, their bounds, transfers, holds | clients for PostgreSQL, TigerBeetle, MySQL, TiDB and Vitess |
| geas | .geas | claims about the code an agent wrote | — |
| yuen | .req | where requirements come from, and what satisfies them | ReqIF, W3C PROV |
| sakai | .ctx | the map of bounded contexts | settings for import linters; Context Mapper |
| sekisho | .gate | who may do what: roles, attributes, relations, with rules and dates as conditions | Cedar, and the TypeScript, Python and Go that ask it |
| kaname | .kn | a functional core: general code with no input or output, which can call rules, dates and books in their types | TypeScript, Python, Go and Rust, in the package ritsu gen writes, and Wasm from the Rust |
Each language stands on its own: you can use rulec without writing a .flow. A file never mixes
two languages, because a tariff, a calendar, a ledger and a workflow are read by different
people.
A rule is a set of tables, with the calculations, the exceptions and the provisos around them.
rulec check proves that every input in the declared domain gets exactly one answer, checks
units and money, and writes a certificate that a proof in Lean rechecks. A figure can be pinned
to the published source it was copied from, so that a change in the source stops the check.
Only a rule that passes compiles, into plain functions in Python, NumPy, TypeScript,
JavaScript, Rust, Ruby, PHP, Go, Swift, Java, SQL and Wasm.
rule paypal_fee v1
description "The PayPal Checkout fee on one payment in the United States. Transcribed from PayPal's published merchant fees"
source paypal = file "sources/paypal-us-fees.md" sha256:0318950a982c3c7d # PayPal's own published figures
table1 sha256:5e481ef40e570eeb
inputs
amount : money[USDc] range >=1USDc <=100000000USDc
international : bool
# The fee has fractions of a cent in it, and the page does not say which way they settle, so
# the direction here is a placeholder — the thing a person has to decide before this ships.
outputs
fee : money[USDc] round half_up(1USDc)
# The page prints the domestic rate and, separately, what an international transaction adds.
# It does not print the sum, so neither does this: the row for a domestic payment adds
# nothing, and that row carries no citation because `0%` is not a figure the copy shows.
table surcharge
policy unique
| international | -> extra : rate[step 0.01%] |
| false | 0% |
| true | 1.5% | @paypal table1
define fee : money[USDc] = amount × 3.49% + amount × extra + 49USDc @paypal table1
examples
| amount | international | -> fee |
| 10000USDc | false | 398USDc |
| 10000USDc | true | 548USDc |
A rule can also leave places for other files to add to: a value its terms put together, an enum, a
table. A contribution file adds to them — a campaign a discount, a product line a kind of product
and its markup — and pins the shape of what it relies on, so that a change to that shape stops it.
There is no list of what to load: ritsu check and ritsu gen merge a project's contributions
into the rules they reach, check each rule alone and merged, and ask every table that reads an
enum for a row of the value a contribution adds to it.
contribution garden_price(garden_price) v1
description "The markup of garden supplies"
to "../product_price.rule" signature:d5379dca48aa13d2
after "garden_kind.rule" signature:650e995b6929bca3
clause garden_markup -> markup
when item_kind garden
then 15%
$ cd crates/ritsu/tests/projects/product_line
$ ritsu check
ok rules/contrib/garden_kind.rule (contributes to rules/shared/product_kinds.rule)
ok rules/contrib/garden_price.rule (contributes to rules/product_price.rule)
ok rules/product_price.rule (2 contributions: rules/contrib/garden_price.rule, rules/contrib/garden_kind.rule)
ok rules/shared/product_kinds.rule (1 contribution: rules/contrib/garden_kind.rule)
pricing.flow: ok
ritsu check: 5 files (rulec 4, dandori 1): all pass; borders between the languages: 0 checked, 0 undecided
A workflow says what is called, in what order, with which retries and timeouts, and what each
call does to a case outside — an order, a payment, a booking. dandori check shows that on
every path, each case the flow does not hand on is left untouched or finished, never halfway.
The same file becomes Temporal (TypeScript, Python, Go), Step Functions, Argo Workflows, Lambda
durable functions or pydantic-graph, and each is run against dandori's reference interpreter.
workflow arrange_delivery v1
description "Book the delivery of an order with the carrier the urgency rule chose, and answer with its tracking number. Written once for every platform, since its calls are HTTP ones that dandori writes for each (`connection` is what Step Functions needs of them): each version of fulfillment runs it as its child, a child workflow on Temporal, an invoked durable function on Lambda durable functions, a workflow of this WorkflowTemplate on Argo, a nested execution on Step Functions"
enum carrier = standard | next_day
record Booking
tracking_number : string
inputs
order_id : string
carrier : carrier
recipient : string?
extra : json
outputs
tracking_number : string
# The next-day carrier sends a van for the parcels; on a busy day it has no van left.
task book_next_day(order_id: string, recipient: string?) -> Booking
http POST "https://next-day.example.com/v1/pickups"
connection "arn:aws:events:ap-northeast-1:123456789012:connection/next-day/2a3b4c"
errors no_van = 409
key
retry 2 times every 5 seconds
task book_standard(order_id: string, recipient: string?, extra: json) -> Booking
http POST "https://post.example.com/v1/parcels"
connection "arn:aws:events:ap-northeast-1:123456789012:connection/post/3b4c5d"
key
retry 2 times every 5 seconds
flow
match carrier
next_day =>
let booked = book_next_day(order_id: order_id, recipient: recipient)
on no_van => fail NoVan "No next-day van is left for order {order_id}"
succeed tracking_number = booked.tracking_number
standard =>
let booked = book_standard(order_id: order_id, recipient: recipient, extra: extra)
succeed tracking_number = booked.tracking_number
A date is written as it is said in a contract or a law: thirty days after the invoice, moved to
the next business day. koyomi check computes it on every day of the declared range and checks
each claim on every one of them. The calendar comes from a pinned source — here GOV.UK's bank
holidays for England and Wales. It generates TypeScript, Python, Go, Rust and SQL, and a Wasm
module that any host calls with one JSON object.
dates net30 v1
description "Net 30: due 30 days after the invoice date, moved to the next business day in England and Wales when that day is closed. The claims are this example's own, written as they read"
use calendar "calendars/england_and_wales.cal"
inputs
invoice_date : date range >=2026-01-01 <=2028-11-29
date due = invoice_date
+ 30 days # "Net 30": due 30 days after the invoice date
roll following # on the next business day when that day is closed
claims
due_on_a_business_day : due is open
at_least_30_days : due >= invoice_date + 30 days
later_invoice_later_due : due is monotonic
$ cd crates/koyomi/examples
$ koyomi check net30.cal
net30.cal: ok — 3 claims hold on all 1,064 days of invoice_date (2026-01-01..2028-11-29)
A book declares accounts and the bounds each must keep, and the transfers between them. chobo checks that no sequence of transfers, holds and expiries can take an account past a bound, and generates clients in which every transfer is one write that keeps them, on PostgreSQL, on TigerBeetle or on the MySQL family (MySQL, TiDB and Vitess), in TypeScript, Python, Go and Rust.
# A refund never exceeds the sale: what is left to refund on each order is an account of its own.
book refunds v1
description "What is left to refund on each order is an account of its own: the sale adds to it, a refund takes from it, and it never goes below 0. A refund is held while it waits for approval"
unit USD scale 2
account refundable(order: string) : USD
description "what is left to refund on the order"
at least 0 refused as refund_exceeds_sale
account sales : USD outside
account refunded : USD outside
transfer sale(order: string, amount: USD)
key order
move amount from sales to refundable(order)
transfer refund(request: string, order: string, amount: USD)
description "held while it waits for approval: posted when it is approved, voided when it is turned down"
key request
pending expires after 7 days
move amount from refundable(order) to refunded
The code is the agent's; the claims are what a person reads. geas runs each claim against the
program — a command line, an HTTP service, a page in a browser — and, given a diff, tells which
claims it touches. Given an OpenSpec spec, geas scenarios lists the scenarios that no claim of the
same name checks. A claim can wait for an answer that comes later, such as a job a service finishes
after it answers, for as long as the claim says.
# The human-auditable half. The implementation (calc.py) is agent-written;
# these claims are what a reviewer actually reads.
target calc {
run "python3 calc.py"
}
claim "adds two integers" {
when calc.run("2", "+", "3")
then stdout is "5"
and exit is 0
}
claim "multiplies" {
when calc.run("6", "*", "7")
then stdout is "42"
}
claim "refuses division by zero" {
when calc.run("1", "/", "0")
then exit is 1
and stderr contains "division by zero"
}
claim "rejects an unknown operator" {
when calc.run("1", "%", "2")
then exit is 2
and stderr contains "unknown op"
}
Each requirement says where it came from (an article of a law, a document, a decision), who owns it, and what satisfies and verifies it. Every link is pinned by hashes at both ends: when the source, the requirement or the thing that satisfies it changes, the links after it stop until someone looks again. yuen reads laws from the US eCFR and Japan's e-Gov, and exports ReqIF and W3C PROV. It pins the requirements of an OpenSpec spec the same way, and stops when a change rewrites one.
requirements osha v1
description "A requirement read from 29 CFR 1910.157, for the tests. Written as an example; it does not say how the regulation is to be read"
role safety "decides how the regulation reads"
source osha = law ecfr "29 CFR 1910" asof 2026-01-01
"§1910.157" sha256:c2a9ce966c7e2269
requirement extinguisher_distance
text "No employee travels more than 75 feet to a portable fire extinguisher for Class A fires"
owner safety
from @osha "§1910.157"
reviewed 2026-10-03 by safety sha256:c2a9ce966c7e2269 -> sha256:08a4819829372b9b
not satisfied "The test material names no artifact"
approved 2026-10-03 by safety sha256:08a4819829372b9b
not verified "The test material names no claim"
approved 2026-10-03 by safety sha256:08a4819829372b9b
A map says which context owns which files, which language each publishes, and who may depend on
whom, through what. sakai checks the references that cross contexts, in the files of every
language here, in the OpenAPI and AsyncAPI documents services keep as their contracts, and in the
code — handing imports to import-linter, dependency-cruiser, ArchUnit and go-arch-lint, and reading
Rust's crates from Cargo. It does not count a use book, since another context reaches a book through the service in
front of it.
ritsu maps its own crates this way; this is one of its contexts:
context Workflows(dandori) v1
description "Workflows that call the rules, made into Temporal, Step Functions, Argo and more (dandori)"
owns
dir "../crates/dandori"
published language dandori
crate "../crates/dandori"
# A language stands on the base, and reads another language only through the ports.
upstream Base conformist
through ritsu_base, ritsu_units, ritsu_proto, ritsu_emit, ritsu_ports
A gate (a .gate file) says which principal may do which action on which resource, with roles,
attributes and relations, and with the answers of a rule and of dates as conditions.
sekisho check asks rulec which answers the rule can give and koyomi which dates can come
together, walks every combination that can happen, and decides each as Cedar does: an action no
one can do, a permit a forbid covers, an expectation that does not hold, two duties one person is
allowed, all stop the check. Only a gate that passes compiles, into Cedar's schema and policies,
and into TypeScript, Python and Go that compute the conditions from the service's own data and ask
Cedar, so a caller cannot hand them in.
action refund_order
description "Refund an order, in part or in whole"
guards orders refundOrder
principal User, Workflow
resource Order from orderId
input
amount : money[GBP, incl_tax] range >=1GBP <=10_000GBP
context
refund_band = refund_limit(amount: amount, limit: principal.refund_limit).band
in_period = today <= refund_terms.last_day(paid_on: resource.paid_on)
business_day = today is open in uk
…
permit clerks_refund_within_their_limit
description "A clerk refunds up to the clerk's own limit, while the refund period lasts"
principal in clerk
action refund_order
when refund_band is within_limit
when in_period
…
expect deny clerks_never_refund_over_their_limit
description "A clerk who is not a manager never refunds more than the clerk's own limit"
principal in clerk
action refund_order
unless principal in manager
when refund_band is over_limit
$ cd crates/sekisho/examples/refunds
$ sekisho check refunds.gate
refunds.gate: ok — 3 actions, 10 policies (7 permits, 3 forbids), 3 expectations, 1 separation
The page on the site follows this example through: what the check asks the rule, the Cedar and the code it writes, and the page for people.
kaname is a small general-purpose language for the functional core of a program: the part with
no input or output, which takes values and answers values. A module (a .kn file) moves an order
from state to state by events, checks and normalizes what a person typed, compares two counts of a
stockroom in dicts and sets, or reshapes a tree, in ritsu's types: records, sums, lists, dicts,
sets, strings, generics, result and function values. It calls a rule, a date and a book in their
own types too, and puts a rule's inputs together, carries one answer into the next call and hands
an amount to a book. kaname check holds a module to the rules the other languages are held to —
units and money, rounding, int64 — and to the range of every value it passes: a function marked
total answers every input of the ranges it declares, and a decision written in code rather than
in a table is a warning. ritsu gen writes it as TypeScript, Python, Go and Rust for a host to
call, beside the code of the rules, dates and books it calls, and the Rust builds to a Wasm module
that any host calls with one JSON object.
An order moved by events is a functional core of its own. The states are the values of an enum
with fields under them, and the next state, or why an event is refused, is a match on the state
and the event:
enum Order
cart
lines : list[Line] count <=50
placed
lines : list[Line] count >=1 <=50
total : money[USDc]
on : date
…
fn next(order: Order, event: Event) -> result[Order, string]
match order
cart c =>
match event
add a =>
if c.lines.count() < 50
ok(Order.cart { lines: c.lines.append(a.line) })
else
err("a cart holds 50 lines")
…
shipped, cancelled => err("the order is closed")
The check holds each match to every value of the state and of the event, and finds that the
lines of a cart stay within its 50 and that their total stays inside int64, so every function is
total:
$ cd crates/kaname/examples/order_flow
$ ritsu check
ok order_flow.ja.kn: 5 functions, total
ok order_flow.kn: 5 functions, total
ritsu check: 2 files (kaname 2): all pass; borders between the languages: 0 checked, 0 undecided
The host keeps the order and calls the code ritsu gen writes of the module for each event. The
TypeScript host (crates/kaname/examples/order_flow/host/main.ts, beside a Python and a Go one that
print the same) goes through seven events, among them a payment of 650 cents for a total of 700:
for (const event of events) {
const r = next(order, event);
if (r.kind === "err") {
console.log(`${event.kind}: refused: ${r.error}`);
continue;
}
order = r.value;
console.log(`${event.kind}: ${order.kind}`);
}
add: cart
add: cart
place: placed
pay: refused: the amount is not the total
pay: paid
ship: shipped
cancel: refused: the order is closed
Here a module joins the other languages:
module late_payment v1
description "What a supplier claims for an invoice paid late: the day it fell due, how many days late it was paid, and the fixed sum and the interest the rule gives for them"
use dates terms from "dates/payment_terms.cal"
use rule charges from "rules/late_payment_charges.rule"
record Invoice
id : string
invoiced : date range >=2026-01-01 <=2027-12-31
amount : money[GBPc] range >=1GBPc <=100_000_000GBPc
record Claim
due : date
days_late : number range >=1 <=1500
compensation : money[GBP]
interest : money[GBPc]
fn claim(invoice: Invoice, paid_on: date range >=2026-01-01 <=2029-12-31, reference_rate: rate[step 0.01%] range >=0% <=15%) -> Claim? total
description "None when the invoice was paid by the day it fell due"
let due = terms.due(invoiced: invoice.invoiced)
let late = paid_on - due
if late >= 1
let c = charges(debt: invoice.amount, days_late: late, reference_rate: reference_rate)
Claim { due: due, days_late: late, compensation: c.compensation, interest: c.interest }
else
none
koyomi says the day the invoice fell due, a rule what the supplier may claim for the days it was
paid late, and kaname counts the days between: paid_on - due is −758 to 1,431 days, and 1 to
1,431 in the branch of late >= 1, inside the rule's 1 to 1,500. ritsu check reads the three
languages at once:
$ cd crates/kaname/examples/late_payment
$ ritsu check
…
ok late_payment.kn: 1 function, total
ritsu check: 6 files (rulec 2, koyomi 2, kaname 2): all pass; borders between the languages: 2 checked, 0 undecided
The pages on the site go through how a module is written, the four functional cores with their hosts in TypeScript, Python and Go, and what the check guarantees; kaname's README has the rest.
The languages share one toolchain so that what one has checked, another can take as given. Here a refund rule takes for granted that nobody asks for more than they paid:
rule refund_check v1
description "Whether a refund is paid at once or reviewed. A refund never asks for more than was paid, which the rule takes for granted"
enum path = at_once | review
inputs
paid : number range >=0 <=10000
asked : number range >=0 <=10000
constraint asked <= paid
outputs
route : path
table pick
policy unique
| asked | -> route : path |
| <=100 | at_once |
| >100 | review |
A workflow calls it with whatever the customer asked for:
workflow refund v1
description "Pays a refund back at once or sends it to review, as the rule decides"
use rule check from "refund_check.rule"
lambda "arn:aws:lambda:us-east-1:123456789012:function:refund-check"
inputs
order : string
paid : int range >=0 <=10000
task ask_amount(order: string) -> int range >=0 <=10000
lambda "arn:aws:lambda:us-east-1:123456789012:function:ask-amount"
idempotent
task pay_back(order: string, amount: int range >=0 <=10000)
lambda "arn:aws:lambda:us-east-1:123456789012:function:pay-back"
key
flow
let asked = ask_amount(order: order)
let decision = check(paid: paid, asked: asked)
match decision.route
at_once => pay_back(order: order, amount: asked)
review => pass
Each file passes its own language's check. Together they do not (the two files are from the
reproduction that ritsu explain E201 prints, which calls the rule from a module of kaname too):
$ ritsu check .
ok refund_check.rule
refund.flow: ok
error[ritsu E201]: refund.flow:21:1: The call of the rule check can give it values that break its precondition `asked <= paid`
21 | let decision = check(paid: paid, asked: asked)
= `asked` is `>=0 <=10000` and `paid` is `>=0 <=10000`, and at asked = 10000, paid = 0 `asked <= paid` does not hold
= The rule's generated code refuses a call that breaks a precondition at its door, so this call fails only when the workflow runs. The ranges are dandori's, gathered from every place the values come from.
= Branch so that the precondition holds before the call, or narrow the ranges (the `range` of an input or a task's result).
ritsu check: 2 files (rulec 1, dandori 1): 1 fail (1 error); borders between the languages: 1 checked, 0 undecided
Every check that crosses languages comes to one of three answers: it holds, here is a case where it does not, or it cannot be decided — and then it says why. Nothing passes in silence. Among them:
range from koyomi), and
against the range the rule declares;A module is a directory with a ritsu.mod, which gives its files a name and a version. Other
projects require it, from a directory beside them, a git repository or an OCI registry, and read its
files by @<name>/… paths. A project checks the files it requires with its own, so a proof does
not stop at a module's edge either. Here, in ritsu's tests
(crates/ritsu/tests/projects/modules), a shop requires
pricing, which requires tax:
module example.com/acme/shop
version 0.1.0
description "The order flow and the checkout of the Acme shop"
require pricing path "../pricing"
The shop's workflow calls the rules of pricing without copying them, by the name the shop's
ritsu.mod gives the dependency:
workflow place_order v1
…
use rule shipping from "@pricing/rules/shipping_fee.rule"
use rule total from "@pricing/rules/order_total.rule"
ritsu check reads every module the shop requires and checks their files with the shop's: a weight
the workflow passes to shipping is held to the range of the rule's input as it would be in one
project.
$ cd crates/ritsu/tests/projects/modules/shop
$ ritsu check .
ok checkout/basket.kn: 2 functions, total
orders/place_order.flow: ok
dependencies: 2 modules, 3 files (rulec 3): all pass
ritsu check: 2 files (kaname 1, dandori 1): all pass; borders between the languages: 0 checked, 0 undecided
A module on a registry is required with ritsu mod, which writes ritsu.mod and ritsu.lock:
$ ritsu mod add holidays ghcr.io/i2y/[email protected]
$ ritsu mod fetch
A module in a git repository is required by the repository's URL. Its versions are the tags
v<version>, or <dir>/v<version> for a module in a directory of the repository, given with
--dir <dir>:
$ ritsu mod add rates https://example.com/acme/[email protected]
ritsu mod lock --check fails a build whose
lock is stale. ritsu mod get <name>@<version> raises a dependency.ritsu mod fetch fetches what the lock pins into a store kept across runs. ritsu mod vendor
puts a copy of it in the module (.ritsu/modules/), for a build that cannot reach the registry or
the repository. Only ritsu mod reaches the network: check, gen, run and the languages'
commands read the store and the copy.signature line that covers a module from git is E309). A
module for others to use is published to a registry, where its signature can be checked. The name
of a module from git is where it is (example.com/acme/rates), so one module is shared one of the
two ways. Moving it to a registry changes its module line, and the projects that use it change
their require line and lock; their files name it by the name they gave it, and do not change.ritsu mod push checks the module, refuses it when a file holds a key (W901) or it requires a
module from git, and pushes it to the registry its name names. It prints the cosign sign command
that signs it. With a signature line in ritsu.mod, a fetch has cosign check the signature
before it keeps the module:signature "ghcr.io/i2y/ritsu-holidays" by matching "^https://github[.]com/i2y/ritsu/" issuer "https://token.actions.githubusercontent.com"
The credentials for a registry come from Docker's configuration file (what docker login and the
credential helpers write), and no output shows them. Those for a git repository are git's own: ssh
keys, credential helpers and url.<base>.insteadOf work as they do for git fetch, and ritsu does
not let git ask for a password on the terminal.
ghcr.io/i2y/ritsu-holidays is a module of this repository,
modules/holidays: the public holidays of the United States, the United
Kingdom and Japan, as calendars of koyomi, with the data pinned to what their governments publish.
Its calendars close the holidays and nothing else. A calendar of your own reads one with
use calendar "@holidays/uk/england_and_wales.cal" and closes your weekends itself, with
closed weekly sat, sun. The other modules under modules/ hold rules transcribed from the
laws of those countries: limits on workplace safety, income tax, stamp duty, social insurance, the
minimum wage and the charges on a late payment.
The list of modules searches them, and for each version
shows what it holds, lets you try its rules and calendars on the page, and says what changed from the
version before.
ritsu mod site writes the same pages for a list of your own, and
the guide to modules has the workflow that publishes them to GitHub Pages.
ritsu mod diff compares two versions of a module: the files added and removed, what each changed
file shows the code that uses it, and, for rules, dates and gates, every input that gets a different
answer, over the whole input space rather than a sample. ritsu mod report answers what a pull
request that raises a dependency asks. It checks the project against the lock of the base and the
lock of the head, and says, for each module that changed, its diff, which answers of your own files
change, and which lines of your flows and kaname modules read an answer that changes. In GitHub
Actions it posts that on the pull request:
# .github/workflows/ritsu-mod-report.yml
name: ritsu mod report
on:
pull_request:
paths: [ritsu.mod, ritsu.lock]
permissions:
contents: read
pull-requests: write
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: i2y/[email protected]
- name: Report what raising the dependencies changes
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
run: |
code=0
ritsu mod report --base "origin/$GITHUB_BASE_REF" --format markdown > report.md || code=$?
ritsu mod report --base "origin/$GITHUB_BASE_REF" --format json > report.json || true
if [ -s report.md ]; then gh pr comment "$PR" --body-file report.md; fi
exit "$code"
- uses: actions/upload-artifact@v7
if: always()
with:
name: ritsu-mod-report
path: report.json
fetch-depth: 0 fetches the history, so that origin/<base> can be read.ritsu mod report fetches the modules both locks pin. It exits 1 when the head has errors, and 2
when it cannot compare a module that changed. Add --fail-on-change to fail when answers change
too.paths and to
--root.--lang ja writes the report in Japanese.The npm package reads a module's dependencies from the store and from the copy ritsu mod vendor
puts in the module. The commands that reach a registry or run git or cosign (add, get, fetch,
versions, push, report and site, and lock, remove and diff when they have to fetch)
need the native ritsu. When dirs opens only some directories, add the store to dirs, or put the
copy in the module with ritsu mod vendor.
ritsu mod --help lists the fifteen commands of ritsu mod, and
Modules on ritsu's site says how to write, require, share by
git, push and sign a module.
The languages also read their files for what leaves a secret or a service exposed: a key written into a file, a connection that is not encrypted, an operation with no authentication, a value a contract marks secret that a workflow keeps in its history or sends away. None of it uses the network or runs anything, and none of it looks dependencies up in a vulnerability database: it reads what the project says.
| Code | What it finds | Where it shows |
|---|---|---|
| W901 | a key in the shape its provider gives it (AWS, GitHub, Slack, Stripe, OpenAI, Anthropic, Google, a PEM private key) written in a file, shown by its kind, prefix and length, never the key | each language's check, in its own files; ritsu check, in the project's .proto files and the documents a .flow, a .rule or a map's published language points at |
| W902 | a connection that is not encrypted, to a host that is not this machine | dandori check, for the URLs a task calls; sakai check, for the servers of a map's OpenAPI and AsyncAPI documents |
| W903 | an operation or a channel of a published language whose document has no security | sakai check |
| W904 | a secret the platform keeps in the history of a run: an input, an output, an argument, an answer | dandori check |
| E906 | a secret sent outside the project: to a model's provider, the API of a decision model (Jev, OpenAI's Decisions API, a server that is not this machine), a host named by its URL alone, an AWS service | dandori check |
| E905, W905 | a secret sent to a file outside the map, or to a context the map does not relate to the one that marked it; or, with a map that does not pass sakai's check, where it goes cannot be decided | ritsu check |
| W910 | an input of a gate's action that a policy reads and that the contract of the operation it guards marks secret: the value goes into the request Cedar is asked, and stays in the record of the decision | sekisho check |
| E907, W907 | an operation a context opens (open host service) that no action of a gate guards; a context none of whose operations is guarded yet | ritsu check, in a project with a gate |
| E908, W908, W909 | a call of a workflow its gate allows in no combination; an action the workflow is allowed and never calls; a call that can be denied, or whether it is allowed cannot be decided, with no error declared for a denial | ritsu check |
A value is secret where a contract marks it (debug_redact in a .proto; x-data-classification,
x-sensitive-data or format: password in an OpenAPI schema) or where a .flow writes secret
after its type, and dandori follows it through every variable it goes into. Here a variant of the
example crates/dandori/examples/payout, from dandori's tests, has the model that drafts the
seller's notice read the name the bank account is held in:
$ cd crates/dandori/tests/fixtures/security
$ dandori check E906_payout_holder.flow
…
error[E906]: E906_payout_holder.flow:41:1: the task `draft_notice` sends the secret `account.holder` to OpenAI, outside the project
41 | let notice = draft_notice(amount: amount, payout_id: paid.payoutId, holder: account.holder)
= The mark is `debug_redact = true` at ../../../examples/payout/specs/payout.proto:35.
= Send a reference or only what the other side needs. If sending it there is intended, write `discloses holder "<why>"` under the task.
…
What is meant is written beside what it is about, and the check takes it at its word:
ritsu: test secret in a comment on the line of a key for tests, plaintext "<why>" where a
.flow writes the URL, x-ritsu-plaintext in a server of a document, security: [] on an
operation open to anyone, history encrypted under a workflow whose history is encrypted with a
key you hold (the Temporal code dandori generates for it then requires a payload codec), and
discloses <parameter> "<why>" under a task that is meant to send what it is given.
The same files give pages, in English or Japanese, for the people who need to understand what the code is to carry out — those who run the business, finance and legal, operators, the developers reviewing the code — so they can read what was written and check it against what they know. A team that signs off on its rules can sign off on these pages too.
rulec doc — a rule's tables, provisos and what was proved of them, in Markdown or HTML;dandori doc — a workflow drawn with every scenario it runs;koyomi doc — a calendar, month by month;chobo doc — a ledger, its bounds and its transfers;sekisho doc — who may do what, action by action, beside the Cedar it compiles to;kaname doc — a module: the decisions in its tables, its functions by their effects and where
each can fail, and the decisions written in its code;yuen trace — a requirement back to its source and on to what satisfies it;explain <code> — every diagnostic, with a reproduction you can run.rulec's certificates are rechecked in Lean 4. Lean models of chobo, koyomi, dandori's core,
kaname's evaluation and its analysis of ranges, and ritsu's checks across languages sit beside the
Rust, and the two are run on the same generated inputs — millions of lines — and compared answer by
answer. For kaname, Lean also proves the analysis of ranges sound, and that a function it infers
total fails on no input in the ranges it declares. No proof rests on sorry or an axiom of its
own.
ritsu and its languages are made to be used by AI agents, and skills/ holds ten
Agent Skills for them: skills/ritsu for a project of more
than one language (the loop from ritsu check to a run and a package, how to read each diagnostic
across the languages and fix it, and which language's skill to read for the rest), and one for each
of the nine languages, in skills/<language>/. skills/README.md lists them.
There are four ways to install them:
ritsu holds the
ten. Run /plugin marketplace add https://i2y.github.io/ritsu/marketplace.json, then
/plugin install ritsu@ritsu; Claude Code fetches the folder skills/ alone, not the whole
repository.ritsu skills install writes them into the project's
.claude/skills/; with --user, into ~/.claude/skills/; with --dir <dir>, where another
agent reads skills. Names after it (ritsu skills install rulec dandori) write only those, and
ritsu skills list lists them.skills/ into ~/.claude/skills/, or into a
project's .claude/skills/.ritsu-skills-v<version>.zip holds the ten folders; unzip it where your
agent reads skills.ritsu check <dir> every language's check, then the checks across them
ritsu run <flow> … a workflow run with its rules evaluated, dates computed, books moved
ritsu gen --target … one package of the whole project per language: TypeScript, Python, Go, Rust
ritsu gen --check … whether the packages are current; with --format json, file by file
ritsu gen --library … a module as libraries for npm, PyPI and Go
ritsu mod <command> … modules: add, lock, fetch, push, diff, report and the rest
ritsu explain <code> what a diagnostic means, with a reproduction
ritsu skills install the Agent Skills, written where an agent reads them
ritsu <language> … a language's own commands, as in ritsu rulec doc fee.rule
A release archive holds ritsu and nine links to it — rulec, dandori, koyomi, chobo,
geas, yuen, sakai, sekisho, kaname — and each runs as its language. cargo install installs the ritsu
binary alone: call a language as ritsu <language> …, or make the link yourself, named for the
language (ln -s "$(command -v ritsu)" ~/.cargo/bin/rulec). Add --lang ja for Japanese.
Publishing libraries on ritsu's site says what
ritsu gen --library writes and how to publish it, and Modules above says what
ritsu mod does.
From source, with a recent stable Rust:
cargo install --git https://github.com/i2y/ritsu --locked ritsu
Only one language — rulec, say, which needs no other:
cargo install --git https://github.com/i2y/ritsu --locked rulec
Every release, from 0.23.0 on (continuing rulec's numbering), carries archives for macOS and
Linux and .deb and .rpm packages, on the releases page.
With Homebrew it is brew install i2y/tap/ritsu, and in GitHub Actions uses: i2y/[email protected].
From 0.26.0, a release also carries i2y-ritsu-<version>.tgz, the npm package @i2y/ritsu: ritsu
built for WebAssembly, which Node 22 or later runs with no native binary, with the native binary's
commands and an API to call them from JavaScript. It is not on the npm registry; install it from
the release:
npm install --save-dev https://github.com/i2y/ritsu/releases/download/v0.43.0/i2y-ritsu-0.43.0.tgz
Its README and ritsu in Node say how to use it, and which commands, starting another program or reaching the network, need the native binary.
crates/<language> — each language, with its own README and design notes:
rulec, dandori,
koyomi, chobo,
geas, yuen,
sakai, sekisho,
kanamecrates/ritsu-* — the shared base, units, ports, project reading, the checks across
languages, the .proto reader, the emitters, the browser buildmodules/holidays — a module of the public holidays of the United Kingdom and of Japan, for other
projects to requireproofs/ — the Lean modelswebsite/ — the site: ritsu's pages and the playground, with rulec's site in website/rulec, dandori's in website/dandori and chobo's in website/choboDESIGN.md, PLAN.md — the design of the whole (in Japanese)SECURITY.md — how to report a vulnerabilityMIT OR Apache-2.0 (LICENSE-MIT, LICENSE-APACHE). Two of the
languages, and ritsu-emit, the crate their generators share, hold data from others: rulec the
names of its built-in divisions, from Unicode CLDR (Unicode-3.0), ritsu-emit tables of the
characters a name of Rust, Python, TypeScript or Go cannot hold and of NFC and NFKC, made from the
Unicode Character Database (Unicode-3.0), which chobo and kaname read, and koyomi a Shift_JIS table made from the
WHATWG Encoding Standard (BSD-3-Clause). So the ritsu binary is under
(MIT OR Apache-2.0) AND Unicode-3.0 AND BSD-3-Clause. It also holds eight crates from
crates.io, under their own licenses, and, for the examples of explain, copies of an article of
Japan's Civil Code from e-Gov (PDL1.0) and of two sections of the US Code of Federal Regulations.
THIRD_PARTY_NOTICES gives where each comes from and the text of its
license or its terms, and every archive and package of a release carries it.