A verb alias is declared, and the definition that makes it callable is generated from the declaration
Accepted
Context
THE DISPATCHER'S ALIAS RULE LIVES IN A COMMENT. `bin/ontoref.nu` states «All aliases delegate to canonical commands» above ~170 definitions written by hand, and nothing declares which verb is an alias of which. Every reader that needs the distinction has had to reconstruct it.
MEASURED 2026-09-16, on the dispatcher as it stood:
171 alias definitions 152 pure forwards · 6 help pages · 6 group funnels (`missing-target`) 1 forward that adds `| default ""` · 6 that RE-IMPLEMENT their target instead of calling it (`conv audit/describe/sync`, `mg l/p`, `wf gen`) 19 accepted less than `bkl add` could not assign a release, `d capabilities` could not reach their target --full, `dg` dropped its positional (tests/test_alias_parity.nu) 1 copies a declaration `main prov` restates `short_alias = "prov"` from provisioning/domain.ncl partial coverage `ix` aliases 3 of interaction's 11 sub-verbs, `pos` 4 of 13, `bkl` 11 of 14 3 hand copies of the list the definitions, 16 `fmt-aliases` blocks in nulib/help.nu, and the ALIASES table printed by `ontoref help`
THE INFERENCE THAT REPLACED THE HAND LIST, AND WHY IT IS NOT THE ANSWER. `describe.nu:: dispatcher-verbs` (same day) derives the command list from parsed commands and classifies an alias by the SHAPE OF ITS BODY — one `main <verb>` call, or `missing-target` towards another head. That took `describe capabilities` from 48 published commands to 338 and let every group answer `help`. It is also the option bl-055 named a proxy: a rule applied to form, written nowhere. Its first version compared a funnel's whole target to the head and classified `coder` as an alias, dropping 21 verbs from the published surface until a second derived list exposed it.
WHY «THE DISPATCHER READS THE DECLARATION» NEEDS GENERATION. Nushell resolves `ontoref ad list` against a `def "main ad list"` that must exist when the script is PARSED; it cannot define a command from data at run time. So a declaration can reach the dispatcher only by producing its definitions before parsing, or by rewriting the arguments before the dispatcher sees them. Probed on 0.115.1: generated definitions can call `main <verb>` defined elsewhere in the dispatcher, and forwarding an optional flag with no default (`--x $x`, `$x` null), a switch (`--sw=$sw`) and a rest parameter (`...$rest`) gives the same result as calling the target directly — so a generated signature can be an exact copy of its target's.
THE PRECEDENT EXISTS TWICE. adr-038: an artefact generated from a catalog, headed with its source, never edited by hand, compared by a dry run. adr-012: a domain's `short_alias` declared in domain.ncl and projected to `domains/aliases.txt` for the wrapper to read.
Decision
A VERB ALIAS IS DECLARED ONCE, AND EVERYTHING THAT NEEDS IT IS PRODUCED FROM OR CHECKED AGAINST THAT DECLARATION.
(1) THE DECLARATION. `reflection/aliases.ncl`, typed by `reflection/schemas/verb-alias.ncl`, closed records. Two shapes, because the measured surface has two:
heads a single word that is a synonym of a canonical HEAD bkl → backlog, ad → adr verbs one or more words naming a canonical VERB dst → describe state adr l → adr list
A target must be a canonical verb or head the dispatcher defines. A domain's alias is NOT declared here: it is read from that domain's `short_alias` (adr-012), so `prov` has one source.
(2) THE SEMANTICS, stated because today's surface is not uniform and the declaration must not copy its accidents: - a head synonym covers the bare head and EVERY canonical sub-verb of its target — `ix` stops covering 3 of 11; - a short form declared under a canonical head (`adr l`) is also generated under each synonym of that head (`ad l`); - a short form means what its group declares; no letter has a global meaning (`s` is `show` in adr and `search` in describe, measured).
(3) THE DEFINITIONS ARE GENERATED. `ontoref verbs generate` rewrites a delimited region at the end of `bin/ontoref.nu` («BEGIN GENERATED VERB ALIASES (adr-115)» … «END GENERATED VERB ALIASES») from the declaration and from each target's PARSED signature — same parameters, types, defaults and short flags. The region is never edited by hand, and outside it no definition delegates. The six that re-implemented their target become calls to it.
A REGION AND NOT A SOURCED FILE, and the difference was measured, not preferred: the first implementation wrote `bin/aliases.nu` and `source`d it, and every ontoref invocation paid ~0.25 s more CPU to parse the dispatcher (parse-only medians, interleaved: 1.61 s against 1.35 s before the change). The same 267 definitions written into the dispatcher parse at 1.38 s — so the cost was `source`, not the aliases.
(4) GENERATION REFUSES instead of guessing: a target that is not a canonical verb, a generated name equal to a canonical one, and two declarations producing the same name. The generator reads target signatures from a copy of the dispatcher with the region emptied, so a stale alias to a removed verb cannot prevent its own regeneration.
(5) EVERY READER USES THE DECLARATION. `dispatcher-verbs` marks aliases from it, not from body shape; the ALIASES table and the per-group alias lines in help are derived from it.
(6) THE EXECUTED SURFACE IS STILL CHECKED. `verbs generate --check` fails when the generated region differs from what the declaration produces; `tests/test_alias_parity.nu` and C1 keep asserting what the dispatcher actually does. The declaration does not replace them.
Constraints
- Hard The dispatcher's generated region is exactly what the declaration produces; no alias definition exists outside it.
- Hard Every alias target is a canonical verb or head the dispatcher defines, and no generated name equals a canonical one.
- Soft `dispatcher-verbs` marks aliases from the declaration and not from the shape of a definition's body.
- Soft Alias parity over the running dispatcher stays asserted after generation.
Alternatives considered
- Keep inferring aliases from body shape (bl-055 option B) — rejected: The rule stays in a comment and every reader keeps reconstructing it. Its first version misclassified a whole group. Sacrifices the declaration for zero new files.
- Declare aliases and keep the hand definitions, with a gate comparing them — rejected: Two copies of every alias, forever, and the dispatcher never reads the declaration. Sacrifices the move from enforcement to gravity: the friction stays external and permanent.
- Declare aliases and rewrite arguments in the bash wrappers — rejected: The expansion policy would be written into both wrappers — adr-101's policy-implemented-in-the-entry-point, twice. Sacrifices one home for the policy.
- Declare aliases and re-enter the dispatcher with the canonical arguments — rejected: Every aliased call parses the dispatcher twice, ~1.5 s measured per invocation, and aliases vanish from `scope commands`. Sacrifices the responsiveness of the surface humans type.
- Declare only the aliases that exist today, sub-verb by sub-verb — rejected: It freezes the partial coverage (`ix` 3 of 11) as intent. A synonym that works for some sub-verbs and not others routes a reader to a second error, the defect bl-055 records.
Anti-patterns
- A structural rule held only in a comment above the code it governs — A rule every reader depends on is written as prose next to the definitions, so each reader reconstructs it from the shape of the code, and the definitions drift while the comment stays true.
- A declaration added next to the hand copies it was meant to replace — A registry is introduced and the original definitions stay, with a gate comparing them. The copies still exist and still drift; the registry becomes one more copy that happens to be checked.
Perspectives