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, with its three parts kept apart: the rule (Leases.LateFee), its parameters (the LateFeePolicies row) and its data (three leases). Move a slider and the policy changes, and the policy is part of the rules: a new hash, a new version, and every tool rebuilds while each lease looks the new value up. Change a lease and that is data: the same version, one lease recomputed. Change a formula (export to Excel, edit, drop it back) and that is a new version too.

Rulebook Leases › LateFee
=IF({{DaysLate}} > {{GraceDays}}, {{MonthlyRent}} * {{LateFeePercent}}, 0)
Policy · LateFeePolicies › standardEach lease looks these up
Witnesses · the data the rule is computed over
LeaseRentDays lateGrace ↗Fee % ↗Late fee
Unit 2B$1,850.00558%$0.00
Unit 4A$2,400.00958%$192.00
Unit 7C$1,200.001358%$96.00
Generated from the rulebook · real tool output
EnglishDetermined by priority: the monthly rent times the late fee percent if the days late is greater than the grace days; in all other cases, 0.
ExcelIF(((D2 > F2)=TRUE), C2 * G2, 0)
Export
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

Rule, parameters, data

The rule is one formula: =IF({{DaysLate}} > {{GraceDays}}, {{MonthlyRent}} * {{LateFeePercent}}, 0). It never names a number. GraceDays and LateFeePercent live in a policy row, and each lease looks them up through the policy it points at. The data is the leases: rent and days late.

A slider changes a parameter

It writes one cell of LateFeePolicies › standard. The policy is part of the rules, so the rulebook is a new version and every tool rebuilds from it. The worker recomputes every table at once, lookups first, so the looked-up columns and the fees move together.

The hash names the rules

SHA-256 over the rulebook with its data rows cleared, the policy rows kept: names, types, formulas, relationships and the parameters. A slider moves a parameter, so it is a new hash; a stepper moves a lease, so it is not. Shown as its last four characters: 0AF1 is the shipped rules.

Every new hash is a version

v1 is the shipped rulebook. Each new hash becomes the next version, stamped with the time it was first computed. Pick a version from the dropdown and its rules apply to the data on screen; Reset clears the list back to v1.

Every line is a receipt

Hover a line: the rulebook hash it was generated from, the tool and its version, and the package hash of everything the tool produced. Click the TypeScript or Postgres line to see all of it, formatted. The lines move only when a formula does.

Every file carries its answers

Each export is the compiled rulebook: compile-rulebook fills in every looked-up and calculated value, so even YAML and CSV list the fees. The file name carries the hash: late-fee-0AF1.xlsx was built from the shipped rules.

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 policy cells the sliders edit, which tools run, and how each is labelled. Adding a tool is a row, not a release. Even the Vibe / Developer / Enterprise picker is a table: WidgetLevels says what each level shows, and each WidgetTools row names the levels it appears at.

The tables, live

LateFeePolicies4 fields · 1 rowsThe parameters of the rule, kept apart from the formula and from the leases. Each lease points at a policy and looks its values up; the sliders edit this row.
LateFeePolicyIdraw string · key
Nameraw string
GraceDaysraw integerDays after the due date before a fee applies.
LateFeePercentraw numberThe share of the monthly rent charged as the fee (0.08 = 8%).
LateFeePolicyIdNameGraceDaysLateFeePercent
standardStandard50.08
Leases8 fields · 3 rowsThe data. Three leases, each pointing at a late-fee policy; the fee is computed from the lease and the policy it looks up.
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.
LateFeePolicyrelationship stringThe policy this lease follows. Holds a LateFeePolicies.LateFeePolicyId.
GraceDayslookup integerLooked up from the lease's policy.=INDEX(LateFeePolicies!{{GraceDays}}, MATCH({{LateFeePolicy}}, LateFeePolicies!{{LateFeePolicyId}}, 0))
LateFeePercentlookup numberLooked up from the lease's policy.=INDEX(LateFeePolicies!{{LateFeePercent}}, MATCH({{LateFeePolicy}}, LateFeePolicies!{{LateFeePolicyId}}, 0))
LateFeecalculated numberThe rule: no fee within the grace period; after it, the policy's share of the rent.=IF({{DaysLate}} > {{GraceDays}}, {{MonthlyRent}} * {{LateFeePercent}}, 0)
LeaseIdNameMonthlyRentDaysLateLateFeePolicyGraceDaysLateFeePercentLateFee
unit-2bUnit 2B18505standard———
unit-4aUnit 4A24009standard———
unit-7cUnit 7C120013standard———
WidgetFields13 fields · 6 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 · stepper · lookup · result · value
Formatraw stringtext · money · integer · percent
RequiredTyperaw stringraw or calculated: what the contract check demands of the field.
RequiredDatatyperaw string
Minraw number
Maxraw number
Stepraw number
Headerraw stringThe column header.
SortOrderraw integer
WidgetFieldIdNameTableNameFieldNameRoleFormatRequiredTypeRequiredDatatypeMinMaxStepHeaderSortOrder
leaseLeaseLeasesNamerow-labeltextrawstring———Lease1
rentRentLeasesMonthlyRentvaluemoneyrawnumber———Rent2
daysDays lateLeasesDaysLatestepperintegerrawinteger0301Days late3
graceGrace (looked up)LeasesGraceDayslookupintegerlookupinteger———Grace ↗4
percentFee percent (looked up)LeasesLateFeePercentlookuppercentlookupnumber———Fee % ↗5
feeLate feeLeasesLateFeeresultmoneycalculatednumber———Late fee6
WidgetSliders13 fields · 2 rowsThe sliders. Each one edits one value in one row; here, the two parameters of the Standard late-fee policy.
WidgetSliderIdraw string · key
Nameraw string
TableNameraw string
FieldNameraw string
RowIdraw stringThe primary key of the row the slider edits.
Minraw number
Maxraw number
Stepraw number
DisplayScaleraw number100 shows 0.08 as 8.
Labelraw stringWhat the slider is labelled.
Unitraw stringHow a value is shown; {v} is the value times DisplayScale. "one|many" gives a singular form for 1.
DefaultValueraw number
SortOrderraw integer
WidgetSliderIdNameTableNameFieldNameRowIdMinMaxStepDisplayScaleLabelUnitDefaultValueSortOrder
graceGrace periodLateFeePoliciesGraceDaysstandard01511GraceDays{v} day|{v} days51
rateLate fee rateLateFeePoliciesLateFeePercentstandard0.010.150.01100LateFeePercent{v}%0.082
WidgetTools17 fields · 12 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
Labelraw stringThe name the tool line shows.
ExportLabelraw stringThe name in the Export menu.
ExportFilePatternraw stringThe downloaded file name. {name} is the rulebook Name, {hash} the first 8 characters of the rulebook hash (the rule the file was built from).
AcceptsExtensionsraw string
Parametersraw stringComma list of name=value the tool runs with, each passed as -p. simpleSyntax=true makes the TypeScript and Postgres lines the plain code a person would write; the engine row runs without it.
Levelsraw stringComma list of WidgetLevels this tool's line and export appear at. Empty = every level. A compute, compile or provenance role runs whatever the level (provenance only where the level ShowsProvenance).
IsEnabledraw boolean
SortOrderraw integer
WidgetToolIdNameToolNameRunnerInverseToolNamePriorityRoleExtractorLanguageLabelExportLabelExportFilePatternAcceptsExtensionsParametersLevelsIsEnabledSortOrder
compileserver/compile-rulebookcompile-rulebookserver—0compilefiles———————true0
postgresserver/rulebook-to-postgresrulebook-to-postgresserver—2showsql-calc-functionsqlPostgres———simpleSyntax=truedeveloper,enterprisetrue4
typescriptserver/rulebook-to-typescriptrulebook-to-typescriptserver—1showts-calc-functiontsTypeScript———simpleSyntax=truedeveloper,enterprisetrue3
typescript-engineserver/rulebook-to-typescript (engine)rulebook-to-typescriptserver—1compute,exportts-calc-functionts—TypeScript{name}-{hash}-typescript.zip——developer,enterprisetrue3
excelserver/rulebook-to-xlsxrulebook-to-xlsxserverxlsx-to-rulebook2show,exportxlsx-formulaexcelExcelExcel{name}-{hash}.xlsx.xlsx—vibe,developer,enterprisetrue2
englishserver/rulebook-to-rulespeakrulebook-to-rulespeakserver—2showrulespeak-definitionproseEnglish————vibe,developer,enterprisetrue1
explainerserver/rulebook-to-explainer-dagrulebook-to-explainer-dagserver—3provenancefiles———————true5
markdownserver/rulebook-to-markdownrulebook-to-markdownserver—3exportfiles——Markdown{name}-{hash}.md——developer,enterprisetrue6
yamlbrowser/rulebook-to-yamlrulebook-to-yamlbrowseryaml-to-rulebook3exportfiles——YAML{name}-{hash}.rulebook.yaml.yaml,.yml—developer,enterprisetrue7
xmlbrowser/rulebook-to-xmlrulebook-to-xmlbrowserxml-to-rulebook3exportfiles——XML{name}-{hash}.rulebook.xml.xml—enterprisetrue8
tomlbrowser/rulebook-to-tomlrulebook-to-tomlbrowsertoml-to-rulebook3exportfiles——TOML{name}-{hash}.rulebook.toml.toml—enterprisetrue9
csvbrowser/rulebook-to-csvrulebook-to-csvbrowser—3exportfiles——CSV{name}-{hash}-csv.zip——enterprisetrue10
WidgetLevels7 fields · 3 rowsHow much of the widget a visitor sees, picked at the bottom right. Vibe: the rule, English and Excel, versions as v1, v2, v3. Developer: adds TypeScript, Postgres and more export formats. Enterprise: everything, with the rulebook hash on every version and the derivation of every fee.
WidgetLevelIdraw string · key
Nameraw string
Labelraw stringThe name on the level picker.
ShowsHashraw booleanVersions show their rulebook hash and time, and tool lines their receipts on hover. Off: just v1, v2, v3.
ShowsProvenanceraw booleanEvery fee is clickable to show how it was derived (runs rulebook-to-explainer-dag).
IsDefaultraw booleanThe level a first-time visitor sees.
SortOrderraw integer
WidgetLevelIdNameLabelShowsHashShowsProvenanceIsDefaultSortOrder
vibevibeVibefalsefalsetrue1
developerdeveloperDeveloperfalsefalsefalse2
enterpriseenterpriseEnterprisetruetruefalse3
WidgetSettings6 fields · 1 rowsSingle values. The shipped rulebook's hash is not stored here: the hash covers every row, so a row cannot hold it.
WidgetSettingIdraw string · key
Nameraw string
PrimaryResultFieldraw stringTable.Field the widget is about.
WorkerTimeoutSecondsraw integerHow long generated code may run before the worker is killed.
HistoryLimitraw integerHow many rulebooks the hash list keeps (the shipped one is always kept).
ParameterTablesraw stringTables whose rows are part of the rules, so they count in the rulebook hash (comma list). Every other table's rows are data and do not.
WidgetSettingIdNamePrimaryResultFieldWorkerTimeoutSecondsHistoryLimitParameterTables
defaultdefaultLeases.LateFee540LateFeePolicies

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

Two hashes, two proofs

Rulebook hash

SHA-256 of the canonical JSON of the rulebook with every data row cleared and the parameter rows (LateFeePolicies) kept: sorted keys, no whitespace. It proves the same rules. The leases are left out on purpose: a hash of the data is a hash of a database, and says nothing about the rule.

3e1cfbf7df76d36025e68415f95d58bd40ed6014fca3bc30db5372f352910af1
Package hash

SHA-256 over the sorted list of (path, sha256 of contents) for everything a tool produced. It proves the same output, whichever runner made it. A browser converter computes the identical shape over its bytes.

294f583f913c2e737cdd40d94a9760c0a377444d7b8e880e80180ebd354b5d95

What shipped with this page

LineToolVersionPackage hashExtractor
Englishrulebook-to-rulespeakv2026.10.05.1648F82Brulespeak-definition
Excelrulebook-to-xlsxv2026.09.17.1646A7ADxlsx-formula
TypeScriptrulebook-to-typescriptv2026.10.06.1051EE35ts-calc-function
Postgresrulebook-to-postgresv2026.10.06.10545D95sql-calc-function

Built 2026-10-07 from rulebook 0AF1 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: Excel, TypeScript, 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 →