Markdown for LLMs
Parameters and tables: when a norm is data
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# 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 `<table>/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/).