docs← Back to article

Markdown for LLMs

nb-13 — Repeatable norms without copying

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# 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

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

You need [nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/) 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](/tutorials/northbridge/nb-03-exceptions/) showed
defeasible rules — rules that hold unless defeated — and priorities,
the named orders saying which rule wins a conflict.
[nb-04: The permit fee](/tutorials/northbridge/nb-04-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

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

```law
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.

```law
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.

```law
    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.

```law
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.

```law
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`.

```law
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

Run the templates suite:

```sh
law test packs/examples/language-demo/templates
```

Observed result (engine `law 0.1.0`):

```text
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 не исполнены; код 0
```

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

```sh
law engine check packs/examples/language-demo/templates/package.law
```

```text
check OK: packs/examples/language-demo/templates/package.law
```

`check 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

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](/tutorials/northbridge/nb-04-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](/tutorials/northbridge/nb-01-first-permit/) 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

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

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

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

Without running the engine, predict, then check with `law test`:

1. For Ann with `files_late` only, then with `files_late` plus
   `repeat_late`: state `truth(late_fined(ann))` and
   `truth(late_noticed(ann))` in each setup. Which of the four answers
   changes, and which generated rule (`WithRecord/applicable` or
   `FirstNotice/applicable`) is responsible for the change?
2. In `sanction applies`, which single added fact flips
   `fined_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?
3. The `ResaleFine` instance declares
   `effective [@2026-01-01, @2027-01-01)`. Which generated nodes inherit
   that window, and what would a December 2027 trade report for
   `fined_for` — TRUE_ONLY, FALSE_ONLY, or NEITHER? Why?
4. 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](/tutorials/northbridge/solutions/nb-13-solutions/).

## Sources

- Source: `packs/examples/language-demo/templates/package.law`
  (expansions `pair`, `visits_threshold`, `sanction`, `late_fee`;
  instances `WorkerPermit`, `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 the `pair` example; the construct glossary
  entries for `expansion`, `expand`, `params`, `exports`, `key`,
  `list`, `option`, `some`),
  `docs/language/06-testing-a-package.law.md`
  (the `law test` reference)
- Prerequisite: [nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/); next:
  [nb-14: Why this answer and what would change
  it](/tutorials/northbridge/nb-14-why-this-answer/)

Three levels:

1. **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.
2. **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.
3. **Confirmed example elsewhere:** the language reference's own
   `pair` expansion at
   `docs/language/09-advanced-cheat-sheet.law.md` — the same
   template-and-instance shape this article scales up with lists,
   cases and options.
4. **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 Article229Part1LegalEntity` with a
   bind/params block, instantiated per part so one expansion shape
   covers Art229 P1/P2.

<details>
<summary>Sources and scope of verification</summary>

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.

</details>