With the other languages
A book stands on its own, and chobo reads no other language. In a project of ritsu's, the book is
also where other files meet: a dandori workflow runs its operations as tasks and follows its holds
as cases, a rulec rule works out the amounts the workflow (or code written by hand) hands it, a
sekisho gate says who may run
each operation, a yuen requirement names the operations that satisfy it, and a sakai map says which
context keeps the book. ritsu check reads them together, and checks where they meet.
A workflow runs the operations
dandori's invoice example holds an order's goods until its payment is due, ships them once paid, and puts them back when not (crates/dandori/examples/invoice/invoice.flow). Its stock is a book like the inventory example, whose holds last 60 days:
use book stock from "books/stock.book"
…
task reserve(order: string, sku: string, qty: int) -> stock.reserve
book stock.reserve.hold
starts stock.reserve
errors out_of_stock
task ship(order: string, sku: string) -> stock.reserve
book stock.reserve.post
sends post
errors expired
task put_back(order: string, sku: string) -> stock.reserve
book stock.reserve.void
sends void
errors expired, already_posted
…
case hold : stock.reserve follows stock.reserve
flow
hold <- reserve(order: order.id, sku: order.sku, qty: order.quantity)
on out_of_stock => succeed outcome = out_of_stock, due = none
book <book>.<transfer>.<operation>makes a task an operation of the book:do, orhold,postandvoid. Its parameters are the transfer's, by name, and an amount is the book's unit as Money and units says.- The reasons the book can refuse the operation with are the task's errors, as they are:
errors out_of_stock. A reason the book cannot give for that operation is an error of the workflow's checker, and a refusal the task does not declare fails the call. - An operation happens once for its key, so the task takes no idempotency key: done again, it answers that it was done before, which the workflow reads as done, and a retry is safe.
case hold : stock.reserve follows stock.reservefollows a hold through the state machine of its life, whichchobo apigives too. A hold can expire by itself, so the checker counts that it may have expired before every post and void, and a workflow that can end with the goods still held is an error.
dandori reads the book through ritsu's port of books, as it reads a rule through the port of rules:
ritsu dandori and ritsu check hand it what chobo knows of the book, and the code dandori writes
calls the client chobo build writes, on every platform it builds for. dandori's
Dates and books says how.
A rule and the book, without a workflow
A book is not always called from a workflow. A checkout works the amounts out and records the
sale in the one request it serves. ritsu gen writes the rule's module and the book's client into
one package, and there an amount of money or of a quantity is of the package's one type of its
unit: the type the rule returns is the type the book's operation takes. In ritsu's tests, the
marketplace splits a sale with a rule, in cents, and its checkout, written by hand, gives the book
what the rule returns
(crates/ritsu/tests/projects/marketplace):
import { books, rules, units } from "../generated/typescript/index";
…
export async function checkout(book: books.marketplace.Book, order: string, shop: string, plan: rules.sale_split.Plan, paid: units.USDc): Promise<books.marketplace.Result> {
const split = rules.sale_split.sale_split(plan, paid);
return book.sale.do({ order, shop, to_shop: split.to_shop, fee: split.fee });
}
The rule's fee and to_shop are units.USDc, and so are the amounts the book's sale takes
(unit USD scale 2). What lies between the two is code of no language of ritsu's, so ritsu
check does not see this border; the type checker of the code's language holds it instead. Were
the rule to count in whole dollars (money[USD]), tsc would stop the checkout where the dollars
meet the book's cents:
service/checkout.ts(10,51): error TS2345: Argument of type 'USDc' is not assignable to parameter of type 'USD'.
Type 'USDc' is not assignable to type '{ readonly __rulec: "USD"; }'.
Types of property '__rulec' are incompatible.
Type '"USDc"' is not assignable to type '"USD"'.
service/checkout.ts(11,38): error TS2322: Type 'USD' is not assignable to type 'USDc'.
Type 'USD' is not assignable to type '{ readonly __rulec: "USDc"; }'.
Types of property '__rulec' are incompatible.
Type '"USD"' is not assignable to type '"USDc"'.
service/checkout.ts(11,62): error TS2322: Type 'USD' is not assignable to type 'USDc'.
Type 'USD' is not assignable to type '{ readonly __rulec: "USDc"; }'.
Types of property '__rulec' are incompatible.
Type '"USD"' is not assignable to type '"USDc"'.
mypy stops the Python checkout the same way (Argument "to_shop" to "do" of "_saleTransfer" has
incompatible type "USD"; expected "USDc"), and go vet the Go one. An amount of a count (pcs,
seats) is a plain integer, as it is everywhere, and so is a balance the client reads. In a
workflow, ritsu check holds the same amounts itself, and what tsc cannot see besides: the range
of the amounts and the refusals they can come to, below.
Where they meet, checked
Each check that crosses a language holds, finds a case where it does not, or says that it cannot be
decided, and why. Three of them are about a book: the amounts a rule hands a transfer, the reasons
the transfer can be refused with for those amounts, and a hold against the wait before it is posted.
The files of each example below are the project ritsu explain <code> prints.
A refusal the task does not handle. A rule works out the seats an event needs, up to 400 for a concert, and the workflow gives them to a hall whose account of seats given is at most 300. chobo's search, with the amounts held to what the rule can return, finds that the transfer can be refused:
error[ritsu E204]: booking.flow:18:1: The book can refuse `assign.do` with `over_capacity`, which the task `give_seats` does not handle
18 | give_seats(event: event, count: need.needed)
= chobo's search, with the amounts held to 30 to 400 (the range of the amounts the call gives), finds runs in which `assign.do` is refused with `over_capacity`
= Declare it as an error of the task and handle it (`errors over_capacity` and `on over_capacity =>`).
An amount chobo does not take. In another version of the rule, a handback gives 20 seats back, as −20. chobo takes an amount from 0 to 2⁶³ − 1, and a call with another fails rather than being refused:
error[ritsu E203]: booking.flow:18:1: The task `give_seats` can give `count` the amount -20, which chobo does not take
18 | give_seats(event: event, count: need.needed)
= `need.needed` is the output `needed` of the rule "seats.rule", which comes to -20 at event_kind = handback
= chobo takes an amount from 0 to 9223372036854775807 in the unit's smallest step, and a call with any other fails rather than being refused.
= Make the rule's amounts 0 or more (a negative one is a transfer the other way), or branch on `kind` with `match` before the transfer and give the transfer the output only in the arms without `handback`.
The workflow can keep the rule as it is: ritsu check counts the rule's output over what the runs
that reach the transfer can give the call, so a match kind before the transfer that gives the
seats only in the arms of a workshop and a talk passes.
A hold that has always expired. A workflow holds goods for 14 days and posts the hold after it waits for a payment day that koyomi counts, 18 days or more later:
error[ritsu E206]: invoice.flow:31:1: The hold of the case `goods` has always expired when `ship` posts it
31 | goods <- ship(order: order, sku: sku)
= `ship` comes 17 days 9 hours or more after the hold is made (line 27), and the hold expires 14 days after it is made (`reserve` of "stock.book")
= the wait until `due.at` (line 30) lasts until the time of koyomi's date `payment`: 18 days after `received` at the fewest (from 2026-02-20 to 2026-03-10), at 09:00, and the `now` given to the date was read after the hold (line 29)
= The book refuses it with `expired` on every run, so what follows `ship` going through never runs.
= Make the hold last longer (the transfer's `pending expires after`), or make the call sooner.
When the rule's amounts all fit the hall, no run comes to over_capacity, and a task that declares
it anyway gets a warning rather than a pass: chobo's search goes only as deep as its check does, so
all it can say is that the refusal does not come within that depth.
A hold, and an answer waited for until a time. In another version, the workflow asks for an
approval instead of waiting for the payment day, waits for the answer until 09:00 of that day
(timeout until due.at), and puts the goods back when none comes. How long that takes depends on
the day the date is given, so whether the hold has expired cannot be decided, and the warning says
the day that makes the wait the longest. Every call between the hold and put_back has an upper
bound: the approval's id is handed on within a minute and its wait ends at the date's time, and a call
of a rule or of a date takes 33 seconds at the most on every platform.
warning[ritsu W206]: invoice.flow:45:1: Whether the hold of the case `goods` has expired when `put_back` voids it cannot be decided
45 | goods <- put_back(order: order, sku: sku)
= `put_back` comes 0 seconds to 51 days 9 hours 1 minute 1 second after the hold is made (line 40), and the hold expires 14 days after it is made
= The longest is when the `now` given to `payment` (line 42) falls on 2026-07-21: `approval` (line 43) waits until `due.at`, 2026-09-10 at 09:00, 51 days after it
= The book refuses a call on an expired hold with `expired`, which the flow handles here.
A run with the book in it
ritsu run runs a workflow with its rules computed by rulec, its dates by koyomi and its book by
chobo, and every other task answered by a scenario. It shows the book's balances and holds at the
end, as chobo run does (crates/ritsu/tests/projects/invoice):
invoice.flow (scenario scenarios/invoice_paid.json, from 2026-04-15T10:00:00Z)
before the run, book stock:
receive.do(delivery: "D-1", sku: "pen", qty: 10) done
the run:
reserve(order: "A-1", sku: "pen", qty: 3) chobo done
method(member: true, amount: 120) rulec {"billing":"invoice"}
terms.payment(received: "2026-04-15T10:00:00Z") koyomi {"day":"2026-05-08","at":"2026-05-08T09:00:00Z"}
wait until 2026-05-08T09:00:00Z 22 days 23:00:00, to 2026-05-08T09:00:00Z
check_payment(order: "A-1", due: "2026-05-08") scenario {"paid":true}
ship(order: "A-1", sku: "pen") chobo done
end: succeed {"outcome":"shipped","due":"2026-05-08"}
book stock (books/stock.book) at the end:
accounts:
shelf(pen) posted 7, held out 0, held in 0
suppliers posted -10, held out 0, held in 0
customers posted 3, held out 0, held in 0
holds:
reserve(A-1, pen) posted
A gate guards each operation
In the small shop of ritsu's playground, the warehouse's gate says who may move its stock
(website/playground/shop). Its
actions guard the book's operations, <transfer>.<operation>:
use book inventory from "../warehouse/inventory.book"
…
action hold_stock
description "Hold the stock an order takes, for 30 minutes at most"
guards inventory reserve.hold
principal Staff, System
resource Stock from sku
…
permit the_system_keeps_the_holds
description "The warehouse's system holds the stock an order takes, posts the hold as the order ships, and voids it when the order is cancelled"
principal is System
action hold_stock, post_hold, void_hold
expect deny holds_are_not_moved_by_hand
description "No member of the staff holds, posts or voids the stock of an order by hand: the holds go with the orders"
principal is Staff
action hold_stock, post_hold, void_hold
sekisho check walks every combination of principal, action and resource, and decides each as
Cedar does; an operation the book does not have, or one two actions guard, is an error. The service
that keeps the book asks the gate before it moves the book, through the Cedar and the code
sekisho gen writes. A workflow's own calls of the book are not asked of a gate: chobo refuses for
reasons of the book, none of which stands for a denial.
A requirement it satisfies
The shop's requirements say where the warehouse's decisions come from, and name what meets them, down to one operation of one transfer:
requirement holds_go_with_the_orders
text "The stock an order takes is held for it by the warehouse's system, which posts the hold when the order ships and voids it when the order is cancelled; no one moves a hold by hand"
…
satisfied by chobo "../warehouse/inventory.book" transfer reserve operation hold
reviewed 2026-10-07 by warehouse_lead sha256:3bb72bea70d71547 -> sha256:ef505e9c8dbb3bf1
The hash after the arrow is of the definition chobo hands over for the operation: the transfer as
chobo api describes it, in a fixed form, with the operation and the names of the reasons it can be
refused with. A
change to the transfer's moves, key or hold, or to what it can be refused with, makes yuen say that
the link is no longer what was reviewed.
The map of contexts
The shop's map gives the warehouse's context the directory that holds the book
(owns dir "../warehouse"), and sakai checks the references that cross from one context into
another, in the files of every language. A use book and a transfer task are not among them: the
host's client of the book applies a transfer, and another context uses a book through the service
in front of it.
The shop opens in the browser, with the book among its files: the shop's book in the playground.