# Parameters and tables: when a norm is data The archive revises the overdue-fee cap every year: 4,000 in 2025, 5,000 in 2026. The discount depends on the reader's category: three quarters for a researcher, half for a student, the full tariff for the rest. These two things differ, though often written identically — as constants in rules. The first is a **parameter**: one magnitude varying along a time axis. The second is a **decision table**: a finite matrix of inputs and outputs. Law DSL gives them their own surfaces, A3: `.lawparam.json` stores a parameter by dates, `.lawtable.json` a table, and a `decision` declaration records a table directly in the statute text. All three lower into the same CLIR as ordinary rules, executed by the same engine — data has no runtime of its own. ```law language "law.core" version "0.2"; package tutorial.archive version "0.15.0"; namespace "urn:law:tutorial:archive"; import tutorial.city version "0.1.0"; entity Person; entity CopyOrder; enum ReaderCategory { Researcher; Student; General; } relation billing_year(p: Person, year: Integer) kind empirical { key(p); } relation reader_category(p: Person, category: ReaderCategory) kind empirical { key(p); } relation copy_order(o: CopyOrder, p: Person, pages: Integer) kind empirical { key(o); } relation days_overdue(p: Person, days: Integer) kind empirical { key(p); } relation copy_fee(o: CopyOrder, amount: Money) kind institutional { key(o); } relation reader_fee(o: CopyOrder, amount: Money) kind institutional { key(o); } relation late_fee(p: Person, amount: Money) kind institutional { key(p); } rule CopyFee strict { for o: CopyOrder; for p: Person; for n: Integer; for y: Integer; for rate: Money; when copy_order(o, p, n) and billing_year(p, y) and tutorial.city::base_page_rate(y, rate); then copy_fee(o, n * rate); } ``` ## A table in the statute text A category discount is a `decision`: typed parameters, a result type, and a table with a hit policy. ```law decision ReaderDiscount(category: ReaderCategory) -> Decimal { table hit first { when category == Researcher => 0.75; when category == Student => 0.5; otherwise => 1.0; } } rule ReaderFee strict { label ru-KZ unofficial "Плата читателя: тариф, умноженный на долю его категории, с округлением до тиына"; for o: CopyOrder; for p: Person; for n: Integer; for a: Money; for c: ReaderCategory; when copy_order(o, p, n) and copy_fee(o, a) and reader_category(p, c); then reader_fee(o, round(a * ReaderDiscount(c), 2, "HALF_UP")); } ``` The label reads: "Reader fee: the tariff times her category share, rounded to the tiyn". | Category | `ReaderDiscount` | `reader_fee` at a 1,050 tariff | |---|---|---| | researcher | `0.75` | `787.50 KZT` | | student | `0.5` | `525.00 KZT` | | other reader | `1.0` | `1050.00 KZT` | The test from this page, byte for byte: ```law test "исследователь: три четверти тарифа — 787,50" { given { context { decision_time @2026-04-20T09:00:00+05:00; knowledge_time @2026-04-20T09:00:00+05:00; legal_time @2026-04-20; timezone "Asia/Almaty"; } assert billing_year(entity_ref("urn:tutorial:ivanova"), 2026) { id "assert-year"; origin case_input; } assert copy_order(entity_ref("urn:tutorial:order1"), entity_ref("urn:tutorial:ivanova"), 7) { id "assert-order"; origin case_input; } assert reader_category(entity_ref("urn:tutorial:ivanova"), Researcher) { id "assert-category"; origin case_input; } } evaluate truth(reader_fee(entity_ref("urn:tutorial:order1"), 787.50 KZT)); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED; } ``` The test name reads: "Researcher: three quarters of the tariff — 787.50." `decision` lowers into a pure function, and the rule calls it as a term. The hit policy obliges the author before the compiler. `unique` requires proving rows pairwise disjoint, which statics over an enumeration cannot prove — it refuses `LDC-E8304`. `first` checks rows in order, and order becomes semantics: put `otherwise` as the first row and everyone gets the full tariff. `otherwise` makes the table total; without it, a category outside the rows yields `NoDecision`, not zero and not an error. ## A parameter by dates The fee cap is a magnitude with the `legal_time` axis. In the practicum package it lies next to the statute, in `late-fee-cap.lawparam.json`: ```text { "schemaVersion": "law.params/0.1", "kind": "parameter-dataset", "id": "urn:law:tutorial:archive:param:late-fee-cap", "parameter": "urn:law:tutorial:archive#LateFeeCap", "valueType": {"name": "urn:law:std#Money"}, "timeAxis": "legal_time", "granularity": "date", "coverage": {"start": "2025-01-01", "endExclusive": "2027-01-01"}, "values": [ {"id": "…#r-2025", "valid": {"start": "2025-01-01", "endExclusive": "2026-01-01"}, "value": {"kind": "value", "type": {"name": "urn:law:std#Money"}, "value": 4000, "currency": "KZT"}, "anchors": ["urn:law:tutorial:archive#ARCHIVE_RULES_P9"]}, {"id": "…#r-2026", "valid": {"start": "2026-01-01", "endExclusive": "2027-01-01"}, "value": {"kind": "value", "type": {"name": "urn:law:std#Money"}, "value": 5000, "currency": "KZT"}, "anchors": ["urn:law:tutorial:archive#ARCHIVE_RULES_P9"]} ] } ``` The statute reads the parameter by name, like a constant: the package's `LateFee` rule writes `min(total, LateFeeCap)`. The engine picks the value by the case's `legal_time`, and outside the parameter's coverage it substitutes nothing — answering `MISSING_PARAMETER_VALUE`, not zero. Each value carries an anchor to a source fragment: data obeys the same provenance discipline as norms. | `legal_time` | 60 days overdue, scale yields 7,000 | `late_fee` | |---|---|---| | 20 December 2025 | cap 4,000 | `4000 KZT` | | 20 April 2026 | cap 5,000 | `5000 KZT` | These two rows execute not on the page but on the **practicum package**: a literate page carries no sidecars, and the `LateFeeCap` name would not resolve in its text. The test from the page file runs on the package world: ```law test "та же просрочка на дату права 2025 года — потолок 4000" { given { context { decision_time @2026-04-20T09:00:00+05:00; knowledge_time @2026-04-20T09:00:00+05:00; legal_time @2025-12-20; timezone "Asia/Almaty"; } assert billing_year(entity_ref("urn:tutorial:ivanova"), 2026) { id "assert-year"; origin case_input; } assert days_overdue(entity_ref("urn:tutorial:ivanova"), 60) { id "assert-days"; origin case_input; } } evaluate truth(late_fee(entity_ref("urn:tutorial:ivanova"), 4000 KZT)); expect truth_status == TRUE_ONLY; } ``` The test name reads: "The same overdue spell on the 2025 date of law — cap 4000." Note the two different times: the billing year arrives as a case fact and serves as the council rate's axis; the date of law serves as the cap's axis. The same overdue spell on different dates of law yields a different cap, and that is a property of the parameter, not a mistake. The parameter is pinned in the version-0.2 `law.lock` as a resource with a byte hash: swap a number in the JSON without regenerating the lock and the compiler refuses `LDC-E8704` before parsing sources. The list of the three A3 sidecar kinds — parameter, table, `decision` — is exhaustive, and as of 05.09.2026 the corpus holds 382 `.lawparam.json` files and 61 `.lawtable.json` ones. ## A JSON table and its input A large matrix — the 77 IBAN-registry countries, a disease schedule — is not kept in the statute text: it lives in its own `.lawtable.json` file with typed columns, anchored rows, and a hit policy. In `function` mode such a table lowers into the same function as `decision`. In `rules` mode it lowers into rules and **emits** the declared relation: so the ISO 13616 registry produces `country_iban_format(letter1, letter2, …)`, and the statute reads it as an ordinary fact. ```law relation country_iban_format(letter1: Text, letter2: Text, ibanLength: Integer, bbanLength: Integer, ibanStructure: Text, bbanStructure: Text) kind institutional { ``` The `rules` mode has a boundary to know in advance: table rows fire only off a fact of the internal `