Skip to content
docs
Arxo ↗

Parameters and tables: when a norm is data

For LLMs6 sections

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.

Arxo 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 category discount is a decision: typed parameters, a result type, and a table with a hit policy.

Arxo 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”.

CategoryReaderDiscountreader_fee at a 1,050 tariff
researcher0.75787.50 KZT
student0.5525.00 KZT
other reader1.01050.00 KZT

The test from this page, byte for byte:

Arxo 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.

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:

Output
{
"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_time60 days overdue, scale yields 7,000late_fee
20 December 2025cap 4,0004000 KZT
20 April 2026cap 5,0005000 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:

Arxo 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 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.

Arxo 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.

MagnitudeFormWhy
council rate for a yeara foreign package’s factanother act’s norm with a year axis
fee cap by dates.lawparam.jsonone magnitude, legal_time axis, values are anchored data
category discountdecisiona finite matrix over an enumeration, three rows — readable in text
format registry for 77 countries.lawtable.jsonthe matrix is set by the source, not the author; unreadable in text
tier bound 2,000constone 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.

A unique policy over an enumeration:

Output
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:

Output
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.

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.