Markdown for LLMs
nb-22 — Expansions you can answer about, answers you can explain
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# nb-22 — Expansions you can answer about, answers you can explain
*Northbridge course, advanced ([nb-13](/tutorials/northbridge/nb-13-expansions/) →
[nb-14](/tutorials/northbridge/nb-14-why-this-answer/) →
[nb-22](/tutorials/northbridge/nb-22-expansions-queries/)).
All law is fictional; every desk, surcharge, waiver and billed month is
synthetic and unofficial. No real municipal deployment or legal-validity
claims. Engine `law 0.1.0`, semantics `law.core/0.2`.*
## 1. Situation
The Northbridge inquiry desk handles walk-in questions all day. Its
norms share shapes with different fillings: a repeat inquirer pays a
callback fee, a first-timer gets only a notice, and an inspection held
at an open desk draws a surcharge unless the case was waived. Writing
each norm by hand would mean copying the same rule shape over and
over — so the desk declares each shape once as an **expansion** and
fills it in per case.
The desk must also answer for its answers. A visitor billed for twelve
months asks: is that a big inquiry, and why? A borderline case at six
months asks: what single record would flip the answer? The desk
replies with a status, a **because-list** naming the exact rule that
fired, and — for the borderline case — the cheapest flip with a
replayable certificate.
[nb-13: Repeatable norms without copying](/tutorials/northbridge/nb-13-expansions/)
introduced the expansion; [nb-14: Why this answer and what would
change it](/tutorials/northbridge/nb-14-why-this-answer/) introduced asking, explaining,
properties and the what-if game. This article joins both in one desk:
expansions explained through their generated rules, queries of both
applicable kinds, one property, one finite counterfactual with all
equi-minimal solutions, and rejection probes for two removed
spellings.
## 2. Prerequisites
[nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/): facts, strict rules,
the four truth statuses, `law test`.
[nb-03: Exceptions and conflicting rules](/tutorials/northbridge/nb-03-exceptions/):
defeasible rules, priorities.
[nb-13: Repeatable norms without copying](/tutorials/northbridge/nb-13-expansions/):
expansion, params, expand, exports, identifiers, metadata.
[nb-14: Why this answer and what would change it](/tutorials/northbridge/nb-14-why-this-answer/):
ask, explanation, property, counterfactual, replay.
Each combination below is explained where it first appears:
**applicable parameter classes**, **optional premises**
(`some`/`none` fillings), **explanations of generated rules** (the
because-list naming a `self/...` identifier), **applicable query
kinds** (`Money`, `Integer`), **all equi-minimal solutions**, and
rejection probes for removed spellings.
## 3. Minimal example
All excerpts are from `packs/examples/language-demo/inquiry/package.law`
(identifiers as written; the rest cut), except the last two — whole files.
Excerpt 1 — the callback template (lines 33–49). Repeat visitors and
first-timers both get a callback response, but only the repeat case
depends on a prior record. Watch the `params` block: a `binder` for
the visitor, an `option<...>` premise for the possibly-absent cause,
and two fixed-arity relations for the ground and the result.
```law
expansion callback {
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;
}
}
```
One idea: the `if some cause as g` line makes the extra premise
conditional. When the instance binds `none`, the generated rule has
no such premise at all.
Excerpt 2 — both fillings (lines 51–65). The repeat fee needs the
repeat record; the first-timer notice needs nothing beyond the
filing:
```law
expand callback RepeatFee {
label en unofficial "repeat inquiry draws a callback fee";
bind subject = a: Applicant;
cause = some repeat_inquirer;
ground = inquiry_filed;
result = callback_fee;
}
expand callback FirstNotice {
label en unofficial "first inquiry draws only a notice";
bind subject = a: Applicant;
cause = none;
ground = inquiry_filed;
result = callback_notice;
}
```
One idea: `none` is a real filling, not an omission. The notice
instance needs no `cause` at all and always fires on its ground.
Excerpt 3 — the inspection template head (lines 82–101). An
inspection at an open desk draws a surcharge of a fixed kind, unless
a listed exception applies. Watch the `params` block grow: a second
binder, a `value` for the surcharge kind, a ground plus a `list<...>`
of conditions, a result, and `cases` with non-empty premise lists
(`min 1`).
```law
expansion inspection {
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;
}
```
One idea: `min 0` versus `min 1` is the contract. Conditions may
be empty, but every exception case must list at least one premise.
Excerpt 4 — the exception loop (lines 102–116). Each listed case
emits a defeater plus a priority: `key(c)` names both, and metadata
comes `from c` with fallback to the 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;
}
}
}
```
One idea: the defeater has a stable address. The
`self/excluded/key(c)/excludes` identifier lets explanations and
priorities point at exactly the generated rule they mean.
Excerpt 5 — the inspection instance (lines 118–131). Ann is
inspected at city hall while the desk is open; a waiver would lift
the surcharge:
```law
expand inspection DeskSurcharge {
label en unofficial "inspection at an open desk draws a surcharge unless waived";
bind subject = a: Applicant;
bind matter = o: Office;
option = Surcharge;
ground = inspects_at;
conditions = [desk_open];
result = surcharged_for;
effective [@2026-01-01, @2027-01-01);
case excluded Waiver {
label en unofficial "waiver exemption";
premises = [waived_case];
}
}
```
One idea: the instance binds every parameter class at once —
binders, value, ground, conditions, result, window and the waiver
case — so the generated rules are fully determined.
Excerpt 6 — the two query kinds and the what-if declaration (lines
139–149 and 174–179). The desk doubles the inquiry fee for one
question, counts central residents for another, and declares the
six-month what-if game up front:
```law
query DoubleInquiryFee(months: Integer) -> Money {
let base: Money = demo.northbridge.calculations::permit_fee(months);
return 2 * base;
}
query InquiryCount() -> Integer {
return count(collect all a: Applicant where demo.northbridge.vocabulary::resident(a) and demo.northbridge.vocabulary::lives_in(a, demo.northbridge.vocabulary::central));
}
counterfactual FlipToBigInquiry {
target big_inquiry(6);
mutable inquired_months;
immutable city_tariff;
cost EditInquiryCost;
}
```
One idea: target, moves and prices first, search second. The
`counterfactual` block declares what may change (`mutable`), what
must not (`immutable`) and what changes cost — before any search
runs.
Excerpt 7 — the property (lines 99–102 of the test file
`packs/examples/language-demo/inquiry/tests/inquiry.lawtest`). The
doubled inquiry count must cover the plain count for every recorded
value:
```law
property InquiryDoubledCoversInquiry {
forall m in collect v: Integer where inquired_months(v);
expect double_inquired(m) >= m;
}
```
One idea: one statement, every matching fact. The `forall`
ranges over all collected values; a single counterexample would fail
the property.
Standalone 1 — the archived rejection probe `inquiry/evidence/snippets/concept-migration.law.txt` (not valid current syntax).
`concept` is no longer accepted as a declaration:
```law
language "law.core" version "0.2";
package probe version "0.1.0";
namespace "urn:probe";
entity Applicant;
concept resident(a: Applicant);
```
Standalone 2 — the archived rejection probe `inquiry/evidence/snippets/except-migration.law.txt` (not valid current syntax).
The current spelling is `unless`:
```law
language "law.core" version "0.2";
package probe version "0.1.0";
namespace "urn:probe";
entity Applicant;
relation filed(x: Applicant) kind empirical;
relation covered(x: Applicant) kind institutional;
relation blocked(x: Applicant) kind empirical;
rule CoverOld defeasible {
for x: Applicant;
when filed(x);
except_when blocked(x);
then covered(x);
}
```
## 4. Command and result
Run the inquiry suite from the repository root:
```sh
law test packs/examples/language-demo/inquiry
```
Observed result (engine `law 0.1.0`) — ten tests plus one property, all
green. Every `ok` line means the answer matched its expectation; the
Russian total reads "11 проверено, 11 прошли, 0 не прошли, 0 не
исполнены; код 0": 11 checked, 11 passed, 0 failed, 0 unexecuted, exit
code 0:
```text
law test demo.northbridge.inquiry: мир demo.northbridge.inquiry, demo.northbridge.calculations, demo.northbridge.vocabulary
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / repeat inquiry draws the fee
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / fee needs the repeat record
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / first inquiry draws only a notice
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / desk inspection draws the surcharge
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / waiver lifts the surcharge
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / rule reads the same function as the query
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / query does not invent support
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / count of central residents leads to the callback
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / inquiry fee is non-negative
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / negative inquiry fails the gate
ok [demo.northbridge.inquiry] tests/inquiry.lawtest / urn:law:demo:northbridge:inquiry#InquiryDoubledCoversInquiry
итого: 11 проверено, 11 прошли, 0 не прошли, 0 не исполнены; код 0
```
The last `ok` line is the property — it holds across all matching
facts. The suite also proves the domain non-empty: the gate tests
show the property ranges over real records, not a vacuous set.
The package also checks clean. The questions below run against the
`InquiryCity` case (facts only, [§168.4](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/24-part-xxiii-cases-snapshots-evaluation.ru.md#1684-пакет-дело-и-операция-ask-decision-0165)). First, the twelve-month
question:
```sh
law engine check packs/examples/language-demo/inquiry
law ask packs/examples/language-demo/inquiry_cases --case InquiryCity --query 'evaluate truth(demo.northbridge.inquiry::big_inquiry(12));' --query-id big-inquiry-12 --format json
```
Observed: `"truthStatus":"TRUE_ONLY"` — twelve months at 10 EUR
each clear the 50 EUR bar, so the desk answers that twelve billed
months are a big inquiry. Now the borderline case:
```sh
law ask packs/examples/language-demo/inquiry_cases --case InquiryCity --query 'evaluate truth(demo.northbridge.inquiry::big_inquiry(6));' --query-id big-inquiry-6 --format text
```
Observed: `Не установлено ни что «big_inquiry» (m: 6), ни обратное` —
"it is established neither that big_inquiry holds nor the reverse".
That is NEITHER, with no derivation steps: six months conclude
nothing either way. The desk cannot call it big — but it has not
shown it small.
Explanations read saved proof graphs, so save each answer (`--out`)
first and then explain it. Start with Ann's surcharge:
```sh
law ask packs/examples/language-demo/inquiry_cases --case InquiryCity --query 'evaluate truth(demo.northbridge.inquiry::surcharged_for(entity_ref("urn:demo:northbridge:ann"), entity_ref("urn:demo:northbridge:city-hall"), demo.northbridge.inquiry::Surcharge));' --query-id surcharge-ann --format json --out /tmp/nb22-ask-surcharge
law engine explain /tmp/nb22-ask-surcharge --json
```
Observed: `"conclusion":"surcharged_for(ann, city-hall, surcharge):
TRUE_ONLY"` with `"because":["urn:proof:apply:DeskSurcharge/applicable:...",
"urn:proof:assert:...#insp","urn:proof:assert:...#open"]`. Three steps:
the generated rule `DeskSurcharge/applicable`, the inspection record
(`#insp`) and the open-desk record (`#open`). The first step names the
generated rule — expansions explain through their instances, not
through the template. The plain rule explains the same way
(`BigInquiry` plus `i12`):
```sh
law ask packs/examples/language-demo/inquiry_cases --case InquiryCity --query 'evaluate truth(demo.northbridge.inquiry::big_inquiry(6));' --query-id big-inquiry-6 --format json --out /tmp/nb22-ask6
law engine explain /tmp/nb22-ask6 --json
```
Observed: `"conclusion":"big_inquiry(6): NEITHER"` with `"because":[]`.
Nothing fired, so there is nothing to list — an empty because-list is
what NEITHER looks like under explanation.
The what-if runs against the frozen exhibits in
`packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/`.
Ask what single record would flip the six-month case:
```sh
law engine counterfactual --program packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input.json
```
Observed: `"status":"FOUND"`,
`"solverProfile":"law.solver.finite-enumeration/0.1"`, one solution —
`...#add-i6` at `"cost":"1"`, `"cutoffCost":"1"`, one candidate checked.
The cheapest flip is adding the six-month record at cost 1, and the
cutoff of 1 certifies that nothing cheaper exists. With two
equal-cost candidates the solver admits the tie instead of picking
silently:
```sh
law engine counterfactual --program packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input-two.json
```
Observed: `"status":"FOUND"` with both solutions, each at cost `1`,
`"cutoffCost":"1"`, two candidates checked. Both cheapest flips are
listed; the cutoff still certifies minimality. The desk ships the
flip with a replay certificate — check it:
```sh
law engine counterfactual --program packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input.json --verify packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-cf-result.json
```
Observed:
`{"kind":"counterfactual-verification","result":"urn:law:demo:northbridge:inquiry#FlipInquiryInput01/result","valid":true}`.
The flip replays to its certificate: the answer can be trusted
without re-running the search.
A certificate pins the exact program it was computed against.
`programHash` is the *semantic* hash of the linked world IR —
directly observable, and equal to the value pinned in both
`nb-input.json` and `nb-cf-result.json`. See it for yourself:
```sh
law engine semhash packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json
```
Observed: `sha256:cfe2fcb840dc20acc62c87636904558c9cb4066d37de132863db1c2478ec0409`,
exit 0. It is not the file's byte hash. Re-serialize the world with
different whitespace and the hash stands still:
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
json.dump(w, open('/tmp/nb22-stale/world-ws.json', 'w'), indent=1, ensure_ascii=False)
EOF
law engine semhash /tmp/nb22-stale/world-ws.json
```
Observed: still `sha256:cfe2fcb8…`, exit 0 — canonicalization
makes byte layout irrelevant. Same for a metadata-only annotation:
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
w['metadata'] = {'note': 'stale-cert probe'}
json.dump(w, open('/tmp/nb22-stale/world-meta.json', 'w'), ensure_ascii=False)
EOF
law engine semhash /tmp/nb22-stale/world-meta.json
```
Observed: still `sha256:cfe2fcb8…`, exit 0 — `metadata` sits
outside the semantic preimage. The minimal change that really moves
the hash is one Money literal in the `BigInquiry` rule node
(`50 EUR` → `51 EUR`, the `>= 50 EUR` line of the inquiry world):
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
n = [x for x in w['nodes'] if x.get('id') == 'urn:law:demo:northbridge:inquiry#BigInquiry'][0]
n['body']['items'][1]['right']['value'] = '51'
json.dump(w, open('/tmp/nb22-stale/world-threshold.json', 'w'), ensure_ascii=False)
EOF
law engine semhash /tmp/nb22-stale/world-threshold.json
```
Observed:
`sha256:8b4127d767a8cd3fb715203d311e58f2c704c30ab1cd9d59868a6bd9a0f00bbf`,
exit 0. One literal moved the hash: the program is semantically a
different world now. Re-verifying the *old* certificate against the
changed program is refused before any reasoning, with both hashes
named:
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
n = [x for x in w['nodes'] if x.get('id') == 'urn:law:demo:northbridge:inquiry#BigInquiry'][0]
n['body']['items'][1]['right']['value'] = '51'
json.dump(w, open('/tmp/nb22-stale/world-threshold.json', 'w'), ensure_ascii=False)
EOF
law engine counterfactual --program /tmp/nb22-stale/world-threshold.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input.json --verify packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-cf-result.json
```
Observed (stdout, stderr empty, exit 1):
`{"error":{"code":"COUNTERFACTUAL_PROGRAM_HASH_MISMATCH","message":"counterfactual
input programHash is sha256:cfe2fcb840dc20acc62c87636904558c9cb4066d37de132863db1c2478ec0409,
actual is sha256:8b4127d767a8cd3fb715203d311e58f2c704c30ab1cd9d59868a6bd9a0f00bbf"}}`.
That refusal is only the first of two layers. Renew the input
envelope for the new program — re-pin `programHash`, re-seal
`contentHash` via `law engine canon` (a recipe that reproduces the
frozen `8133f6f0…` envelope hash exactly, so it is trusted for the
new one) — but present the *old* result, and the replay layer
refuses instead:
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
n = [x for x in w['nodes'] if x.get('id') == 'urn:law:demo:northbridge:inquiry#BigInquiry'][0]
n['body']['items'][1]['right']['value'] = '51'
json.dump(w, open('/tmp/nb22-stale/world-threshold.json', 'w'), ensure_ascii=False)
inp = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input.json'))
inp['programHash'] = 'sha256:8b4127d767a8cd3fb715203d311e58f2c704c30ab1cd9d59868a6bd9a0f00bbf'
json.dump({k: v for k, v in inp.items() if k != 'contentHash'}, open('/tmp/nb22-stale/input-new-nohash.json', 'w'), ensure_ascii=False)
json.dump(inp, open('/tmp/nb22-stale/input-new-tmp.json', 'w'), ensure_ascii=False)
EOF
CH=$(law engine canon /tmp/nb22-stale/input-new-nohash.json | sha256sum | cut -d' ' -f1)
python3 - <<EOF
import json
inp = json.load(open('/tmp/nb22-stale/input-new-tmp.json'))
inp['contentHash'] = 'sha256:$CH'
json.dump(inp, open('/tmp/nb22-stale/input-new.json', 'w'), indent=1, ensure_ascii=False)
EOF
law engine counterfactual --program /tmp/nb22-stale/world-threshold.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input /tmp/nb22-stale/input-new.json --verify packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-cf-result.json
```
Observed (exit 1):
`{"error":{"code":"COUNTERFACTUAL_CERTIFICATE_INVALID","message":"counterfactual
result does not replay to the claimed canonical certificate"}}`.
Pin check first, canonical replay comparison second: a certificate
cannot be accepted piecemeal, and it cannot silently survive a
program change even when the outcome is the same.
The fix is mechanical — re-obtain the certificate against the
changed program with stock tools only. Solve afresh, then verify
the fresh result:
```sh
mkdir -p /tmp/nb22-stale
python3 - <<'EOF'
import json
w = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-world.json'))
n = [x for x in w['nodes'] if x.get('id') == 'urn:law:demo:northbridge:inquiry#BigInquiry'][0]
n['body']['items'][1]['right']['value'] = '51'
json.dump(w, open('/tmp/nb22-stale/world-threshold.json', 'w'), ensure_ascii=False)
inp = json.load(open('packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-input.json'))
inp['programHash'] = 'sha256:8b4127d767a8cd3fb715203d311e58f2c704c30ab1cd9d59868a6bd9a0f00bbf'
json.dump({k: v for k, v in inp.items() if k != 'contentHash'}, open('/tmp/nb22-stale/input-new-nohash.json', 'w'), ensure_ascii=False)
json.dump(inp, open('/tmp/nb22-stale/input-new-tmp.json', 'w'), ensure_ascii=False)
EOF
CH=$(law engine canon /tmp/nb22-stale/input-new-nohash.json | sha256sum | cut -d' ' -f1)
python3 - <<EOF
import json
inp = json.load(open('/tmp/nb22-stale/input-new-tmp.json'))
inp['contentHash'] = 'sha256:$CH'
json.dump(inp, open('/tmp/nb22-stale/input-new.json', 'w'), indent=1, ensure_ascii=False)
EOF
law engine counterfactual --program /tmp/nb22-stale/world-threshold.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input /tmp/nb22-stale/input-new.json > /tmp/nb22-stale/result-new.json
law engine counterfactual --program /tmp/nb22-stale/world-threshold.json --case packs/examples/language-demo/inquiry/counterfactual/inquiry-flip/nb-case.json --input /tmp/nb22-stale/input-new.json --verify /tmp/nb22-stale/result-new.json
```
Observed: solve reports `"status":"FOUND"`, solution
`...#add-i6` at cost `1` (new `contentHash sha256:9e74d3f3…`);
`--verify` reports `"valid":true`, exit 0. Same verdict, new
hashes, fresh certificate — old certificates stay invalid by
design. The stale program now has a certificate of its own, and
the two worlds can no longer share paperwork.
Finally, the archived source files serve as rejection probes for
the two removed spellings:
```sh
law engine check packs/examples/language-demo/inquiry/evidence/snippets/concept-migration.law.txt
law engine check packs/examples/language-demo/inquiry/evidence/snippets/except-migration.law.txt
```
The warning and `check OK` outputs recorded for engine `law 0.1.0`
are historical. The removed spellings are rejected by the current
compiler and must not be used in new packages; the probes remain
only as evidence of the former migration phase. Neither file may
serve as a template for new norms.
## 5. Why this construct
The desk declares each norm shape once. Applicable parameter
classes say in one `params` block what an instance supplies: binders,
fixed-arity relations, `value`, `integer`/`window`, `list`, `cases`,
`option`. The inspection instance binds every class at once and the
suite stays 11/11.
Optional premises let a rule fire on a record when it exists and
without one when `none` is bound. The fee needs its repeat record
(without it, NEITHER); the notice always fires on its ground alone.
Exports and identifiers address generated rules from outside.
`exports` publishes `self/applicable`; `key(c)` addresses each
defeater and priority. The surcharge explanation names
`DeskSurcharge/applicable` to prove it.
Metadata carries scope, dates, labels and sources from the site (and
per case) into every generated node. The 2026 window bounds the
surcharge tests; the waiver case carries its `[excludes]` prefix.
Applicable query kinds compute `Money` and `Integer` values once
instead of copying arithmetic into rules. Both reader rules derive
through the same `permit_fee` (12 → TRUE_ONLY).
The property holds one claim across all matching facts — the
eleventh `ok` line — while the gate tests prove the domain
non-empty. The finite counterfactual names the cheapest flip with a
price tag: `add-i6` at cost 1 with `cutoffCost 1`. Equally-minimal
solutions admit ties honestly: both cost-1 solutions listed,
`cutoffCost 1`, never a silent pick.
The replay certificate lets the desk trust the flip without
re-running the search: `--verify` reports `valid:true` on pinned
hashes. Migration warnings keep old spellings loud: E1328 on both
snippets, yet `check OK` and no norm executes.
<details>
<summary>Why not plain hand-written tests?</summary>
Hand-written tests assert conclusions, but they never prove
minimality, list ties, or explain generated rules by identifier —
`cutoffCost` plus the because-list do.
</details>
The proof is the suite, the asks, the explanations, the FOUND pair,
the replay and both warnings: each executed above, none asserted in
prose.
What is not proven: that six months *should* be big, that cost 1 is
fair, or that the waiver is wise. The rest is fictional data.
## 6. Changed condition
File the waiver and the surcharge flips. `inspects_at` plus
`desk_open` with no waiver gives TRUE_ONLY through
`DeskSurcharge/applicable`; add the single `waived_case` fact and
the same question is FALSE_ONLY through
`DeskSurcharge/excluded/Waiver/excludes` winning by priority. One
added assertion, two statuses — because the defeater fires only on
its premise, and the priority prefers it over the base rule.
The six-month question is the same logic as a what-if: NEITHER
today, TRUE_ONLY after adding `inquired_months(6)`.
## 7. Typical mistake
The mistake is calling the query declaration as if it were a
question — asking `law ask` to evaluate the query itself:
```sh
law ask packs/examples/language-demo/inquiry_cases --case InquiryCity --query 'evaluate demo.northbridge.inquiry::DoubleInquiryFee(12);' --query-id try-query-call --format text
```
Observed: `{"error":{"code":"LDC-E1105","message":"...
DoubleInquiryFee: символ не экспортирован пакетом ..."}}` — "the symbol
is not exported by the package". A `query` is a declaration, not a
relation and not a term: not askable, not usable in rule conditions
(`LDC-E2105`), not cross-package addressable.
The fix is to ask through a reader rule instead. The computation
inside the query runs only through reader rules like `BigInquiry` —
`fee(...)` is the function, `DoubleInquiryFee` is the query, and the
query is never askable through `truth(...)`.
## 8. Limits
Expansions generate; they do not reason. A missing `key(c)` would
collapse two defeaters into one address, with no error to warn you.
Queries invent no support. One recorded month yields NEITHER for
the big-inquiry question, never FALSE_ONLY: the machine concludes
nothing either way rather than establishing the negation.
The boundaries are profile facts. `LDC-E1105`/`LDC-E2105`, the
`property-expect` `E1105/E2403` restrictions, the `E1328` warnings,
and the solver profile describe this implementation (`law 0.1.0`,
`law.core/0.2`), never the language in general.
Finite search needs a finite game. The solver checks the listed
candidates; it never invents new ones.
Certificates pin the semantic program, not its bytes. `--verify`
refuses a stale `programHash` before any reasoning — but whitespace
and metadata never move that hash. Only a real program change does,
and one Money literal suffices. Old certificates stay invalid after
renewal by design.
The observed refusal stands: calling a `query` from `law ask` is
`LDC-E1105`.
## 9. Exercise
Predict each answer without running the engine, then check with
the commands above:
1. `some repeat_inquirer` versus `none`: which filling needs a record,
and which status does the fee test report without one?
2. Which three proof steps would `explain` list for the surcharge, and
why is the waiver defeater absent?
3. Two cost-1 candidates: how many solutions, what `cutoffCost`, and what
does `--verify` report?
4. `law engine check` over each migration snippet: which code, and does
the file still pass?
5. Three world edits — re-indent the JSON, add a top-level
`metadata` note, change the `BigInquiry` `50` to `51`: predict
which move `programHash`, then run the three `semhash`
commands. Then predict the stale `--verify` diagnostic code
and exit code before running it.
Write each prediction first, run the commands, and explain any
miss in one sentence. Checkable solution:
[full solution with checkable answers](/tutorials/northbridge/solutions/nb-22-solutions/).
## 10. Sources
- Source: `packs/examples/language-demo/inquiry/package.law` (both
expansions, both queries, the reader rules, the counterfactual)
- Tests: `packs/examples/language-demo/inquiry/tests/inquiry.lawtest`
- Case and exhibits: `inquiry_cases/` (InquiryCity),
`inquiry/counterfactual/inquiry-flip/`, `inquiry/evidence/snippets/`
- Suite tour: `packs/examples/language-demo/README.md`
- Language reference: `docs/language/09-advanced-cheat-sheet.law.md`,
`docs/language/06-testing-a-package.law.md`
- Prerequisite: [nb-01: First permit: facts, a rule and a question](/tutorials/northbridge/nb-01-first-permit/),
[nb-03: Exceptions and conflicting rules](/tutorials/northbridge/nb-03-exceptions/),
[nb-13: Repeatable norms without copying](/tutorials/northbridge/nb-13-expansions/),
[nb-14: Why this answer and what would change it](/tutorials/northbridge/nb-14-why-this-answer/)
Three levels:
1. **Northbridge use** (this article): one desk writes each norm shape
once, answers with status plus because-list (including through
generated rules), guarantees a doubling property, quotes the cheapest
flip with a replay certificate, lists every tied flip, and preserves
the former spelling probes as rejection cases — verified by the
suite, the asks, the explanations, the FOUND pair and the replay
above.
2. **Domain template:** declare each shape once; publish generated rules
through exports and stable identifiers; explain from the proof graph;
state universal claims as properties; declare what-ifs with target,
mutables and cost model, and ship every flip with its replay check.
Never call a `query` where a relation is expected; never write a
rejected legacy probe as a norm.
3. **Confirmed example elsewhere:** the templates package (8/8) and
the queries package use the same shapes and machinery.
4. **Confirmed external formalization (corpus):** pledge successor
duties (Civil Code of Kazakhstan, art. 323) — package
`kz.corpus.civilcode`,
`corpus/laws/kz/codes/civil-code/tests/323-art323-successor-bears-duties-by-default.lawtest:10-13`:
the canonical `truth` expectation triple (`result_kind`,
`truth_status`, `evaluation_status` — all three axes) behind
`evaluate truth(successor_bears_all_pledgor_duties(...))`, the same
triple the art. 192/160/163 scenarios repeat. Evidence:
`docs/research/constructs/24-queries/corpus-forms.en.md` [§2](/tutorials/northbridge/nb-01-first-permit/#2-prerequisites)
(rated exemplary). Limit of verification: presence of the named
construct at the cited lines only, confirmed by direct file read;
no claim about deployment, runtime behaviour, or legal
correctness.