nb-18 — Vocabulary and package composition
Northbridge course, foundations branch (needs beginner only:
nb-01: First permit: facts, a rule and a question).
All law is fictional; every applicant, clerk, counter, slip and coin
is synthetic and unofficial. No real municipal deployment or
legal-validity claims. Tool law 0.1.0, language version 0.2,
semantics law.core/0.2, std 0.2.0 (from law --version, quoted
below).
1. Situation
Section titled “1. Situation”Talgat, the new Northbridge night clerk, inherits a drawer of loose
concepts: applicants, counters, desks, shifts, payment slips, a duty
roster — and three older packages (vocabulary, permits,
calculations) that already define half of them. His task is to
assemble one new package, demo.northbridge.composition, that
reuses the shared vocabulary instead of redefining it, declares its
own local kinds, states which symbols outsiders may touch, and keeps
one internal note private. The auditor’s question is blunt: which
name means what, who may read it, and what does the machine refuse
— and is each refusal a property of our build or of the language
itself?
A package is the unit of reuse: a manifest, .law files, a
pinned law.lock, and deps/. It carries a namespace, its
stable identity, and it reads other packages through imports —
but only by qualified names, and only to declarations marked
pub.
Inside the new package, each kind of thing gets the declaration
that fits it. A shared sort keeps one identity through a type
alias, a local nickname for the foreign type. A new sort of thing
is an entity, a nominal sort: a Counter is never an
Applicant. Fixed menus are enums, and a map wires two
enums together as one total table. A record bundles explicitly
named fields, and a role names a capacity an actor-subtype
holds over an interval. Open-ended values, like payment
instruments, live in the Option domain.
Two more pieces complete the picture. A pub facts group
asserts package-level facts that hold in the package’s own world
without any test given. And truth statuses work as in
nb-01: First permit: facts, a rule and a question:
TRUE_ONLY means established, NEITHER means established neither
way.
2. Prerequisites
Section titled “2. Prerequisites”nb-01: First permit: facts, a rule and a question:
facts, strict rules, the four truth statuses, and law test as the
way to check a claim.
New here: type X = pkg::Y (alias), pub / private visibility,
and use self::{...} — the per-file declaration of cross-file
names that an explicit package demands. You will also meet two
refusal codes: LDC-E1105 (a read of a non-exported symbol is
refused) and LDC-E1116 (an undeclared cross-file reference is
refused). Both are implementation refusals of this build, not
language-wide inability claims.
3. Minimal example
Section titled “3. Minimal example”Excerpts 1–5 are from
packs/examples/language-demo/composition/package.law
(identifiers as written; rules cut where noted). Standalone A is
the complete second file of the package, desk.law. Standalone
B — the complete scratch consumer — appears once, as the replay
heredoc in §4, and is not repeated here.
Excerpt 1 — header, import and aliases (lines 4–11). One idea: language, name, namespace, dependency, two local nicknames.
language "law.core" version "0.2";package demo.northbridge.composition version "0.1.0";namespace "urn:law:demo:northbridge:composition";import demo.northbridge.vocabulary version "0.1.0";type Applicant = demo.northbridge.vocabulary::Applicant;type Zone = demo.northbridge.vocabulary::Zone;Look at the import line first: it names the dependency and pins
its version. The two type lines then give those foreign sorts
short local names. Inside this package, Applicant is
vocabulary’s applicant — one identity, not a copy.
Excerpt 2 — entity, enums and map (lines 13–28). One idea: a new nominal sort plus two closed menus wired by a total table.
pub entity Counter;pub enum Desk { open; busy;}pub enum Shift { day; night;}pub map DeskShift : Desk -> Shift { open => day; busy => night;}Counter is a fresh nominal sort: no applicant fact can ever
fill a counter position. The two enums list every member up
front — Desk has open and busy, nothing else — and the map
routes each desk state to exactly one shift. Every arm is visible
in one table.
Excerpt 3 — record, role and Option (lines 30–39). One idea: named fields, an actor-subtype capacity, one named coin.
pub record Slip { who: Text; months: Integer;}entity Clerk : Actor;role OnDuty for Clerk;const Cash: Option = entity_ref("urn:demo:northbridge:cash");The record names both its fields — who and months — and
section 7 shows what happens when one goes missing. Clerk is
declared as a subtype of Actor, and the role OnDuty names a
capacity a clerk can hold. Cash is one named inhabitant of the
open Option domain: unlike an enum, the domain stays open to
further values.
Excerpt 4 — the visibility split (lines 49–54). One idea: pub
surface and one exported group against a single private note.
pub relation resident_seen(a: Applicant) kind institutional;relation internal_note(a: Applicant) kind institutional;pub facts CounterFacts { assert "c-desk-1": counter_open(entity_ref("urn:demo:northbridge:desk-1")) { origin case_input; }}Compare the two relations: resident_seen is pub, while
internal_note has no marker and stays private. Section 4 shows
the machine enforcing that split. The pub facts group asserts
that desk-1’s counter is open — a fact the package’s own world
sees without any test supplying it.
Excerpt 5 — consumer rule over the import (lines 56–62). One idea: qualified vocabulary reads joined with the exported fact.
rule CounterReady strict { for a: Applicant; for c: Counter; when demo.northbridge.vocabulary::resident(a) and counter_open(c); then counter_ready(a, c);}The when line joins two worlds: a fully qualified read of
vocabulary’s resident and the local counter_open fed by the
facts group above. Because the alias keeps one applicant
identity, both sides join on the same a.
Standalone A — full content of desk.law (20 lines): the second
file. One idea: every borrowed name declared up front.
language "law.core" version "0.2";package demo.northbridge.composition version "0.1.0";namespace "urn:law:demo:northbridge:composition";use self::{ Desk, DeskShift, Shift,};relation shift_posted(d: Desk) kind empirical;relation shift_ok() kind institutional;rule ShiftOk strict { for d: Desk; when shift_posted(d) and DeskShift(d) == day; then shift_ok();}The use self block at the top is the point of this file: every
name borrowed from package.law — Desk, DeskShift, Shift
— is declared up front. The rule then reads the map as a term,
comparing DeskShift(d) against day. Section 4 shows what
happens when one borrowed name is missing from that list.
4. Command and result
Section titled “4. Command and result”Run the package suite:
law test packs/examples/language-demo/compositionObserved (tail; every one of the 10 tests reports ok above
it — consumer use, silence, record pair, map pair, role pair,
Option pair):
итого: 10 проверено, 10 прошли, 0 не прошли, 0 не исполнены; код 0All 10 tests pass. The summary is in Russian: 10 checked, 10
passed, 0 failed, 0 skipped, exit code 0. Each pair covers one
construct from section 3 — records, the map, the role, and
Option — plus the consumer rule over the import and a silence
case. A passing test means the answer matched its expectation; it
does not mean the roster is fair or the coin real.
Check the package statically and confirm its borrow lists are in canonical form:
law engine check packs/examples/language-demo/compositionObserved: check OK: packs/examples/language-demo/composition.
law fix imports ./packs/examples/language-demo/compositionObserved:
./packs/examples/language-demo/composition блоки `use self` каноническиеThe static check passes, and the second verdict — in Russian,
“use self blocks are canonical” — means the borrow lists need
no rewriting: desk.law declares exactly the names it borrows,
in the expected form.
Record the pinned profile behind every verdict above:
law --versionObserved:
law 0.1.0семантика: law.core/0.2std для языка 0.2: 0.2.0хэш бинаря: sha256:78dea06ce928547e87bd9875a2556cc663def37cbb78c8b04aa0da57839c95d7The Russian lines name the semantics (law.core/0.2), the
standard library for language 0.2 (0.2.0), and the binary hash.
Every status in this article holds for exactly this build.
The use self file-block rule is a whole-run refusal: one
missing declaration fails the entire run, not just one test.
Append a probe rule that uses two undeclared names to desk.law,
run, then revert, so the repo file is untouched afterwards:
cp packs/examples/language-demo/composition/desk.law /tmp/nb18-desk.bakcat >> packs/examples/language-demo/composition/desk.law <<'EOF'
relation probe_e1116() kind institutional;
rule ProbeE1116 strict { for a: Applicant; when resident_seen(a); then probe_e1116();}EOFlaw test packs/examples/language-demo/compositionObserved (frozen as
packs/examples/language-demo/composition/evidence/ev-use-self.txt):
law test: ОТКАЗ LDC-E1116: /Users/rifatjumagulov/Downloads/law-dsl/packs/examples/language-demo/composition/desk.law:26:12: error LDC-E1116: `Applicant` объявлена в файле /Users/rifatjumagulov/Downloads/law-dsl/packs/examples/language-demo/composition/package.law и не заявлена в `use self` файла /Users/rifatjumagulov/Downloads/law-dsl/packs/examples/language-demo/composition/desk.law: в пакете с `local_imports = "explicit"` ссылка на декларацию другого файла заявляется (§23.2)cp /tmp/nb18-desk.bak packs/examples/language-demo/composition/desk.lawlaw test packs/examples/language-demo/compositionObserved: 10 проверено, 10 прошли, 0 не прошли, exit 0 — the
revert is byte-identical and the suite is green again. The
refusal above names Applicant, the first undeclared name
(resident_seen is undeclared too): one gap fails the whole run,
and the diagnostic quotes only the first gap it meets. In
Russian, it says that Applicant is declared in
package.law but not in the use self block of desk.law,
which an explicit package requires (§23.2).
The second refusal guards the package boundary: a private symbol
cannot be read from outside. Replay it with a scratch consumer in
/tmp; the repo tree stays untouched. First the three files —
manifest, Standalone B (the complete consumer package.law: a
foreign package importing composition and reaching for its
private note), and a one-probe suite:
rm -rf /tmp/nb18-consumer && mkdir -p /tmp/nb18-consumer/tests /tmp/nb18-consumer/deps && cat > /tmp/nb18-consumer/law.toml <<'EOF'[package]name = "probe.consumer"version = "0.1.0"language = "0.2"namespace = "urn:probe:consumer"
[dependencies]"demo.northbridge.composition" = "0.1.0""demo.northbridge.vocabulary" = "0.1.0"
[features]semanticLayer = "L1"
[authoring]local_imports = "explicit"
[[tests]]family = "probe.consumer"suites = ["tests/consumer.lawtest"]world = ["probe.consumer", "demo.northbridge.composition", "demo.northbridge.vocabulary"]EOFcat > /tmp/nb18-consumer/package.law <<'EOF'language "law.core" version "0.2";package probe.consumer version "0.1.0";namespace "urn:probe:consumer";
import demo.northbridge.composition version "0.1.0";
relation probe_hit() kind institutional;
rule ProbeHit strict { when demo.northbridge.composition::internal_note(entity_ref("urn:demo:northbridge:ann")); then probe_hit();}EOFcat > /tmp/nb18-consumer/tests/consumer.lawtest <<'EOF'language "law.core" version "0.2";package probe.consumer version "0.1.0";namespace "urn:probe:consumer";
test "probe" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } } evaluate truth(probe_hit()); expect truth_status == NEITHER;}EOFThen pin its world with the repo tooling (run from the repo root; only the Python standard library plus the repo’s own resolver modules — no new files):
python3 - <<'PYEOF'import importlib.util, sys, tomllibfrom pathlib import PathROOT = Path.cwd()sys.path.insert(0, str(ROOT / "engines" / "lawref"))sys.path.insert(0, str(ROOT / "corpus"))spec = importlib.util.spec_from_file_location("check_primitives", ROOT / "verify/ci/gates/differential/check_primitives.py")CP = importlib.util.module_from_spec(spec)spec.loader.exec_module(CP)from lawref import resolverfrom lawref.canon import canonical_bytesDEMO = ROOT / "packs" / "examples" / "language-demo"def roots(): got = CP.package_dirs(ROOT / "packs" / "primitives") + CP.package_dirs(ROOT / "packs" / "units") r = {CP.package_name(p): p for p in got} for toml in DEMO.glob("*/law.toml"): r[tomllib.loads(toml.read_text(encoding="utf-8"))["package"]["name"]] = toml.parent return rCP.package_roots_by_name = rootsCP.dependency_roots_by_name = rootsbinary = ROOT / "engines" / "lawc" / "target" / "gate" / "law-cli"consumer = Path("/tmp/nb18-consumer")manifest = resolver.parse_manifest((consumer / "law.toml").read_bytes())deps = CP.consumer_deps(binary, consumer)assert deps is not None, CP.FAILURESregistry = {(ir["package"]["name"], ir["package"]["version"]): resolver.content_descriptor(ir) for ir in deps.values()}lock = resolver.generate_lock(manifest, registry, resources=resolver.discover_resources(consumer))(consumer / "law.lock").write_bytes(canonical_bytes(lock, document=False) + b"\n")for dep_name, ir in deps.items(): (consumer / "deps" / f"{dep_name}.lawir.json").write_bytes(canonical_bytes(ir, document=False))print("consumer lock+deps regenerated")PYEOFThen:
law test /tmp/nb18-consumerObserved (frozen as
packs/examples/language-demo/composition/evidence/ev-private.txt):
law test: ОТКАЗ LDC-E1105: /private/tmp/nb18-consumer/package.law:10:10: error LDC-E1105: demo.northbridge.composition::internal_note: символ не экспортирован пакетом (§24: межпакетные ссылки — только на `pub`-декларации; экспортировано: 19 символов)Exit code 2. In Russian, the refusal says that
demo.northbridge.composition::internal_note is not exported by
its package: cross-package references may only reach pub
declarations (§24), and the package exports 19 symbols. The probe
never runs — the read is refused before evaluation.
The same consumer rewritten over the pub counter_open builds
and passes 1/1, which proves the boundary is the only gate. But
note what its probe answers: NEITHER. The exported
CounterFacts feed the composition suite world — the first suite
test asserts no counter fact in given and still sees it — not
the foreign consumer world. Exported facts travel with their own
world; visibility lets the consumer read the relation, not the
facts.
What the lock-pinning step does
The Python step regenerates the scratch consumer’s law.lock
and deps/ with the repository’s own resolver modules, so the
consumer’s world is pinned exactly like a real package build.
It prints consumer lock+deps regenerated and writes only under
/tmp/nb18-consumer. The refusal itself comes from law test,
not from the pinning.
5. Why this construct
Section titled “5. Why this construct”Which name means what. Talgat’s drawer stops being loose
concepts once each declaration is read for what it says. Counter
is an entity, so no Applicant fact can ever fill a counter
position — the no resident, no readiness test stays NEITHER
on the counter side alone. Desk and Shift are closed menus:
open and busy, nothing else. Slip carries its fields
explicitly (who, months). And Cash is one named inhabitant
of the open Option domain, which stays open to further values.
Why an alias. Applicant inside composition is
vocabulary’s applicant, so resident(a) and counter_ready(a, c) join on the same a and the consumer test answers
TRUE_ONLY. Redefining the sort locally would compile but
silently fork identity: vocabulary facts would never join with
composition rules, and no refusal would warn you.
Why a map. One total table replaces a rule per arm. ShiftOk
reads DeskShift as a term instead of branching on desk states.
Two members work today; twenty would rot as rules, while the map
keeps every arm in one place.
Why a role. The person is not the capacity. Filing a roster
entry alone yields nothing; with has_role(... OnDuty ...) the
rule fires. The role test pair pins both halves.
Why pub. Nineteen symbols are touchable from outside, and
internal_note is not one of them: it is derived inside the
package but invisible across the boundary (LDC-E1105 above),
while the counter_open mirror over a pub relation builds and
passes.
Why use self. desk.law borrows three names and declares
three; the fourth, undeclared name fails the whole run
(LDC-E1116). Merging both files into one would dodge the borrow
list — and the guarantee with it.
What proves all this: the 10/10 suite, check OK, the
canonical borrow-list verdict, and the two quoted refusals — each
executed in section 4. What is not proven: that the roster is
fair or the coin real. The machine proves which names resolve and
which reads are allowed; policy stays with the office, and the
office is fiction.
6. Changed condition
Section titled “6. Changed condition”Change one token in the desk test’s given:
shift_posted(open) becomes shift_posted(busy). The rule is the
same, the map is the same — but shift_ok() flips from
TRUE_ONLY to NEITHER with COMPUTED. Both suite tests pass
in section 4, so both answers are pinned.
Why the flip? ShiftOk requires DeskShift(d) == day, and the
map routes busy to night. A total map leaves no member
unrouted: busy is decisively not a day shift, so the negative
case is as decisive as the positive one. The answer is silence
(NEITHER), not denial — no rule derives not shift_ok().
7. Typical mistake
Section titled “7. Typical mistake”The mistake is dropping a record field and trusting the rest to
carry the meaning. Take the suite’s record assert and remove
months: 12:
Sketch — the mutated line for a /tmp scratch copy of the suite
(repo untouched); observed FAIL with TYPE_ERROR, exit 1, quoted
below (reproduced 2026-10-02).
assert "s1": filed_slip(Slip { who: "ann" }) { origin case_input; }This failing result is the expected demonstration: run the
mutated line on a scratch copy in /tmp (repo suite untouched)
and the record test FAILs — truth_status is NEITHER, but
evaluation_status is TYPE_ERROR, not COMPUTED, and the
suite exits 1.
An incomplete Slip is ill-typed at evaluation, so the rule
never opens: the assert contributes no usable record. The fix is
to name every field, always — a record literal with all fields is
well-typed, and only then can the rule read it.
8. Limits
Section titled “8. Limits”Verified profile only. Every status above holds for tool
law 0.1.0, language 0.2, semantics law.core/0.2, std
0.2.0. Newer builds and other versions have their own
visibility and authoring rules; re-run, do not assume.
Refusals are profile facts. E1105 (only pub crosses a
package boundary) and E1116 (only declared names cross a file
boundary in an explicit package) describe this build’s
§23–§24 enforcement. Neither states that private notes or
undeclared borrows are inexpressible in general.
Exported facts feed their own world. CounterFacts fires
the suite with no given, while the foreign probe stays
NEITHER — an observed boundary of this profile, not a
cross-version rule.
E1116 names the first gap (Applicant at 26:12), not
every gap. Fix one, re-run, and read the next diagnostic.
9. Exercise
Section titled “9. Exercise”Predict each answer without running the engine, then check with the commands from section 4:
counter_ready(ann, desk-1)withresident(ann)asserted but theCounterFactsgroup removed: which truth status, and which half of theCounterReadyconjunction fails?shift_posted(open)asserted twice (two identical asserts): doesshift_ok()change status? What does that say about facts versus derivations?- The E1116 probe fixed by adding
Applicanttouse selfbut leavingresident_seenundeclared: pass or refuse, and which name does the next diagnostic quote? - The consumer probe rewritten over
demo.northbridge.composition::resident_seen(apubrelation): refuse or build? And if it builds, what makes its rule fire? Slip { who: "ann", months: "12" }(text instead of integer): parse refusal,TYPE_ERROR, or silentNEITHER— and which of the three would still exit the suite nonzero?
Write down each prediction first; run the commands; explain any miss in one sentence. Check your work against the full solution with replayed commands.
10. Sources
Section titled “10. Sources”- Source:
packs/examples/language-demo/composition/package.law(aliases, entity, enums, map, record, role, Option const, visibility split, exported facts, consumer rule) andpacks/examples/language-demo/composition/desk.law(two-file explicit package,use selfborrow list, map-reading rule) - Tests:
packs/examples/language-demo/composition/tests/composition.lawtest(10 tests: consumer use, silence, record pair, map pair, role pair, Option pair) - Manifest:
packs/examples/language-demo/composition/law.toml(local_imports = "explicit", vocabulary dependency),law.lock+deps/(pinned world) - Refusal exhibits:
packs/examples/language-demo/composition/evidence/ev-use-self.txt(E1116) andevidence/ev-private.txt(E1105); package tour inpacks/examples/language-demo/composition/README.md - Prerequisite: nb-01: First permit: facts, a rule and a question
Three levels:
- Northbridge use (this article): the shared applicant is aliased, desks are routed by a total table, slips are fully named, and one note stays private — verified by the 10/10 suite plus the two quoted refusals.
- Domain template: alias shared sorts instead of
redefining them; close menus as enums and route them with
total maps; name every record field; mark the touchable
surface
pub; declare cross-file borrows; freeze every refusal with its code; never read a profile refusal as language-wide inability. - Confirmed example elsewhere: the same
use selfrule guards boundaries (tariff.law, seeboundaries/evidence/ev-use-enforcement.txt), and the same §24 boundary fires in the vectors (ERR-E1105-HIDDEN-NAME-02) — package-system-wide, not one roster’s quirk. - Confirmed external formalization (corpus): budget-linked
benefit parameters — the Kazakhstan Social Code reads the
monthly calculation index from the budget package. Package
kz.corpus.socialcode(importskz.corpus.budget),corpus/laws/kz/codes/social-code/00-package.law:4987:when kz.corpus.budget::mrp(2026, amount)— live cross-package composition where the Code consumes another package’s predicate (import rationale in the comment at line 4958), with intra-package composition viause self::{...}blocks andlaw.tomldeps. Evidence: package header verified by inspection (no construct note covers imports). Limit of verification: presence of the named construct at the cited line only, confirmed by direct file read; 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.