# 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 `/input` relation, fed by the case, not by the act's natural facts. Binding table inputs to package predicates remains a draft; until it exists, natural facts do not answer "which category for such a diagnosis" without an explicit input. This bridge must not be taught as existing. ## Parameter, table, or constant | Magnitude | Form | Why | |---|---|---| | council rate for a year | a foreign package's fact | another act's norm with a year axis | | fee cap by dates | `.lawparam.json` | one magnitude, `legal_time` axis, values are anchored data | | category discount | `decision` | a finite matrix over an enumeration, three rows — readable in text | | format registry for 77 countries | `.lawtable.json` | the matrix is set by the source, not the author; unreadable in text | | tier bound 2,000 | `const` | one value with no axis and no rows | One question decides the choice: **does the magnitude have an axis or rows?** Neither — write a constant. An axis — start a parameter. Rows — a table: in text while eyes can read it, in JSON when the source produces it. ## Compiler refusals A `unique` policy over an enumeration: ```text error LDC-E8304: decision "ReaderDiscount": UNSUPPORTED_FEEL: cannot prove pairwise disjointness required by hit unique ``` An enum member that does not exist — `Pensioner` instead of `Student`: ```text error LDC-E1330: имя "Pensioner" в позиции терма §53 не разрешается ни в одно объявление: ни `const` §54, ни член перечисления §38, ни символ пакета, ни экспорт зависимости §24, ни биндер правила — это unresolved name §188 ``` The diagnostic reads: 'name "Pensioner" in term position resolves into no declaration: neither `const`, nor an enum member, nor a package symbol, nor a dependency export, nor a rule binder — it is an unresolved name'. Both refusals belong to the language. A parameter named on a literate page is the same `LDC-E1330`: only a package build sees the sidecar, the third reason the package step runs on a package, not on a page. ## Next There is an answer — or there is none. Why none, who blocked the derivation, and what it composed of when there is one — [investigating the answer](/tutorials/why/).