# 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 1` plus the two lists: topic ... -choicerestricted 0 -choiceprefix 1 -choices {topics help tcl environment console terminal} -choiceprefixdenylist {help} -choiceprefixreservelist {en ter} gives: `env`..`environment` -> normalized to `environment` in the parse result (the parser returns the canonical choice, so downstream alias resolution needs no prefix logic); `e`/`en` pass through to the unrestricted fallthrough (the reserved phantom `en` blocks them without becoming selectable); `term`+ -> `terminal` with `t`/`te`/`ter` passing through; `help` requires the full word (denylist) so `h`/`he`/`hel` stay 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 `env` in `environment`, `c` in `console`), 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 `c` and `co` to require `con` for console). Open policy question for punk::help: whether to allow the free extra prefixes `c`->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 (`console` and `terminal` both 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 1. 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. 2. punk::ns cmd_traverse parity: the subcommand walk currently does its own `tcl::prefix::match` over -choices/-choicegroups and honours only `-choiceprefix` - `-choiceprefixdenylist` and `-choiceprefixreservelist` are ignored (divergence proven: `i help en`-style lookups prefix-match where `help en` execution 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. 3. 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 topics` output and `helptopic::resolve` stay as-is (resolve receives canonicals from the parse). 4. 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 resolver `punk::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::match` over 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.