One rule, written once. Four real tools, rebuilt live.
The card below is not a mock-up. It is a very small Effortless project running in this page: one rulebook with one rule, Leases.LateFee. Move a slider and the rule changes, the rulebook gets a new hash, and the real effortless tools rebuild the Postgres function, the TypeScript, the Excel formula and the English sentence from it. Change a lease and nothing leaves your browser: the generated TypeScript recomputes that one row.
Drop your rulebook here
The longer version, with the whole loop from a rulebook to a running project. A film made from this widget is coming; until then, this one.
What moves, and what does not
Each slider points at one number in the formula: “Grace period” is number #1, “Late fee rate” is #2. On release the number is rewritten, the schema hashes differently, and every tool line goes from rebuilding… to the tool’s real output as each one finishes. rulebook-to-typescript runs first, because what it generates is what computes the fees.
The rulebook hash covers names, types, formulas and relationships, never a data row. So −/+ on a lease leaves the hash alone, calls no server, and the generated TypeScript recomputes that one lease in a Web Worker. The box stays green.
Hover a line: the rulebook hash it was built from, the tool and its version, and the package hash of everything the tool produced. Nothing on the card is typed in by hand; what you read is what the tool wrote.
Export to Excel, change the formula in the LateFee column, drop the workbook on the card. xlsx-to-rulebook patches the rulebook with Excel’s spelling of the formula, and all four lines rebuild from it, English included.
Back to the rulebook the site shipped with, rules and data, and to the outputs built when the site was published. The hash returns to the stable value 4e2c…d89b and the box turns green. No network.
Every word on the card is a row in the rulebook’s WidgetTexts table, in English, Español, Deutsch. Switching language changes rows, not code.
The widget is a rulebook
The React component is a generic renderer of a contract, and the contract lives in the same rulebook as the domain. The Leases table is the data; the Widget* tables say which fields bind to which column, which numbers the sliders own, which tools run, and what every label says. Adding a tool is a row, not a release.
The tables, live
LeasesThe domain. Three leases, one rule: a late fee when the rent is more than a few days late.
LeaseIdraw string · keyNameraw stringThe unit, as the tenant knows it.MonthlyRentraw numberRent due each month, in dollars.DaysLateraw integerHow many days past the due date the rent arrived.LateFeecalculated numberThe rule. Nothing after the grace period costs nothing; after it, a share of the rent.=IF({{DaysLate}} > 5, {{MonthlyRent}} * 0.08, 0)| LeaseId | Name | MonthlyRent | DaysLate | LateFee |
|---|---|---|---|---|
| unit-2b | Unit 2B | 1850 | 5 | — |
| unit-4a | Unit 4A | 2400 | 9 | — |
| unit-7c | Unit 7C | 1200 | 13 | — |
WidgetFieldsWhich domain fields the widget binds to, and how. A row here is a column in the data table.
WidgetFieldIdraw string · keyNameraw stringTableNameraw stringFieldNameraw stringRoleraw stringrow-label · money · stepper · resultRequiredTyperaw stringraw or calculated: what the contract check demands of the field.RequiredDatatyperaw stringMinraw numberMaxraw numberStepraw numberHeaderTextKeyraw stringSortOrderraw integer| WidgetFieldId | Name | TableName | FieldName | Role | RequiredType | RequiredDatatype | Min | Max | Step | HeaderTextKey | SortOrder |
|---|---|---|---|---|---|---|---|---|---|---|---|
| lease | Lease | Leases | Name | row-label | raw | string | — | — | — | col.lease | 1 |
| rent | Rent | Leases | MonthlyRent | money | raw | number | — | — | — | col.rent | 2 |
| days | Days late | Leases | DaysLate | stepper | raw | integer | 0 | 30 | 1 | col.days | 3 |
| fee | Late fee | Leases | LateFee | result | calculated | number | — | — | — | col.fee | 4 |
WidgetSlidersThe rule sliders. Each points at one number inside one formula and rewrites it.
WidgetSliderIdraw string · keyNameraw stringTableNameraw stringFieldNameraw stringLiteralIndexraw integerWhich numeric literal in the formula, counting from 1, outside {{refs}} and quotes.Minraw numberMaxraw numberStepraw numberDisplayScaleraw number100 shows 0.08 as 8.LabelTextKeyraw stringUnitTextKeyraw stringDefaultValueraw numberSortOrderraw integer| WidgetSliderId | Name | TableName | FieldName | LiteralIndex | Min | Max | Step | DisplayScale | LabelTextKey | UnitTextKey | DefaultValue | SortOrder |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| grace | Grace period | Leases | LateFee | 1 | 0 | 15 | 1 | 1 | slider.grace | unit.days | 5 | 1 |
| rate | Late fee rate | Leases | LateFee | 2 | 0.01 | 0.15 | 0.01 | 100 | slider.rate | unit.percent | 0.08 | 2 |
WidgetToolsEvery tool or converter the widget runs. A row here is a line in the widget, an export button, or a drop-box format. No code change needed.
WidgetToolIdraw string · keyNameraw stringrunner/tool-name, unique.ToolNameraw stringRunnerraw stringserver = the effortless CLI through /api/live-rulebook · browser = a converter registered in converters/InverseToolNameraw stringThe tool that turns this format back into a rulebook, if any.Priorityraw integerLower runs first. rulebook-to-typescript is 1: it computes the numbers.Roleraw stringComma list of compile · compute · show · export · provenanceExtractorraw stringts-calc-function · sql-calc-function · xlsx-formula · rulespeak-definition · filesLanguageraw stringHow the shown line is coloured: sql · ts · excel · proseLabelTextKeyraw stringExportTextKeyraw stringExportFilePatternraw stringAcceptsExtensionsraw stringIsEnabledraw booleanSortOrderraw integer| WidgetToolId | Name | ToolName | Runner | InverseToolName | Priority | Role | Extractor | Language | LabelTextKey | ExportTextKey | ExportFilePattern | AcceptsExtensions | IsEnabled | SortOrder |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| compile | server/compile-rulebook | compile-rulebook | server | — | 0 | compile | files | — | — | — | — | — | true | 0 |
| postgres | server/rulebook-to-postgres | rulebook-to-postgres | server | — | 2 | show | sql-calc-function | sql | tool.postgres | — | — | — | true | 1 |
| typescript | server/rulebook-to-typescript | rulebook-to-typescript | server | — | 1 | compute,show,export | ts-calc-function | ts | tool.typescript | export.typescript | {name}-typescript.zip | — | true | 2 |
| excel | server/rulebook-to-xlsx | rulebook-to-xlsx | server | xlsx-to-rulebook | 2 | show,export | xlsx-formula | excel | tool.excel | export.excel | {name}.xlsx | .xlsx | true | 3 |
| english | server/rulebook-to-rulespeak | rulebook-to-rulespeak | server | — | 2 | show | rulespeak-definition | prose | tool.english | — | — | — | true | 4 |
| explainer | server/rulebook-to-explainer-dag | rulebook-to-explainer-dag | server | — | 3 | provenance | files | — | — | — | — | — | true | 5 |
| markdown | server/rulebook-to-markdown | rulebook-to-markdown | server | — | 3 | export | files | — | — | export.markdown | {name}.md | — | true | 6 |
| yaml | browser/rulebook-to-yaml | rulebook-to-yaml | browser | yaml-to-rulebook | 3 | export | files | — | — | export.yaml | {name}.rulebook.yaml | .yaml,.yml | true | 7 |
| xml | browser/rulebook-to-xml | rulebook-to-xml | browser | xml-to-rulebook | 3 | export | files | — | — | export.xml | {name}.rulebook.xml | .xml | true | 8 |
| toml | browser/rulebook-to-toml | rulebook-to-toml | browser | toml-to-rulebook | 3 | export | files | — | — | export.toml | {name}.rulebook.toml | .toml | true | 9 |
| csv | browser/rulebook-to-csv | rulebook-to-csv | browser | — | 3 | export | files | — | — | export.csv | {name}-csv.zip | — | true | 10 |
WidgetTextsEvery word the widget shows, in every language it offers. Placeholders look like {this}.
WidgetTextIdraw string · keyNameraw stringTextKeyraw stringLocaleraw stringTextraw string| WidgetTextId | Name | TextKey | Locale | Text |
|---|---|---|---|---|
| hash.label:en | hash.label:en | hash.label | en | rulebook |
| hash.label:es | hash.label:es | hash.label | es | rulebook |
| hash.label:de | hash.label:de | hash.label | de | Rulebook |
| hash.stable:en | hash.stable:en | hash.stable | en | stable hash |
| hash.stable:es | hash.stable:es | hash.stable | es | hash estable |
| hash.stable:de | hash.stable:de | hash.stable | de | stabiler Hash |
| hash.changed:en | hash.changed:en | hash.changed | en | new hash |
| hash.changed:es | hash.changed:es | hash.changed | es | hash nuevo |
| hash.changed:de | hash.changed:de | hash.changed | de | neuer Hash |
| slider.grace:en | slider.grace:en | slider.grace | en | Grace |
| slider.grace:es | slider.grace:es | slider.grace | es | Gracia |
| slider.grace:de | slider.grace:de | slider.grace | de | Karenz |
| slider.rate:en | slider.rate:en | slider.rate | en | Late fee |
| slider.rate:es | slider.rate:es | slider.rate | es | Recargo |
| slider.rate:de | slider.rate:de | slider.rate | de | Säumnisgebühr |
| unit.days:en | unit.days:en | unit.days | en | {v} days |
| unit.days:es | unit.days:es | unit.days | es | {v} días |
| unit.days:de | unit.days:de | unit.days | de | {v} Tage |
| unit.percent:en | unit.percent:en | unit.percent | en | {v}% |
| unit.percent:es | unit.percent:es | unit.percent | es | {v} % |
| unit.percent:de | unit.percent:de | unit.percent | de | {v} % |
| tool.postgres:en | tool.postgres:en | tool.postgres | en | Postgres |
| tool.postgres:es | tool.postgres:es | tool.postgres | es | Postgres |
| tool.postgres:de | tool.postgres:de | tool.postgres | de | Postgres |
| tool.typescript:en | tool.typescript:en | tool.typescript | en | TypeScript |
| tool.typescript:es | tool.typescript:es | tool.typescript | es | TypeScript |
| tool.typescript:de | tool.typescript:de | tool.typescript | de | TypeScript |
| tool.excel:en | tool.excel:en | tool.excel | en | Excel |
| tool.excel:es | tool.excel:es | tool.excel | es | Excel |
| tool.excel:de | tool.excel:de | tool.excel | de | Excel |
| tool.english:en | tool.english:en | tool.english | en | English |
| tool.english:es | tool.english:es | tool.english | es | Inglés |
| tool.english:de | tool.english:de | tool.english | de | Englisch |
| col.lease:en | col.lease:en | col.lease | en | Lease |
| col.lease:es | col.lease:es | col.lease | es | Contrato |
| col.lease:de | col.lease:de | col.lease | de | Mietvertrag |
| col.rent:en | col.rent:en | col.rent | en | Rent |
| col.rent:es | col.rent:es | col.rent | es | Renta |
| col.rent:de | col.rent:de | col.rent | de | Miete |
| col.days:en | col.days:en | col.days | en | Days late |
WidgetLocalesThe languages the widget offers.
WidgetLocaleIdraw string · keyNameraw stringCoderaw stringIsDefaultraw booleanSortOrderraw integer| WidgetLocaleId | Name | Code | IsDefault | SortOrder |
|---|---|---|---|---|
| en | English | en | true | 1 |
| es | Español | es | false | 2 |
| de | Deutsch | de | false | 3 |
WidgetSettingsSingle values. The stable hash is a row, so recording it does not move the hash.
WidgetSettingIdraw string · keyNameraw stringStableRulebookHashraw stringSHA-256 of the schema of the 5-day / 8% rulebook. Green when the live hash equals it.DefaultLocaleraw stringPrimaryResultFieldraw stringTable.Field the widget is about.WorkerTimeoutSecondsraw integerHow long generated code may run before the worker is killed.| WidgetSettingId | Name | StableRulebookHash | DefaultLocale | PrimaryResultField | WorkerTimeoutSeconds |
|---|---|---|---|---|---|
| default | default | 4e2ce86326020cda2706f5f7b3fe199968a8f9ace6dbefdc2d55b656923cd89b | en | Leases.LateFee | 5 |
7 tables · 61 fields · 312 rows, read from the bundled late-fee.rulebook.json.
Three hashes, three proofs
SHA-256 of the canonical JSON of every table with data removed: sorted keys, no whitespace. It proves the rule is the same. The stable value is a row in WidgetSettings, so recording it cannot move it.
The same hash with the rows kept. It proves the data is the same too, and is only used as the cache key for exports, which carry the rows.
SHA-256 over the sorted list of (path, sha256 of contents) for everything a tool produced. It proves the output is the same, whichever runner made it. A browser converter computes the identical shape over its bytes.
What shipped with this page
| Line | Tool | Version | Package hash | Extractor |
|---|---|---|---|---|
| Postgres | rulebook-to-postgres | v2026.10.05.0056 | 3e56…657b | sql-calc-function |
| TypeScript | rulebook-to-typescript | v2026.09.14.2224 | ba8b…475e | ts-calc-function |
| Excel | rulebook-to-xlsx | v2026.09.17.1646 | c5f2…8517 | xlsx-formula |
| English | rulebook-to-rulespeak | v2026.10.05.1648 | 6d92…20af | rulespeak-definition |
Built 2026-10-05 from rulebook 4e2c…d89b by the same extractors the server runs, so the first paint needs no server and Reset is instant.
Two jobs
POST /api/live-rulebook/transpile takes a tool name and a rulebook, runs the effortless CLI in a temp project (anonymously; the server holds no account key), computes the package hash, pulls out the one line the widget shows, and deletes the temp project. POST /api/live-rulebook/import does the same for a dropped workbook. A cold tool takes half a minute to wake; the card says so.
Hashing with crypto.subtle, caching by tool@hash in memory and localStorage, one request in flight per tool with the newest replacing a queued one, latest-wins when results arrive out of order, running generated code in a Web Worker, and the converters: rulebook-to-yaml, rulebook-to-xml, rulebook-to-toml, rulebook-to-csv run here, with no server call. The explainer DAG from rulebook-to-explainer-dag drives the derivation panel.
A dropped rulebook, one stage at a time
- 1Read
JSON, YAML, XML or TOML is parsed in the browser. A parse error names the line and column.
- 2Import
An .xlsx goes to the server once, where xlsx-to-rulebook patches the current rulebook with the workbook's formulas and reports what changed.
- 3Contract
Does this rulebook fit the widget? Every missing table, wrong type, short formula and missing text is listed. You can cancel or use it anyway.
- 4Compile
compile-rulebook recomputes every calculated value as a set. A formula that does not compile is reported by field and formula.
- 5TypeScript build
rulebook-to-typescript runs; the generated files are transformed in a Web Worker. A transform error names the file and line.
- 6Run
The generated code computes every row in the worker, under a timeout. Per-field errors come back on the row; an infinite loop is killed.
Each stage reports its own failure, in the words of the tool that failed. Only after all six does the new rulebook replace the current one and the other lines rebuild from it. Exports today: TypeScript, Excel, Markdown, YAML, XML, TOML, CSV, plus the rulebook itself.
Why a new tool is a row
A WidgetTools row names a tool, a runner, an extractor and a text key. Add one for any rulebook-to-xyz in the catalog and the widget grows a line, an export button, or a drop-box format, with no change to the component. The contract-in-the-rulebook is the point of the whole method, shown on the method’s own home page. Back to the widget →