10 KiB
G-040 punk::args choice aliasing (-choicealiases) with parse normalization, display folding, and doc-lookup parity
Status: achieved 2026-07-08
Scope: src/modules/punk/args-999999.0a1.0.tm (parse + usage rendering), src/modules/punk/ns-999999.0a1.0.tm (cmdinfo/cmd_traverse choice resolution parity), src/modules/punk-999999.0a1.0.tm (punk::help topic argdoc as first consumer), src/tests/modules/punk/args/testsuites/, src/tests/modules/punk/ns/testsuites/
Goal: punk::args supports choice aliases (-choicealiases {alias canonical ...}) accepted at parse and normalized to the canonical choice in results, folded into the canonical entry in usage display - and the punk::ns doc-lookup walk resolves choice words by the same rules as the parser (aliases, -choiceprefix, -choiceprefixdenylist, -choiceprefixreservelist) - so alias sets like punk::help's topics|help and console|term|terminal collapse to one displayed entry per topic with help X and i help X agreeing.
Acceptance: a definition using -choicealiases parses an alias (and an alias prefix where -choiceprefix allows) to its canonical choice in the parse result, with -choicerestricted 0 passthrough and the deny/reserve lists honoured unchanged; usage display shows one entry per canonical choice with aliases folded (no duplicate rows; -choicelabels attach to the canonical); punk::ns::cmdinfo/cmd_traverse resolve subcommand words to docids with the same outcome as the parser for alias, prefix, denied, reserved and unknown words (the pre-goal characterization tests updated from pinned-GAP to fixed); punk::help's topic definition adopts the feature so i help lists one entry per registered topic while help h/help e still fall through to command lookup; definitions without -choicealiases behave unchanged (existing punk::args and punk::ns suites pass).
Context
Arose from the 2026-07-08 punk::help topic-registry restructure. The ::punk::help argdoc
declares the topic leader with -choices {topics help tcl env environment console term terminal} (-choiceprefix 0 -choicerestricted 0): every alias is its own choice entry, so
the i help usage table shows 8 entries for 4 topics, with duplicated -choicelabels.
What the existing -choice* machinery already expresses (all verified 2026-07-08 on the dev tree, punk902z src, tcl 9.0.2):
-
Prefix-shaped alias pairs collapse completely via
-choiceprefix 1plus the two lists:topic ... -choicerestricted 0 -choiceprefix 1 -choices {topics help tcl environment console terminal} -choiceprefixdenylist {help} -choiceprefixreservelist {en ter}gives:
env..environment-> normalized toenvironmentin the parse result (the parser returns the canonical choice, so downstream alias resolution needs no prefix logic);e/enpass through to the unrestricted fallthrough (the reserved phantomenblocks them without becoming selectable);term+ ->terminalwitht/te/terpassing through;helprequires the full word (denylist) soh/he/helstay available as command-fallthrough words; unknown words (lindex) pass through unchanged. -
The usage renderer self-documents the scheme: each choice cell highlights its minimal accepted prefix (bright green
envinenvironment,cinconsole), and a denied choice renders fully highlighted (type all of it). -
Minimum-prefix policy is fully controllable per word by reserving each shorter phantom (e.g. reserve
candcoto requireconfor console). Open policy question for punk::help: whether to allow the free extra prefixesc->console,to->topics,tc->tcl, which mildly shadow those words as command-fallthrough candidates.
What it cannot express - the actual feature gap:
- Aliases with no prefix relation to their canonical (
help->topics) must remain separate displayed choice entries with duplicated labels. - Even prefix-related aliases remain separate display entries when both full words are
listed (
consoleandterminalboth render, same label), because a choice entry is an atomic string with no canonicalization link.
Hence -choicealiases {alias canonical ...}: alias accepted at parse (participating in
prefix calculation like a reservelist entry, but selectable), normalized to its canonical
in the parse result, and folded in usage display (shown as an annotation on the canonical
entry - e.g. dimmed help beneath/beside topics - not as its own row). -choicelabels
keys stay canonical-only.
Approach
- punk::args::define: accept and validate
-choicealiases(each value must name an existing choice; alias must not collide with a choice or another alias). Parse layer: alias hits (exact, or unique prefix when -choiceprefix and not denied/reserved) normalize to the canonical, before -choicemultiple/-multiple accounting so duplicates collapse predictably. Usage/arg_error rendering: fold alias names into the canonical entry's cell; extend the minimal-prefix highlighting to alias words. - punk::ns cmd_traverse parity: the subcommand walk currently does its own
tcl::prefix::matchover -choices/-choicegroups and honours only-choiceprefix--choiceprefixdenylistand-choiceprefixreservelistare ignored (divergence proven:i help en-style lookups prefix-match wherehelp enexecution falls through). Factor the choice-word resolution into one shared punk::args helper (choices + groups + aliases + prefix + deny/reserve -> canonical-or-miss) consumed by both the parser and cmd_traverse, rather than a second implementation. choiceinfo lookups (subhelp et al) then key on the canonical. - punk::help adoption: registry aliases map onto -choicealiases (topics/help,
env/environment, console/term/terminal); decide and record the minimum-prefix policy
(reservelist phantoms) including the c/to/tc question above;
help topicsoutput andhelptopic::resolvestay as-is (resolve receives canonicals from the parse). - Test-first sequencing (user-directed): characterization tests for the current parse choice behaviour (prefix/deny/reserve under -choicerestricted 0, normalization) and the punk::ns cmdinfo/cmd_traverse docid flow land BEFORE this goal's implementation, with the parse-vs-traverse divergence pinned as a GAP test (fossilmove pattern). This goal flips those GAP pins to the fixed behaviour.
Implementation (2026-07-08 - acceptance met, goal achieved)
- punk::args 0.3.0:
-choicealiases {alias canonical ...}accepted on argument lines (dict-shape validated in the option switch; alias->existing-choice and no-collision cross-validated after spec merge - surfacing at definition resolve time, since define is lazy). The choice-word matching chain was extracted from get_dict into the shared resolverpunk::args::choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist}-> dict {matched exact canonical}; get_dict now delegates to it (the writeback/lset logic stays at the call site). Exact aliases match under any -choiceprefix setting and under -nocase; alias names join the prefix calculation pool when -choiceprefix is true; deny applies to the matched NAME (an alias in the denylist requires the full alias; a canonical reached via its alias is exempt from the canonical's own deny entry). Display: aliases are folded into the canonical entry as an '(alias: x)' / '(aliases: x|y)' note on its label (single convergence point covers all render paths), and alias names join the display prefix calculation so highlighted minimal prefixes stay truthful. - punk::ns 0.1.2: cmd_traverse's subcommand walk resolves choice words via the same choiceword_match call - the two parity GAP tests flipped to agreement (denied prefix and reserved word now resolve for NEITHER parse nor doc walk; previously the walk accepted both), choiceinfo lookups key on the normalized canonical, and -nocase is now honoured in the walk.
- punk::help (punk 0.2.3): registry aliases map onto -choicealiases; choices are the four canonical topics; minimum-prefix policy per user decision 2026-07-08 recorded as namespace variables: denylist {help} (h/he/hel stay command words), reservelist {c to tc} (fall through to command lookup; e/en and te/ter fall through naturally as ambiguous). 'i help' lists one entry per registered topic with aliases folded.
Verification: punk/args suite 98 pass + 1 skip (5 alias tests incl. the full punk::help policy matrix, noprefix/nocase modes, resolve-time validation), punk/ns 28 pass (parity tests flipped + alias traverse + display-fold assertion), punk/lib 21; argless 'help' output byte-identical to the original strict-80 baseline; 'i help environment'/'i help term'/'i help co' all render the canonical topic's documented usage.
Alternatives considered
- Prefix mechanics alone (no new feature) - covers env|environment and term|terminal but cannot fold topics|help or unify console/terminal display; rejected as the full answer, retained as the interim option for punk::help if the feature is deferred.
- Synthesising alias folding in punk::help only (custom display, registry-side resolution)
- rejected: the duplication belongs to every ensemble-style argdoc with alias words, and cmd_traverse would still diverge; solve it in punk::args once.
- Documenting subcommand aliases via duplicate -choices entries with shared -choicelabels (status quo) - works but scales the display linearly with alias count and lets labels drift; this is the shortcoming that motivated the goal.
Notes
- Session probe scripts (scratchpad, 2026-07-08): choicecollapse.tcl (parse behaviour table for the deny/reserve/prefix recipe), choicedisplay.tcl (usage rendering of the collapsed def, minimal-prefix highlighting).
- cmd_traverse divergence site: src/modules/punk/ns-999999.0a1.0.tm ~5157 (subcommand walk
tcl::prefix::matchover allchoices; consults -choiceprefix at ~5162, never the deny/reserve lists). - Parser reservelist/denylist semantics doc: 'i punk::args::define' -choice* section (clarified 2026-07-08 alongside this goal's creation).
- Related: the punk::help topic registry (punk module 0.2.0) is the intended first consumer; the possible future punk::config-declared subshell topic sets (not yet a goal) would inherit the folding for free.