nlir·natural-language IR Guide Spec Workspace Spec lab Explain Examples GitHub ↗

nlir — natural-language IR

nlir transpiles a terse, sigil-laden shorthand into fluent English. The shorthand is an intermediate representation: it is tokenised, parsed into a DAG using a config-defined grammar, and evaluated over a small stack machine with a tiny type system, where each operator is realised either deterministically (mechanical string/number expansion) or via an LLM call (a structured text-transformation).

Invocation — typically from a coding agent's prompt window:

nlir -e 'EXPR'

The engine ships only a tiny set of builtins (stack / context / indexing / assignment / arithmetic / coercion / list plumbing). Everything else — the operator vocabulary, their fixity/priority/arity/types, the models, the prompts, the coercions, the tests — lives in ~/.config/nlir/config.yaml. The binary is a small VM; the language is config.


Mental model

EXPR ──tokenise──▶ tokens ──parse──▶ DAG ──schedule/eval──▶ English
                             (grammar from config)   (stack machine; types+coercion; per-op det|llm; parallel)

Runtime state (two namespaces)

Name Owner Lifetime Access
context (NLIR_CONTEXT env, or a context file) harness / scripts / nlir set / in-expr = transient by default; persistent with a context file read $name / ^; write LHS=RHS (immediate) or set/append-message
stack nlir, internal one run ; pushes, $ / $N peek, nullary-op pops

Types & coercion

Every value has a type: string (default), number, bool, list, dict (an insertion-ordered key=value map — see Dictionaries & accessors below), form (a quoted {…} expression — see metaprogramming below).

Coercion that cannot produce the target type is a loud error (the LLM path is constrained by its output JSON schema, so it returns a valid typed value or fails). list → number is an error. A dict coerces to string as its key=value pairs (joined with _sep); dict → number/bool is an error.

Coercion is part of the same expand/contract machinery as operators, and its results honour _cache.


Modes

det (no network) or llm. Default defaults.mode, override --mode. Realisation resolution for an operator:

  1. command: (subprocess, det) or reduce: (builtin numeric add/sub/mul/div/pow, det) → run it;
  2. else mode det → template: / join:;
  3. else mode llm → model: + prompt:.

command/reduce/coercion math are deterministic regardless of mode; only string transformations differ by mode.


Builtins (fixed engine layer, not config)

Sequencing & stack

Context: read & assign

Message indexing (role-filtered views of _messages)

Structure

Forms, application & macros (metaprogramming)

Dictionaries & accessors

Data trees: loading and navigating structured data

Context values keep their JSON structure: objects are insertion-ordered dicts, arrays are lists, all the way down (a context file, NLIR_CONTEXT, the wasm contextJson, and assigned dicts round-tripping through context). Load a whole file as a native value with --bind NAME=PATH (.json and .yaml/.yml load as data; any other file — Markdown, text — binds as one string; repeatable; transient — never written to a context file):

nlir --bind spec=spec.json -f lib.nlir -e '$join%({$0.id}↦$spec.items, ", ")'

-f PATH runs a program file (repeatable; files then any -e are joined as ;-separated statements, so a library file can define named forms used later). In program files, lines whose first non-blank characters are -- are comments.

Deterministic data builtins (all loud on bad input; a context key of the same name shadows them):

builtin result
$keys%d / $values%d a dict's keys / values (a list's indices / items)
$entries%d [{key=k, value=v}, …] — map a dict like a list of records
$dict%pairs a dict from [key, value] pairs or {key, value} records — the inverse of $entries, and the way to build a dict with computed keys
$get%(c, k) / $get%(c, k, default) dynamic .: dict key or list/string index ($get%($nodes, $id) for keys with spaces); missing → default, else loud
$has%(c, k) Bool: is k a key / valid index
$omit%(d, keys) / $pick%(d, keys) the dict without / with only those keys (one key or a list), in its own order — e.g. keep reserved packet keys out of a prompt: $json%($omit%($p, [_pin, _concepts]))
$join%(list, sep) items rendered and joined with sep
$flat%list flatten one level — the concat recursive tree walks need (and list append: $flat%[$xs, [x]])
$zip%(a, b, …) [[a0, b0], [a1, b1], …], truncated to the shortest — pair parallel results with their inputs
$slice%(c, start[, end]) sub-list / substring, end-exclusive, negatives from the end, clamped
$json%v / $json%(v, indent) the value as JSON text (dicts stay ordered)
$parse%text JSON text as a value — the inverse of $json; tolerates a code fence or prose around the JSON (model replies), loud when there is none
$type%v string · number · bool · list · dict · form
$trim%s · $lower%s · $upper%s · $lines%s text plumbing ($lines splits on newlines)
$replace%(s, from, to) literal replace-all
$match%(s, re) first regex match — the whole match, its one group, or a list of groups; '' if none
$matches%(s, re) every match (same shape per match)
$sub%(s, re, repl) regex replace-all ($1, ${name} refer to groups)

Write regexes in single quotes ('\d+'): double-quoted strings process \ escapes. A recursive named form walks a tree: ids={$flat%[[$0.id], $flat%({$ids%$0}↦$0.children)]}; $ids%$tree.

Higher-order list builtins ($map / $fold / $scan / $filter)

Deterministic, model-free list combinators (bd-14af74): each takes a form as its first argument and applies it structurally — the form's body only touches the model if it itself contains an llm op. $map has a keyboard-typable alias (<$>) and a visual alias (↦); $fold has ⊘ (see the operator table). $scan and $filter are builtin-only. These aliases are ordinary config.yaml operators using builtin: — the engine does not hardcode their spellings.

Together these are the deterministic composable core — statistics (mean = $fold%(…)/$len%…), running series, and predicate-filtered aggregates — all exact, model-free, and CI-gateable.

Set-notation builtins ($elem / $union / $inter / $diff)

Deterministic, total set algebra over values (bd-49d65a). Like $sort/$contains, element identity is the rendered value (trimmed), so lists of numbers, strings, or mixed values compare predictably. $union/$inter/$diff return a list; results are order-preserving (first-occurrence order of the left operand) and deduped. Coercion for the algebra ops: a list spreads to its items, a dict spreads to its keys (set ops on a dict operate on its key set), and a scalar is a one-element set.

They compose with the rest of the value-builtin family: $if%($elem%('ERROR',$0),'page','ok') gates on membership; $nth%(0,$sort%$union%(a,b)) = the least element of a merged set. Sigil aliases (∪ ∩ ∈ ∖) are a planned follow-up (config-operator layer).

Length builtin ($len)

$len%coll — the length/cardinality of coll as a Number (bd-b9b491): a list → item count, a dict → key count, and anything else (string/number/bool/form) → the Unicode character count of its rendered form (so a string's length is its chars: $len%'hello' → 5, $len%42 → 2). Total + deterministic — the model-free counting primitive: $len%[2,3,5,7,11] → 5, $len%{a=1,b=2} → 2, and set cardinality $len%($inter%([2,3,5,7,11],[1,3,5,7,9])) → 3. A spread list $len%[a,b,c] (3 args) counts 3; a single scalar in a single-element list $len%[x] is indistinguishable from $len%x after the spread.

Predicate builtins ($gt / $lt / $not)

$gt%(a,b) / $lt%(a,b) — STRICT numeric comparison (bd-89b3d0), a > b / a < b, returning a Bool. They fill the strict-comparison gap: the >/< sigils are taken (expand/shorten), and the >=/<= operators are non-strict, so $gt%(3,3) → false where 3>=3 → true. Operands coerce to number like >=/<=.

$not%(x) — boolean NOT: negates x's truthiness and returns a Bool.

$and%(a, b, ...) / $or%(a, b, ...) — boolean AND / OR over two or more operands' truthiness, returning a Bool. They short-circuit left to right: an operand that is not needed is never evaluated (no model call, no error), so $and%($has%($x, 'k'), $gt%($x.k, 0)) is a safe guard. (& is not boolean AND: it is a semantic operator.)

$try%(expr, fallback) — expr's value or, if evaluating it fails (a model call that errors or returns nothing, a missing key, an index out of range), fallback's. Lazy like $if: the fallback is evaluated only when needed, and a failing fallback still fails. Use it where one failed call should degrade a long pipeline rather than abort it: $try%(=>('Trim: ' ++ $d), $d) keeps the draft if the trim fails. Unlike ! (which is textual negation, e.g. !(3>=5) → the string "not (false)"), $not inverts any Bool — including a fuzzy ~> result — so it composes in fold-fusion count-if-NOT. Since Bools coerce true→1 under +, map a predicate then fold to count: $fold%({$0+$1},$map%({$lt%($0,5)},0..10)) → 5. The sigil alias ¬ (config operator, prefix) completes ∧ ∨ ¬ — see the operator table.

Rounding builtins ($floor / $ceil / $round)

Fundamental numeric primitives (bd-9004bb): $floor%x → ⌊x⌋, $ceil%x → ⌈x⌉, $round%x → nearest integer (half away from zero). Operand coerces to number. They keep $nth strict (it rejects a fractional index) while letting median fall out of sort + index — no special $median function: $nth%($floor%($len%L/2),$sort%L) is the middle element for odd length. The two middle indices are $floor and $ceil of ($len-1)/2, so one expression gives the median for both parities (mean of the two middles when even). Same principle as max = sort-then-last: composable primitives over special-cased sugar. A sigil alias (⌊⌋) is a config-operator follow-up.

$sqrt%x (bd-92cbf0) — square root (f64::sqrt; negative operand → NaN). Same unary-numeric family. It completes stddev — variance already composes from $fold/$map/$len, so stddev = $sqrt%variance — and unlocks Pythagoras/hypotenuse ($sqrt%(3*3+4*4) → 5), Euclidean distance, and geometric mean. A √ sigil is a config-operator follow-up.

Modulo builtin ($mod)

$mod%(a,b) — mathematical modulo (mathy golf): the remainder of a divided by b via rem_euclid, so the result is non-negative in [0, |b|) — $mod%(7,3) → 1, $mod%(0-1,3) → 2 (not -1, unlike C-style %). Operands coerce to number like $gt/$lt; a zero divisor is a loud error. The non-negative convention is what makes number theory compose cleanly: even/odd ($mod%(n,2) → 0/1), divisibility ($mod%(n,k) → 0), cyclic indexing, and Euclid-style GCD. Count evens in 1..10: $len%($filter%({$not%($mod%($0,2))},1..10)) → 5.

Reserved builtin sigils: ; $ ^ = [ ] , ( ) { } % \ ` , the quote chars " ', the escape \. Configured operator sigils (# ! & | :: .. + - * / ** …) add to this. After ^/$, * _ / are role modifiers and a leading - is a negative index.


Lenses: backward passes (nlir put)

A program is a get: inputs (bound data, its own literals) → output. nlir put --edited FILE [-f …] [--bind …] [EXPR] is its put: it runs the program forward (det), pushes the edited output back through it, and prints JSON — the edits to bound inputs (kind: data, with /-separated leaf paths), the edits to program literals (kind: program, with the statement and AST path), edits that could not be pushed further (stuck, naming the expression and why), and clashing edits (conflicts). Both lens laws are checked by re-evaluation: GetPut (the unedited output implies no edits) and PutGet (the edited inputs and program reproduce the edited output).

Each deterministic construct carries its own put: . and $get set the field they read; ++ aligns the edited text with its old parts by a character diff (an edit at a boundary between a literal and a computed part goes to the computed part — content belongs in data); $join splits on its separator; $trim restores whitespace; ↦ / $map put each item through the mapped form; $if puts into the branch it took; list and dict literals put element-wise. A form application puts through the form's body (a program-defined form through the body as written), and edits to $0 $1 … become edits to the call's arguments, merged three-way so separate field edits to one record combine. A name bound by an earlier statement puts into that statement.

_inverses declares isomorphisms: _inverses={quote=unquote} makes a call to either form put through the other, which is how a regex-built layout ($sub has no general inverse) inverts exactly. $json puts by parsing the edited text back into a value.

Model operators (=>, ::, .., !, …) get a model-backed put in llm mode (nlir put --mode llm): a request on the large tier asks for minimal find/replace patches to the operator's inputs that would turn its output into the edited output (preferring the subject matter over the operation's directions). The patches apply exactly and each changed input puts further back with the exact rules — so a patch to an instruction built with ++ lands in the data it was built from, e.g. a field of a packet rendered with $json. An operator may carry an authored backward instruction (put: in its config), folded into the request. The request is an ordinary content-addressed realisation, so it replays from the store. In llm mode PutGet is judged: put_get_similarity is ≈ between the edited output and the re-run; and tensions lists input changes that contradict each other — across fields, or within one edited text (edits can merge textually — text edits to one value merge three-way when their regions do not overlap — and still contradict each other).

Authored puts and alternatives

Some code has no exact put: a selection (the highest card), a summary, a comparison, an $if whose branch would have to flip. Such an edit has many possible inputs, or none the construct rules can reach, and nlir put reports it as stuck. Three ways back cover it:

--partial (JSON-object outputs) makes only the fields the edit changed binding; the others are recomputed, so editing the played card shows the reason that follows. --freeze PATH (repeatable) keeps an input fixed, which asks for a particular kind of explanation. An edit that adds a field the program never outputs is reported at once as outside what the program can produce: no input edit can make it, the program must change. examples/cards.sh runs all of this on a card game.

Whitespace, escapes & quoting


Interpolation & evaluation timing


Execution graph & parallelism


Output & tracing


Config operators (the language proper)

The canonical operator vocabulary — each realised via template / det / reduce / builtin / command (deterministic) or prompt (llm). These descriptions are the authoritative semantics, mirrored by nlir help.

Glyphs are reserved for generic transforms: operations recognisable from category theory and functional programming, polymorphic over the values they act on. Task-specific rewrites (formal, plain, question, gist, …) are not glyphs; they are prelude forms built from the core (§Prelude), readable and overridable.

Semantic core (model-realised; each has a deterministic stub for --mode det). Values are texts read as concepts.

op name fixity · arity what it does
=> respond prefix · 1 Obey: treat the operand as an INSTRUCTION and return only the result of following it (length/format constraints included). Generative rather than transformative; carries the generative system frame, so it obeys on any tier. A bare => on a pipe obeys the piped text.
:: recast infix · 2 x :: kind re-expresses x as the kind of text named on the right — a register, form, genre, format or length ('formal', 'a question', 'plain language', 'a haiku', 'JSON') — keeping its meaning. Saturates: x::k::k ≈ x::k.
.. project infix · 2 x .. aspect returns the part of x the right operand describes — an aspect ('its subject', 'the gist', 'the risks') or a position (3, 'the last item'). The semantic twin of .; two integers give the deterministic range a..b.
! dual prefix · 1 The opposite: a claim negated clause-wise (!(a&b) = neither), a concept-word's antonym. An involution: !!x ≈ x.
& and mixfix · >0 Product: joins operands into one "X and Y" statement (not boolean ∧); nullary & folds the premise stack.
| or mixfix · >0 Coproduct: joins operands into one "X or Y" choice, kept as genuine alternatives.
Δ change infix · 2 How the first text becomes the second — added, removed, shifted. Directional (aΔb ≠ bΔa).
▷ apply infix · 2 x ▷ change returns x changed as described, and nothing else. The partner of Δ: a ▷ (a Δ b) ≈ b.
~> entails infix · 2 Bool: does the first text imply the second? The preorder on meanings. det = substring containment.
≈ similarity infix · 2 Number in [0,1]: how fully the second text keeps the meaning of the first (reference). det = word-set Jaccard; llm = judged equivalence of claims, conditions and criteria ($gen ≈ $gold >= 0.8).

Structural core (deterministic, polymorphic — the typeclass-shaped layer):

op name fixity · arity what it does
. access infix · 2 Lens get: [a,b,c].1→b (0-based, negative from end), {k=v}.k→v, "the".2→e. Missing → loud error. Binds above %.
++ concat mixfix · >0 Monoid append: text concatenates, lists concatenate, dicts merge right-biased ({a=1}++{a=2,b=3} → {a=2,b=3}). A lone list operand mconcats its items (++[a,b,c] → abc).
_ mtimes infix · 2 Monoid power: x _ n = n copies appended — 'ab'_3 → ababab, a list repeats its items, and a {form} composes with itself n times ({$0+1}_3 % 5 → 8).
∘ compose mixfix · >0 Composition: (f ∘ g) % x = f % (g % x); variadic, right to left. Builds pipelines from named forms: ($gist ∘ $formal) % notes.
# length prefix · 1 Foldable length: list items, dict keys, text characters. Alias for $len.
// split infix · 2 Split text by a separator into a list: "a,b,c"//"," → [a,b,c].
<$> mapascii infix · 2 Functor map: apply the left form to each item of the right list: {$0*$0}<$>[1,2,3] → [1,4,9]. Alias for $map%.
↦ mapop infix · 2 Visual map alias with the same builtin: map semantics as <$>.
⊘ foldop infix · 2 Fold the right list with the left binary form: {$0+$1}⊘[1,2,3,4] → 10. Alias for $fold%.
∪ setunion infix · 2 Set union — order-preserving, deduped; dict operands → their keys. Alias for $union%.
∩ setinter infix · 2 Set intersection — items of a also in b. Alias for $inter%; binds tighter than ∪/∖.
∖ setdiff infix · 2 Set difference — items of a not in b. Alias for $diff% (U+2216).
∈ setelem infix · 2 Membership → Bool: list element, dict key, or substring. Alias for $elem%.

Numeric operators (operands: number, result: number, reduce:):

op name fixity · arity what it does
+ add mixfix · >0 Sum of the operands (words / units / bases coerce to numbers first).
* mul mixfix · >0 Product of the operands.
- sub infix · 2 Difference, a − b (left-associative).
/ div infix · 2 Quotient, a ÷ b (guards divide-by-zero).
** pow infix · 2 a to the power b — right-associative (2**3**2 = 512).
√ sqrtop prefix · 1 Non-negative square root.

Comparison and boolean operators (result: bool, deterministic):

op name fixity · arity what it does
== equals infix · 2 Equal by rendered value: 5==5 → true.
!= not-equals infix · 2 The operands differ.
< less infix · 2 Numeric less-than: 3<5 → true.
> greater infix · 2 Numeric greater-than.
<= at-most infix · 2 Numeric ≤.
>= at-least infix · 2 Numeric ≥.
∧ booland infix · 2 Logical AND (truthiness) — distinct from the semantic product &.
∨ boolor infix · 2 Logical OR — distinct from the semantic coproduct |.
¬ boolnot prefix · 1 Logical NOT; negates any Bool, including a fuzzy ~> result. The boolean twin of !.

Prelude

Config prelude: is nlir source evaluated into every run before the program: name={…} named forms (and literals), -- comment lines. Bindings are transient (never written to the context file), and a context binding of the same name wins, so a project can redefine or extend the vocabulary. The shipped prelude:

form definition
$gist {$0 .. 'its gist, in one short sentence'}
$subject {$0 .. 'its primary subject, as a short noun phrase'}
$formal / $casual {$0 :: 'a formal, professional register'} / … 'a casual, conversational register'
$plain {$0 :: 'plain language a child could follow'}
$question {$0 :: 'a question with the same subject and meaning'}
$expand / $shorten {$0 :: 'a fuller version …'} / {$0 :: 'the fewest words that keep every fact and figure'}
$implies {=>('State, in one sentence, what follows from: ' ++ $0)}

Prelude forms compose like any form: ($gist ∘ $formal) % notes, $formal ↦ [a, b], $draw%({$question%$q}, 3).

The algebraic laws (!!x ≈ x, x::k::k ≈ x::k, a ▷ (a Δ b) ≈ b) are realised-semantics: they hold in llm realisation — the user-facing output — while det mode applies the literal stubs (!!a → "not not a").


Grammar & parsing

Tokens: bare literal [a-zA-Z0-9]+; numeric literal; quoted literal ('…'/"…"); operator (longest configured op:, so ** before * and <$> as one token); builtin sigils ; $ ^ = [ ] , ( ) ` (with $name/$N, ^/^_/^*/^/, LHS= sub-forms); escapes \x. A builtin sigil may appear later in a configured punctuation operator — <$> is config, not a lexer special case. At a leading $, configured punctuation operators such as $>/$( longest-match before the fallback; $name, $_key, $N, $-N, and bare $ remain reserved stack/context forms and cannot be shadowed by config.

Operator attributes (config): op, arity (1,2,…,>0), fixity (prefix/postfix/infix/mixfix), priority (higher binds tighter, default 9), assoc (left/right, default left; only meaningful for infix), operands/result types.

Precedence (config-tunable): ^ indexing is tightest; prefix unary (# ! ¬ √) binds above all binary; binary follows normal math — ** > * / > + - — then string & |; = is loosest. Concretely: ^ 20 · . 19 · % 18 · ∘ _ 17 · .. 16 · :: 15 · # ! ¬ √ 14 · ** // 13 · * / 12 · + - 11 · ++ 10 · & | Δ ▷ 9 · <$> ↦ ⊘ 8 · ∩ 7 · ∪ ∖ 6 · ∈ == != < > <= >= ≈ 5 · ∧ 4 · ∨ 3 · = 0 (nlir help lists the exhaustive per-op priority; this prose summarises the tiers). prefix takes one right operand; postfix takes leftward to its priority; variadic flattens; mixfix unifies infix/list/nullary; ties → prefix > infix > postfix. Associativity: infix operators are left-associative by default (a-b-c = (a-b)-c, 16/4/2 = (16/4)/2); ** sets assoc: right, so exponentiation is right-associative (2**3**2 = 2**(3**2) = 512), matching normal math and Python. Mixfix operators flatten same-op chains into one n-ary node, so associativity does not affect them. (…) overrides and is preserved in output.


Worked examples

Assume the last assistant message (^-1) is about a rust-rewrite.

Expression Reading
^-1 last assistant message
^_-1 / ^/-1 last user / last system message
(1+1)^(5+5) assistant messages 2..10, joined with _sep
1+1 "2" (number, stringified on output)
$subject%^-1 "a rust-rewrite"
!($subject%^-1) "not a rust-rewrite"
^-1 :: 'one line' the last message, recast as one line
#(0^*-1 // "\n") how many lines the whole thread spans
&[a,b,c] "a, b, and c"
!(a&b) "not (a and b)" — parens preserved
'one two' "one two"
"the subject is $k" interpolates $k at eval time
_sep=\ ;[a,b] "a b"
`k=$subject%^-1;$k serial: store the subject as k, then read it

CLI surface

nlir -e 'EXPR' [--quiet] [--mode det|llm] [--model haiku | --model medium=haiku,large=opus] [--parallelism 8] [--dry-run]
nlir -f lib.nlir [-f prog.nlir] [-e 'EXPR']   # program files (`--` comment lines)
nlir … --bind NAME=PATH           # JSON/YAML → native dict/list `$NAME`; other files → string (transient)
nlir … --cache-file PATH [--replay strict] [--cache-stats]   # realisation store (§Execution graph: Caching)

# context read (precedence: --context-file › --session-file › NLIR_CONTEXT env › default file)
# (default file loads for the REPL / set / get; a one-shot `-e` skips it — bd-85c49d)
nlir … --context-file PATH
nlir … --session-file PATH        # e.g. Pi session: roles kept, tool calls dropped

# context write (immediate; on active/default context file); set = key replacement
nlir set KEY VALUE
nlir set '{"k":"v","_messages":[…]}'   # each named key replaced (not deep-merged)
nlir get KEY
nlir append-message [--role user] "text"

# interactive: one expr per submission, trailing `\` continues; :cmd == `nlir cmd`
nlir repl [--context-file F] [--raw]     # :set/:get/:append-message inside repl

# graphs: the expression DAG, or one node per top-level statement
nlir show 'EXPR' [--png F | --out F | --kitty | --frames | --save-animation F | --animate]
nlir show --statements [-f FILE…] ['EXPR'] [--png F | --out F | --text | --json | --tikz]
nlir show --tikz [-f FILE…] ['EXPR']   # each statement's expression tree as a LaTeX forest tree

# plumbing (CLI template stack)
nlir parse 'EXPR'
nlir test
nlir mcp stdio        # mcp-cli
nlir self-update …    # updatable-cli
nlir feedback …       # feedback-cli

Pi plugin: a prompt starting with | → the remainder is expanded as nlir first; the plugin append-messages each turn and pipes shorthand into a long-lived nlir repl --raw.


Example ~/.config/nlir/config.yaml

The shipped config.example.yaml is the reference config: the v2 operator set (§Config operators), the prelude, models, prompts, types, sessions and the tests: suite nlir test runs. nlir writes it to ~/.config/nlir/config.yaml on first run; include_str! embeds the same file in the binary and the browser build, so the CLI, the docs and the workspace never drift apart.

Implementation / project shape

Rust, Nix-flake-managed, on the harryaskham CLI template stack (mature examples under ~/.cacophony/daemon/checkouts/…): mcp-cli (nlir mcp stdio — the whole surface is agent-callable), updatable-cli (nlir self-update), feedback-cli (nlir feedback). Behaviour above is the spec; this is the scaffolding pattern.

OFFICIAL · A.SKH.AMOFFICIAL · A.SKH.AMSEAL OF ORIGINSEAL OF ORIGINMADE BYMADE BYAGENTSAGENTS