nb-13 — Repeatable norms without copying
Northbridge is fictional. Every office, fine, notice and filer rule
is synthetic and unofficial. No real municipal deployment or
legal-validity claims. Verified profile: law 0.1.0, law.core/0.2.
Situation
Section titled “Situation”The Northbridge permit desk keeps writing the same norm shape with different fillings: resale draws a fine unless an amnesty applies; a repeat late filing draws a fine, a first one only a notice; two visits mark a frequent filer. Hand copies drift — someone forgets the amnesty exception, or the priority that lets it win.
An expansion is the construct for this task: a rule template written once, with named params (parameters). Each expand block instantiates the template by binding every param, and the engine generates ordinary rules from it. No copies are maintained by hand.
Prerequisites
Section titled “Prerequisites”You need nb-01: First permit: facts, a rule and a question for facts,
strict rules, and law test as the way to check a claim. Below,
TRUE_ONLY means “holds”, FALSE_ONLY means “provably does not
hold”, and NEITHER means “no conclusion either way”.
nb-03: Exceptions and conflicting rules showed
defeasible rules — rules that hold unless defeated — and priorities,
the named orders saying which rule wins a conflict.
nb-04: The permit fee showed decision tables, an
alternative way to compress repetitive norms.
The new words all describe the template machinery. The expansion is the template; each expand block is one instantiation. Its params are what an instance binds: binders, relations, values, lists, cases, integers, windows, options. Exports are the names an instance publishes, and each generated rule is one emit rule with a fixed stable identifier. Scope, dates, labels and sources flow from the expand site to the generated rules — that flow is metadata inheritance. Each word is explained again where it first appears below.
Minimal example
Section titled “Minimal example”All fragments are excerpts from
packs/examples/language-demo/templates/package.law (identifiers as
written; surrounding relations and unrelated expansions cut).
Excerpt 1 — a whole template plus its instance (lines 28–42). The
pair expansion takes three params — a subject binder (a placeholder
for the quantified individual), a ground relation and a result
relation — and emits one strict rule named self/ok, where self
means “this instance”.
expansion pair { params { subject: binder; ground: relation(subject); result: relation(subject); } exports { ok = self/ok; } emit rule self/ok strict { for subject; when ground(subject); then result(subject); scope from self; effective from self; labels from self; source from self; }}
expand pair WorkerPermit { label en unofficial "Workers in the city get a worker permit"; bind subject = a: Applicant; ground = employed_in_city; result = worker_permit;}Look at how the two halves divide the work: the template says how,
the instance says what. The instance binds a real binder
(a: Applicant) and two real relations to the three params. Look also
at the last emit line — metadata inheritance: the generated rule takes
scope, effective dates, labels and source from the expand site
(self), so the answer carries the “Workers in the city” label.
Excerpt 2 — the sanction template head (lines 84–101): two binders,
a value (option), a ground relation, a list of extra conditions, a
result relation, and excluded — named exception cases with
non-empty premise lists.
expansion sanction { params { subject: binder; matter: binder; option: value; ground: relation(subject, matter); conditions: list<relation(subject, matter)> min 0; result: relation(subject, matter, option); excluded: cases { premises: list<relation(subject, matter)> min 1; }; } exports { applicability = self/applicable; } emit rule self/applicable defeasible { for subject; for matter; when ground(subject, matter); for each p in conditions { when p(subject, matter); } then result(subject, matter, option); scope from self; effective from self; labels from self; source from self; }Look at the for each p in conditions line: it unrolls the list, so
one template covers “ground plus conditions yields a sanction” for any
arity-two relations. Look also at the emit head: the main rule is
defeasible, because exceptions exist by design (next excerpt).
Excerpt 3 — the exception loop of the same template (lines 102–116).
Per excluded case it emits two nodes: a defeater concluding
not result(...), and a priority preferring the defeater over the
main rule. key(c) names each node after its case; the case label is
prefixed, falling back to the expand site.
for each c in excluded { emit rule self/excluded/key(c)/excludes defeasible { for subject; for matter; for each p in c.premises { when p(subject, matter); } then not result(subject, matter, option); scope from self; effective from self; labels from c prefix "[excludes] "; source from c fallback self; } emit priority self/excluded/key(c)/priority { prefer self/excluded/key(c)/excludes over self/applicable; reason explicit_exception; labels from c prefix "[priority] "; source from c fallback self; } }}Look at the emitted names: each carries key(c), so the case key
becomes part of the stable identifier. The amnesty defeater is
addressable as ResaleFine/excluded/Amnesty/excludes, its priority as
ResaleFine/excluded/Amnesty/priority. Per excluded case, two nodes:
a defeater concluding not result(...), and a priority preferring it
over the main rule.
Excerpt 4 — the sanction instance (lines 118–131): ground
trades_at, one condition (office_open), option Fine, the 2026
window, and the Amnesty case with its premise.
expand sanction ResaleFine { label en unofficial "Resale at an open office draws a fine unless exempt"; bind subject = a: Applicant; bind matter = o: Office; option = Fine; ground = trades_at; conditions = [office_open]; result = fined_for; effective [@2026-01-01, @2027-01-01); case excluded Amnesty { label en unofficial "Amnesty exemption"; premises = [exempt_case]; }}Look at how much this one block fixes: the ground, the condition
list, the option, the 2026 window, the labels, and the Amnesty case
with its premise. Window, labels and exception all come from here —
change the window in this block and all three generated nodes move
together.
Excerpt 5 — the optional-premise template (lines 140–155). The
cause param has type option<relation(subject)>: a relation or
absent. if some cause as g adds the premise only when the instance
passes one; with none the rule fires on the ground alone.
expansion late_fee { params { subject: binder; cause: option<relation(subject)>; ground: relation(subject); result: relation(subject); } exports { applicability = self/applicable; } emit rule self/applicable strict { for subject; when ground(subject); if some cause as g { when g(subject); } then result(subject); scope from self; effective from self; labels from self; source from self; }}Look at the if some cause as g line: the extra premise appears
only when the instance passes a relation. some versus none is
chosen per instance, not per case — the same template produces both a
“needs a record” rule and an “always fires” rule.
Excerpt 6 — the two instances (lines 157–171). WithRecord passes
some repeat_late; FirstNotice passes none.
expand late_fee WithRecord { label en unofficial "late filing draws a fine on repeat"; bind subject = a: Applicant; cause = some repeat_late; ground = files_late; result = late_fined;}
expand late_fee FirstNotice { label en unofficial "first late filing draws a notice"; bind subject = a: Applicant; cause = none; ground = files_late; result = late_noticed;}Look at the cause lines: WithRecord passes some repeat_late,
FirstNotice passes none. Each instance keeps its own label and
result; the point of the pair is the cause premise — present in
one generated rule, absent in the other.
Command and result
Section titled “Command and result”Run the templates suite:
law test packs/examples/language-demo/templatesObserved result (engine law 0.1.0):
law test demo.northbridge.templates: мир demo.northbridge.templates, demo.northbridge.vocabulary ok [demo.northbridge.templates] tests/templates.lawtest / pair: city worker ok [demo.northbridge.templates] tests/templates.lawtest / threshold: two visits ok [demo.northbridge.templates] tests/templates.lawtest / threshold not met ok [demo.northbridge.templates] tests/templates.lawtest / sanction applies ok [demo.northbridge.templates] tests/templates.lawtest / exception lifts the sanction ok [demo.northbridge.templates] tests/templates.lawtest / some-premise without a record does not fire ok [demo.northbridge.templates] tests/templates.lawtest / some-premise with a record fires ok [demo.northbridge.templates] tests/templates.lawtest / none-premise always firesитого: 8 проверено, 8 прошли, 0 не прошли, 0 не исполнены; код 0All eight tests pass: each answer matched its expectation. That says the machine generates and applies the declared templates — it is not a ruling that Northbridge fines the right people. The first line names the world under test: the templates package together with the vocabulary package it is evaluated with. The summary line is in Russian and says that 8 tests were checked, 8 passed, none failed and none were left unexecuted, with exit code 0.
The package also passes the static check:
law engine check packs/examples/language-demo/templates/package.lawcheck OK: packs/examples/language-demo/templates/package.lawcheck OK means the package file is well-formed; the suite run above
is what exercises its behavior.
What the decisive tests assert (evaluate truth(...) on generated
rules):
| Test | Setup | Expects |
|---|---|---|
pair: city worker | employed in the city | worker_permit TRUE_ONLY |
threshold: two visits | on record, year set, two 2026 visits | frequent_filer TRUE_ONLY |
threshold not met | same, but one visit | frequent_filer NEITHER |
sanction applies | trades at an open office, June 2026 | fined_for(..., Fine) TRUE_ONLY |
exception lifts the sanction | same plus exempt_case | fined_for(..., Fine) FALSE_ONLY |
some-premise without a record does not fire | files_late only | late_fined NEITHER |
some-premise with a record fires | files_late plus repeat_late | late_fined TRUE_ONLY |
none-premise always fires | files_late only | late_noticed TRUE_ONLY |
Reading a generated rule back to its source works because every
generated node carries a stable identifier plus an inherited label.
The fine applicator is ResaleFine/applicable, its defeater is
ResaleFine/excluded/Amnesty/excludes, and the repeat-fine rule is
WithRecord/applicable. When a test reports TRUE_ONLY, the emit block
with the matching self/... name is the exact rule that fired.
Why this construct
Section titled “Why this construct”The task is to write each recurring norm shape once and instantiate it with different grounds, conditions, exceptions and thresholds, so a fix — a new exception, a moved window — is made in exactly one place. The repetitions differ in relations and values, not in arithmetic. An expansion binds relation names, binders, lists of premises, cases, integers, windows and options — the very things that vary between the instances. One construct covers all of them.
Copying rules by hand drifts: the amnesty priority is exactly the line a copy forgets. Decision tables from nb-04: The permit fee compress rows sharing one head, but they cannot emit a defeater plus a priority per case, nor switch a premise with some/none. Plain strict rules from nb-01: First permit: facts, a rule and a question cannot abstract over relation names at all.
The proof is the eight tests above: every generated rule — pair,
threshold, sanction applicator, amnesty defeater, both late-fee
variants — is executed by law test, not asserted in prose. The
some/none trio is the sharpest proof: identical ground, three
outcomes from the one differing premise. What the tests do not prove
is that these are the right municipal norms. The tests prove the
machine generates and applies the declared templates; no test can
prove Northbridge fines the right people. All acts are fictional data,
not legal advice.
Changed condition
Section titled “Changed condition”Take some-premise without a record does not fire and add one fact:
repeat_late(ann).
With files_late alone, truth(late_fined(ann)) is NEITHER — the
WithRecord/applicable rule waits on the optional premise. Add the
record and the twin test is TRUE_ONLY. The third test, none-premise always fires, is TRUE_ONLY in both setups: no optional premise to
wait on. The same one-change logic holds for the sanction, where only
exempt_case separates TRUE_ONLY from FALSE_ONLY, and for the
threshold, where one visit is NEITHER and two visits are TRUE_ONLY.
Typical mistake
Section titled “Typical mistake”A natural mistake is to assume the optional premise is satisfied:
“Ann filed late, so the repeat fine applies.” It does not — the query
is NEITHER with files_late on record. cause = some repeat_late
adds an extra premise; some names what the option holds, it does not
waive it.
The fix is to read the two spellings exactly: some X means “and X
must also hold”, none means “no further premise”. The trio holds you
to it — ground alone gives NEITHER for the some rule and TRUE_ONLY
for the none rule.
Limits
Section titled “Limits”Generation happens before evaluation. By the time law test runs,
instances are ordinary rules. Params cannot be rebound at question
time — an expand binds everything up front.
Identifiers are stable, not free text. The defeater’s address contains
the case key (excluded/Amnesty/excludes); renaming the case renames
the node. Quote the full Instance/... path in explanations, never
just “the exception rule”.
Metadata flows downhill. Scope, effective dates, labels and sources
come from the expand site (from self), with case labels prefixed
([excludes], [priority]) or falling back. An instance without a
label generates working rules with poor explanations.
NEITHER is not FALSE_ONLY. A threshold not met, or a some premise
unrecorded, yields NEITHER — nothing concluded. Only an operating
defeater, like the amnesty case, yields FALSE_ONLY.
Verified profile: engine law 0.1.0, semantics law.core/0.2. The
expansion vocabulary (params, exports, self/... identifiers, from self inheritance, some/none options) is a fact about this profile’s
implementation, never a claim about the language in general.
Exercise
Section titled “Exercise”Without running the engine, predict, then check with law test:
- For Ann with
files_lateonly, then withfiles_lateplusrepeat_late: statetruth(late_fined(ann))andtruth(late_noticed(ann))in each setup. Which of the four answers changes, and which generated rule (WithRecord/applicableorFirstNotice/applicable) is responsible for the change? - In
sanction applies, which single added fact flipsfined_for(ann, city-hall, Fine)from TRUE_ONLY to FALSE_ONLY, and what are the stable identifiers of the two generated nodes that produce the flip? - The
ResaleFineinstance declareseffective [@2026-01-01, @2027-01-01). Which generated nodes inherit that window, and what would a December 2027 trade report forfined_for— TRUE_ONLY, FALSE_ONLY, or NEITHER? Why? - In
threshold not met, why is the status NEITHER rather than FALSE_ONLY — and what would have to be added to the template family to ever report FALSE_ONLY for Bob?
Predict first; run the suite; explain any miss in one sentence. Checkable solution: full solution: predictions and checkable answers.
Sources
Section titled “Sources”- Source:
packs/examples/language-demo/templates/package.law(expansionspair,visits_threshold,sanction,late_fee; instancesWorkerPermit,FrequentFiler,ResaleFine,WithRecord,FirstNotice) - Tests:
packs/examples/language-demo/templates/tests/templates.lawtest(the eight tests) - Language reference:
docs/language/09-advanced-cheat-sheet.law.md(expansion definition with thepairexample; the construct glossary entries forexpansion,expand,params,exports,key,list,option,some),docs/language/06-testing-a-package.law.md(thelaw testreference) - Prerequisite: nb-01: First permit: facts, a rule and a question; next: nb-14: Why this answer and what would change it
Three levels:
- Northbridge use (this article): worker-permit pair, two-visit threshold, resale sanction with amnesty, repeat-versus-first late norms — verified by the eight tests above.
- Domain template: whenever one norm shape recurs, write one
expansion with relation, list, case, integer, window and option
params; emit the main rule plus one defeater-plus-priority per
excluded case; bind metadata from the expand site; address
generated rules by their
Instance/...stable identifiers. - Confirmed example elsewhere: the language reference’s own
pairexpansion atdocs/language/09-advanced-cheat-sheet.law.md— the same template-and-instance shape this article scales up with lists, cases and options. - Confirmed external formalization (corpus): offence
qualification and liability (Administrative Offences Code of
Kazakhstan, art. 229, legal-entity parts 1–2) — package
kz.corpus.administrative_code,corpus/laws/kz/codes/administrative-code/83-insurance-payments.law:28-40:expand offence_liability Article229Part1LegalEntitywith a bind/params block, instantiated per part so one expansion shape covers Art229 P1/P2.
Sources and scope of verification
The instance binds subject/act binders, qualifies/liability/actor
relations, element lists, empty condition lists, and guilt = none.
Evidence: the packs/expansions presumption+offence series, selected by
inspection of repeated identical instantiations. The recorded
verification confirms the construct’s presence at the cited lines
only, by direct file read; it makes no claim about deployment,
runtime behaviour, or legal correctness.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.