ritsu in Node
From 0.26.0, every release of ritsu also carries an npm package, @i2y/ritsu: the same ritsu,
built for WebAssembly (WASI), which Node runs. Its commands print what the native binary prints
(all but the few that start another program or reach the network, below),
and a program can call them from JavaScript. It needs no native binary, has no dependencies
and runs nothing when it is installed, so it fits where a native binary is a nuisance or is not
allowed: the CI of a JavaScript or TypeScript project, a Node service that checks the repositories
it knows about, or a job that installs a package into a clone of each repository with
npm install --ignore-scripts, runs its command there with no network, and opens a pull request
when the command changed something.
Install
The package is not on the npm registry; a release carries it as i2y-ritsu-<version>.tgz, with its
SHA-256 in the release's SHA256SUMS, on the
releases page. Install it from there:
$ npm install --save-dev https://github.com/i2y/ritsu/releases/download/v0.31.0/i2y-ritsu-0.31.0.tgz
Or download the file, check it against SHA256SUMS, and install the file itself
(npm install --save-dev ./i2y-ritsu-0.31.0.tgz), which works with --ignore-scripts and with no
network (--offline). It needs Node 22 or later, on Linux or macOS; 22, 24 and 26 are tested.
On Node 22, loading the package turns off V8's fast API calls for the whole process
(v8.setFlagsFromString("--no-turbo-fast-api-calls")): Node 22's WASI can crash the process when
it is called that way, and Node 23 and later do not. The flag makes those calls slower, not
different, but a program that loads the package on Node 22 runs under it too. On Node 23 and later
nothing is set.
The commands
The package puts nine commands into node_modules/.bin: ritsu, and rulec, dandori, koyomi,
chobo, geas, yuen, sakai and sekisho, each the same as ritsu <language> …, as the links
of a release's archive are. In a project with a rule, a calendar, a dates file, a ledger and a
workflow:
$ npx ritsu check
ok rules/delivery.rule
…
ritsu check: 5 files (rulec 1, koyomi 2, chobo 1, dandori 1): all pass (4 warnings); borders between the languages: 4 checked, 4 undecided
$ npx rulec check rules/delivery.rule
ok rules/delivery.rule
$ npx ritsu gen --target typescript
generated: generated/typescript/rules/delivery.ts
generated: generated/typescript/dates/weekdays.ts
…
The exit codes are the native command's: 0 nothing wrong (warnings may be printed), 1 an error, 2
bad arguments or a file that cannot be read. The standard input, output and error are the process's
own: a command reads a pipe, as git diff | npx geas affected spec.geas - does, and prints as it
goes.
What ritsu gen --format json says
ritsu gen --check writes nothing and says which files of the packages are missing, stale or
changed by hand, or are left from what is generated no more. With --format json it says the same
to a program, which can keep, for each repository, whether its generated code is current:
$ echo '// changed by hand' >> generated/typescript/index.ts
$ npx ritsu gen --target typescript --check --format json
{
…
"ok": false,
"check": true,
"out": "generated",
"targets": [
"typescript"
],
"files": [
{
"target": "typescript",
"path": "generated/typescript/rules/delivery.ts",
"from": "rules/delivery.rule",
"state": "same"
},
…
{
"target": "typescript",
"path": "generated/typescript/index.ts",
"from": null,
"state": "stale"
},
…
],
"diagnostics": [],
"error": null
}
The first three keys are those of ritsu check --format json: the version, the root, and whether all
is well (the exit code is 0). Then whether it was --check, --out and the languages asked for;
files, each file with the language of its package (or cedar), its path as the text prints it,
the file of the project it is made from (null for what the package itself is made of), and its
state — written, same or removed when ritsu writes, and same, missing, stale or left
under --check. When nothing is generated, files is empty, error says why with the exit code,
and diagnostics holds the diagnostics of the files that do not pass their checks, each as
ritsu check --format json writes one. The same JSON comes without the package, from the native
binary.
From JavaScript
run(args, options?) runs ritsu <args> in a worker thread and returns a promise of
{ code, stdout, stderr }; the event loop goes on while it runs, and any number of runs may go at
once. runSync(args, options?) runs it on the thread that calls it. args leaves out the
program's name (["check", ".", "--format", "json"]; a language's command is
["rulec", "doc", "fee.rule"]). options.cwd is the directory the command works in (by default
process.cwd()), options.env its environment (by default process.env; given, it replaces it),
and options.stdin what it reads from its standard input (by default nothing). options.dirs
narrows what the module can open to those directories, each at its own path (a relative one from
options.cwd; by default the whole filesystem, as the native binary has it), and options.cwd has
to be in one of them: a service that checks a repository it was handed can give it that repository
alone, and a path in it that leads out of it, by its name (import proto "../../other.proto") or
through a symbolic link (to a file, or to a directory on the way), can then be neither read nor
written; the command says it cannot read it, and nothing of what is outside comes into a diagnostic.
It narrows what ritsu, which you trust, reads; it does not make Node's WASI a sandbox for code you
do not trust, which Node's documentation says not to rely on it for. version is the version of
ritsu the module was built from, and wasmPath the module's path. In the same project:
import { run } from "@i2y/ritsu";
const { code, stdout } = await run(["gen", "--target", "typescript", "--check", "--format", "json"]);
const report = JSON.parse(stdout);
console.log(code, report.ok, report.files.filter((f) => f.state !== "same").map((f) => `${f.state} ${f.path}`));
From CommonJS, require("@i2y/ritsu") gives the same, on Node 22.12 and later.
What does not run in Node
A WebAssembly module under WASI can neither start another program nor open a connection. The
commands that need either stop with a message that says so and says to run them with the native
ritsu (a release's archive, Homebrew, the .deb or the .rpm); each exits with the code it gives
when the program it needs is missing:
| Command | What it needs |
|---|---|
rulec source fetch, rulec source outdated, and the same of koyomi and yuen |
the network (e-Gov, the eCFR, GOV.UK, a URL), through curl |
rulec test |
the toolchains the generated code is run with |
rulec verify --adapter …, rulec source fetch --via … |
the program it is given |
rulec mcp |
rulec, started for each call of a tool |
rulec check --diff-base, rulec diff and rulec replay of <file>@<revision>, chobo check --diff-base |
git |
geas check, geas snap, geas drift, geas map |
the programs the claims run |
sakai check (and ritsu check) of a map with code rust |
cargo metadata; sakai says E107 and checks the rest |
Everything else runs as the native binary does: every language's check, ritsu check across the
languages, ritsu gen and ritsu run, every doc, api, vectors, export, trace, affected,
scenarios and explain, and ritsu skills install. Each release holds the package to the native
binary on the examples of the eight languages, ritsu's test projects and the projects of the
playground: the same output, the same exit code and the same files written, for
every command a person runs there, in English and in Japanese.
On Windows the package stops at once, with a message: the working directory and the paths cannot be handed to the WebAssembly module as they are. Use the native ritsu, or WSL.
License
(MIT OR Apache-2.0) AND Unicode-3.0 AND BSD-3-Clause, as the native binary. Besides what the
binary holds from others, the module holds parts of wasi-libc, the C library of WASI;
THIRD_PARTY_NOTICES, which the
package carries too, says where each comes from and gives the text of its license.