Start a projectStart
How this works

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.

Leases › LateFeerulebook 4e2c…d89bstable hash
=IF({{DaysLate}} > 5, {{MonthlyRent}} * 0.08, 0)
PostgresSELECT (CASE WHEN COALESCE(((SELECT days_late FROM leases WHERE lease_id = p_lease_id))::NUMERIC, 0) > 5 THEN ((COALESCE((SELECT CASE WHEN v::text ~ '^-?[0-9]*\.?[0-9]+$' THEN v::numeric ELSE NULL END FROM (SELECT ((SELECT monthly_rent FROM leases WHERE lease_id = p_lease_id)) AS v OFFSET 0) __safe_numeric), 0) * COALESCE(0.08, 0)))::text ELSE (0)::text END)::numeric;
TypeScripttoFloatPtr(erbIf(erbBool3(erbCmp(erbNullif(vIntPlain(tc.days_late)), ">", vI(5))), () => erbMul(vNumPlain(tc.monthly_rent), vF(0.08)), () => vI(0)))
ExcelIF(((D2 > 5)=TRUE), C2 * 0.08, 0)
EnglishDetermined by priority: the monthly rent times 0.08 if the days late is greater than 5; in all other cases, 0.
LeaseRentDays lateLate fee
Unit 2B$1,850.005$0.00
Unit 4A$2,400.009$192.00
Unit 7C$1,200.0013$96.00
Postgres, TypeScript, Excel and English agree✓ All 4 agree
Export
EN
The film

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.

In plain language

What moves, and what does not

A slider changes the rule

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.

A stepper changes data

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.

Every line is a receipt

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, edit, drop it back

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.

Reset

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.

Three languages

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.

Deep dive

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

Leases5 fields · 3 rowsThe domain. Three leases, one rule: a late fee when the rent is more than a few days late.
LeaseIdraw string · key
Nameraw 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)
LeaseIdNameMonthlyRentDaysLateLateFee
unit-2bUnit 2B18505—
unit-4aUnit 4A24009—
unit-7cUnit 7C120013—
WidgetFields12 fields · 4 rowsWhich domain fields the widget binds to, and how. A row here is a column in the data table.
WidgetFieldIdraw string · key
Nameraw string
TableNameraw string
FieldNameraw string
Roleraw stringrow-label · money · stepper · result
RequiredTyperaw stringraw or calculated: what the contract check demands of the field.
RequiredDatatyperaw string
Minraw number
Maxraw number
Stepraw number
HeaderTextKeyraw string
SortOrderraw integer
WidgetFieldIdNameTableNameFieldNameRoleRequiredTypeRequiredDatatypeMinMaxStepHeaderTextKeySortOrder
leaseLeaseLeasesNamerow-labelrawstring———col.lease1
rentRentLeasesMonthlyRentmoneyrawnumber———col.rent2
daysDays lateLeasesDaysLatestepperrawinteger0301col.days3
feeLate feeLeasesLateFeeresultcalculatednumber———col.fee4
WidgetSliders13 fields · 2 rowsThe rule sliders. Each points at one number inside one formula and rewrites it.
WidgetSliderIdraw string · key
Nameraw string
TableNameraw string
FieldNameraw string
LiteralIndexraw integerWhich numeric literal in the formula, counting from 1, outside {{refs}} and quotes.
Minraw number
Maxraw number
Stepraw number
DisplayScaleraw number100 shows 0.08 as 8.
LabelTextKeyraw string
UnitTextKeyraw string
DefaultValueraw number
SortOrderraw integer
WidgetSliderIdNameTableNameFieldNameLiteralIndexMinMaxStepDisplayScaleLabelTextKeyUnitTextKeyDefaultValueSortOrder
graceGrace periodLeasesLateFee101511slider.graceunit.days51
rateLate fee rateLeasesLateFee20.010.150.01100slider.rateunit.percent0.082
WidgetTools15 fields · 11 rowsEvery 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 · key
Nameraw stringrunner/tool-name, unique.
ToolNameraw string
Runnerraw 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 · provenance
Extractorraw stringts-calc-function · sql-calc-function · xlsx-formula · rulespeak-definition · files
Languageraw stringHow the shown line is coloured: sql · ts · excel · prose
LabelTextKeyraw string
ExportTextKeyraw string
ExportFilePatternraw string
AcceptsExtensionsraw string
IsEnabledraw boolean
SortOrderraw integer
WidgetToolIdNameToolNameRunnerInverseToolNamePriorityRoleExtractorLanguageLabelTextKeyExportTextKeyExportFilePatternAcceptsExtensionsIsEnabledSortOrder
compileserver/compile-rulebookcompile-rulebookserver—0compilefiles—————true0
postgresserver/rulebook-to-postgresrulebook-to-postgresserver—2showsql-calc-functionsqltool.postgres———true1
typescriptserver/rulebook-to-typescriptrulebook-to-typescriptserver—1compute,show,exportts-calc-functiontstool.typescriptexport.typescript{name}-typescript.zip—true2
excelserver/rulebook-to-xlsxrulebook-to-xlsxserverxlsx-to-rulebook2show,exportxlsx-formulaexceltool.excelexport.excel{name}.xlsx.xlsxtrue3
englishserver/rulebook-to-rulespeakrulebook-to-rulespeakserver—2showrulespeak-definitionprosetool.english———true4
explainerserver/rulebook-to-explainer-dagrulebook-to-explainer-dagserver—3provenancefiles—————true5
markdownserver/rulebook-to-markdownrulebook-to-markdownserver—3exportfiles——export.markdown{name}.md—true6
yamlbrowser/rulebook-to-yamlrulebook-to-yamlbrowseryaml-to-rulebook3exportfiles——export.yaml{name}.rulebook.yaml.yaml,.ymltrue7
xmlbrowser/rulebook-to-xmlrulebook-to-xmlbrowserxml-to-rulebook3exportfiles——export.xml{name}.rulebook.xml.xmltrue8
tomlbrowser/rulebook-to-tomlrulebook-to-tomlbrowsertoml-to-rulebook3exportfiles——export.toml{name}.rulebook.toml.tomltrue9
csvbrowser/rulebook-to-csvrulebook-to-csvbrowser—3exportfiles——export.csv{name}-csv.zip—true10
WidgetTexts5 fields · 288 rowsEvery word the widget shows, in every language it offers. Placeholders look like {this}.
WidgetTextIdraw string · key
Nameraw string
TextKeyraw string
Localeraw string
Textraw string
WidgetTextIdNameTextKeyLocaleText
hash.label:enhash.label:enhash.labelenrulebook
hash.label:eshash.label:eshash.labelesrulebook
hash.label:dehash.label:dehash.labeldeRulebook
hash.stable:enhash.stable:enhash.stableenstable hash
hash.stable:eshash.stable:eshash.stableeshash estable
hash.stable:dehash.stable:dehash.stabledestabiler Hash
hash.changed:enhash.changed:enhash.changedennew hash
hash.changed:eshash.changed:eshash.changedeshash nuevo
hash.changed:dehash.changed:dehash.changeddeneuer Hash
slider.grace:enslider.grace:enslider.graceenGrace
slider.grace:esslider.grace:esslider.graceesGracia
slider.grace:deslider.grace:deslider.gracedeKarenz
slider.rate:enslider.rate:enslider.rateenLate fee
slider.rate:esslider.rate:esslider.rateesRecargo
slider.rate:deslider.rate:deslider.ratedeSäumnisgebühr
unit.days:enunit.days:enunit.daysen{v} days
unit.days:esunit.days:esunit.dayses{v} días
unit.days:deunit.days:deunit.daysde{v} Tage
unit.percent:enunit.percent:enunit.percenten{v}%
unit.percent:esunit.percent:esunit.percentes{v} %
unit.percent:deunit.percent:deunit.percentde{v} %
tool.postgres:entool.postgres:entool.postgresenPostgres
tool.postgres:estool.postgres:estool.postgresesPostgres
tool.postgres:detool.postgres:detool.postgresdePostgres
tool.typescript:entool.typescript:entool.typescriptenTypeScript
tool.typescript:estool.typescript:estool.typescriptesTypeScript
tool.typescript:detool.typescript:detool.typescriptdeTypeScript
tool.excel:entool.excel:entool.excelenExcel
tool.excel:estool.excel:estool.excelesExcel
tool.excel:detool.excel:detool.exceldeExcel
tool.english:entool.english:entool.englishenEnglish
tool.english:estool.english:estool.englishesInglés
tool.english:detool.english:detool.englishdeEnglisch
col.lease:encol.lease:encol.leaseenLease
col.lease:escol.lease:escol.leaseesContrato
col.lease:decol.lease:decol.leasedeMietvertrag
col.rent:encol.rent:encol.rentenRent
col.rent:escol.rent:escol.rentesRenta
col.rent:decol.rent:decol.rentdeMiete
col.days:encol.days:encol.daysenDays late
… and 248 more rows
WidgetLocales5 fields · 3 rowsThe languages the widget offers.
WidgetLocaleIdraw string · key
Nameraw string
Coderaw string
IsDefaultraw boolean
SortOrderraw integer
WidgetLocaleIdNameCodeIsDefaultSortOrder
enEnglishentrue1
esEspañolesfalse2
deDeutschdefalse3
WidgetSettings6 fields · 1 rowsSingle values. The stable hash is a row, so recording it does not move the hash.
WidgetSettingIdraw string · key
Nameraw string
StableRulebookHashraw stringSHA-256 of the schema of the 5-day / 8% rulebook. Green when the live hash equals it.
DefaultLocaleraw string
PrimaryResultFieldraw stringTable.Field the widget is about.
WorkerTimeoutSecondsraw integerHow long generated code may run before the worker is killed.
WidgetSettingIdNameStableRulebookHashDefaultLocalePrimaryResultFieldWorkerTimeoutSeconds
defaultdefault4e2ce86326020cda2706f5f7b3fe199968a8f9ace6dbefdc2d55b656923cd89benLeases.LateFee5

7 tables · 61 fields · 312 rows, read from the bundled late-fee.rulebook.json.

Three hashes, three proofs

Rulebook hash

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.

4e2ce86326020cda2706f5f7b3fe199968a8f9ace6dbefdc2d55b656923cd89b
Content hash

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.

9ef8a01bf201712c01a446114f9b89f1b46ffacf9e09a550e19c902ef0e8c813
Package hash

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.

3e5655c5abf56c484ccbf531f5a761ea9f8ef522c84b5d489da20ce2f2ed657b

What shipped with this page

LineToolVersionPackage hashExtractor
Postgresrulebook-to-postgresv2026.10.05.00563e56…657bsql-calc-function
TypeScriptrulebook-to-typescriptv2026.09.14.2224ba8b…475ets-calc-function
Excelrulebook-to-xlsxv2026.09.17.1646c5f2…8517xlsx-formula
Englishrulebook-to-rulespeakv2026.10.05.16486d92…20afrulespeak-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

The server runs real transpiles. Nothing else.

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.

The browser does everything else.

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

  1. 1
    Read

    JSON, YAML, XML or TOML is parsed in the browser. A parse error names the line and column.

  2. 2
    Import

    An .xlsx goes to the server once, where xlsx-to-rulebook patches the current rulebook with the workbook's formulas and reports what changed.

  3. 3
    Contract

    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.

  4. 4
    Compile

    compile-rulebook recomputes every calculated value as a set. A formula that does not compile is reported by field and formula.

  5. 5
    TypeScript build

    rulebook-to-typescript runs; the generated files are transformed in a Web Worker. A transform error names the file and line.

  6. 6
    Run

    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 →