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.
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
Section titled “A table in the statute text”A category discount is a decision: typed parameters, a result type, and
a table with a hit policy.
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:
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
Section titled “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:
{ "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:
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
Section titled “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.
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
Section titled “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
Section titled “Compiler refusals”A unique policy over an enumeration:
error LDC-E8304: decision "ReaderDiscount": UNSUPPORTED_FEEL: cannot provepairwise disjointness required by hit uniqueAn enum member that does not exist — Pensioner instead of Student:
error LDC-E1330: имя "Pensioner" в позиции терма §53 не разрешается ни водно объявление: ни `const` §54, ни член перечисления §38, ни символпакета, ни экспорт зависимости §24, ни биндер правила — это unresolvedname §188The 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.
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.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.