Browse Source

G-040 achieved: punk::args -choicealiases with shared choiceword_match resolver; doc-lookup parity; punk::help topic collapse

Activated and completed under user direction (2026-07-08). Aliases for choice
values are now first-class: accepted at parse, normalized to their canonical
choice in results, folded into the canonical entry in usage display, and
resolved identically by the punk::ns doc-lookup walk.

- punk::args 0.3.0:
  * -choicealiases {alias canonical ...} on argument lines; dict shape
    validated in the option switch, alias->existing-choice and no-collision
    cross-validated after spec merge (surfaces at resolve time - define is
    lazy)
  * the choice-word matching chain extracted from get_dict into the shared
    resolver punk::args::choiceword_match {word nocase allchoices
    choicealiases choiceprefix denylist reservelist} -> {matched exact
    canonical}; get_dict delegates to it (writeback/lset logic stays at the
    call site) - one implementation for parser and doc walk
  * exact aliases match under any -choiceprefix/-nocase setting; alias names
    join the prefix-calculation pool when -choiceprefix is true; deny applies
    to the matched NAME (denied alias requires the full alias; a canonical
    reached via its alias is exempt from the canonical's own deny entry)
  * display: aliases fold into the canonical entry as an '(alias: x)' /
    '(aliases: x|y)' label note (single convergence point covers all render
    paths); alias names join the display prefix calculation so highlighted
    minimal prefixes stay truthful
  * define doc gains the -choicealiases section
- punk::ns 0.1.2: cmd_traverse resolves subcommand words via choiceword_match
  - -choiceprefixdenylist/-choiceprefixreservelist now honoured in doc lookup
  (the two pinned parity GAP tests flipped to agreement), aliases normalize
  before choiceinfo lookup, -nocase honoured in the walk
- punk 0.2.3: punk::help adopts the feature - 'i help' lists one entry per
  registered topic (topics+help, tcl, env+environment, console+term|terminal);
  topic words accept aliases and unique prefixes with the user-decided
  minimum-prefix policy recorded in ::punk::helptopic: 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).
  Argless 'help' output byte-identical to the strict-80 baseline.
- tests: choices.test +5 (alias exact/prefix normalization, noprefix exact-
  only, nocase, the punk::help policy matrix, resolve-time validation);
  cmdflow.test parity tests flipped from GAP to agreement + alias-traverse and
  display-fold assertions (+flowaliased fixture)
- suites: punk/args 98 pass + 1 skip, punk/ns 28 pass, punk/lib 21 pass
- G-040 flipped active -> achieved 2026-07-08; detail file records
  implementation and verification
- project 0.4.10 (CHANGELOG entry)

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 6 days ago
parent
commit
850b7f23cc
  1. 6
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 35
      goals/G-040-punkargs-choicealiases.md
  4. 2
      punkproject.toml
  5. 28
      src/modules/punk-999999.0a1.0.tm
  6. 3
      src/modules/punk-buildversion.txt
  7. 371
      src/modules/punk/args-999999.0a1.0.tm
  8. 3
      src/modules/punk/args-buildversion.txt
  9. 24
      src/modules/punk/ns-999999.0a1.0.tm
  10. 3
      src/modules/punk/ns-buildversion.txt
  11. 133
      src/tests/modules/punk/args/testsuites/args/choices.test
  12. 78
      src/tests/modules/punk/ns/testsuites/ns/cmdflow.test

6
CHANGELOG.md

@ -5,6 +5,12 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.4.10] - 2026-07-08
- G-040 completed (activated and flipped to achieved same day, user-directed): punk::args 0.3.0 adds `-choicealiases {alias canonical ...}` — aliases accepted exact under any `-choiceprefix`/`-nocase` setting, participating in prefix calculation when prefixing is on, always normalizing to the canonical choice in parse results; deny semantics apply to the matched name (a denied alias needs the full alias; a canonical reached via its alias is exempt from the canonical's own deny entry); usage display folds aliases into the canonical entry as an `(alias: …)` label note. The choice-word matching chain was extracted from `get_dict` into the shared resolver `punk::args::choiceword_match`, now also consumed by punk::ns 0.1.2's `cmd_traverse` — closing the doc-lookup parity gap (deny/reserve lists were honoured by parse but ignored by the walk; the pinned GAP tests flipped to agreement) and normalizing aliases before choiceinfo lookup.
- punk 0.2.3: punk::help adopts the feature — `i help` lists one entry per registered topic (`topics` +alias help, `tcl`, `env` +alias environment, `console` +aliases term|terminal), and topic words accept aliases and unique prefixes with the user-decided minimum-prefix policy: `h`/`he`/`hel`, `e`/`en`, `te`/`ter`, and the reserved `c`/`to`/`tc` all fall through to command lookup. Argless `help` output remains byte-identical to the strict-80 baseline.
- Suites: punk/args 98 pass + 1 skip (5 new alias tests incl. the punk::help policy matrix), punk/ns 28 pass (parity tests flipped, alias traverse + display-fold assertions), punk/lib 21 pass.
## [0.4.9] - 2026-07-08
- punk 0.2.2 (doc-only, from interactive review of the help restructure): `i help` / `i help <topic>` usage tables narrowed to reasonable widths (61-68 columns, previously ~160) by manually folding the helptopic argdoc help texts at ~70 columns and rendering the generated topic-choice grid with `-choicecolumns 2`. Only the argless `help` overview is strictly 80-column (unchanged, byte-identical); usage tables do not yet wrap to terminal width, so punk::args source line lengths directly set table width — automated (potentially language-dependent) wrapping, e.g. via tcllib's text-adjust facilities, is a possible future improvement.

2
GOALS.md

@ -285,7 +285,7 @@ Detail: goals/G-039-orphan-console-spin.md
Goal: the observed failure mode - an interactive punk902z left running after its hosting terminal/console went away spins roughly a full core indefinitely (observed 2026-07-08: a 37-minute orphan with a single hard-looping thread) - is reliably reproduced and root-caused, then fixed or mitigated so a shell whose console dies exits or reaches zero-CPU idle cleanly.
Acceptance: a documented procedure reproduces the spin on the current kit (e.g. launch an interactive shell in a terminal, then kill/close the hosting terminal or conhost), or the investigation records the attempts made and what evidence would reopen it; the spinning code path is identified (prime suspect: a console read/event loop treating a dead console's immediate EOF/error as retryable without backoff or termination - adjacent to the console-EOF restart path G-038 takes ownership of); after fix/mitigation, the same procedure shows the orphaned process exiting or settling at effectively zero CPU within a short grace period, with live-console interactive behaviour unchanged; the wedge-scoring hazard note (orphans polluting process-liveness checks in test harnesses) is updated to match the outcome.
### G-040 [proposed] punk::args choice aliasing (-choicealiases) with parse normalization, display folding, and doc-lookup parity
### G-040 [achieved 2026-07-08] punk::args choice aliasing (-choicealiases) with parse normalization, display folding, and doc-lookup parity
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/
Detail: goals/G-040-punkargs-choicealiases.md
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.

35
goals/G-040-punkargs-choicealiases.md

@ -1,6 +1,6 @@
# G-040 punk::args choice aliasing (-choicealiases) with parse normalization, display folding, and doc-lookup parity
Status: proposed
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/
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).
@ -75,6 +75,39 @@ keys stay canonical-only.
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

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.4.9"
version = "0.4.10"

28
src/modules/punk-999999.0a1.0.tm

@ -8464,16 +8464,32 @@ namespace eval punk {
#(re)generate the punk::args definitions for ::punk::help and ::punk::help_chunks from the registry
#so that 'i help' shows a documented topic table matching the registered topics.
#minimum-prefix policy for topic words (user decision 2026-07-08):
# - prefixes of the 'help' alias are denied: h/he/hel stay available as command
# words ('help' itself must be typed in full)
# - c, to and tc are reserved phantoms so they fall through to command lookup
# (co.../top.../full 'tcl' still match; e/en and te/ter fall through naturally
# as ambiguous prefixes of env|environment and term|terminal)
variable topic_prefixdenylist {help}
variable topic_prefixreservelist {c to tc}
proc define_docs {} {
variable topics
variable topic_prefixdenylist
variable topic_prefixreservelist
#G-040: canonical topic names are the choices; registry aliases become
#-choicealiases (accepted + normalized at parse, folded into the canonical
#entry in usage display). choiceinfo/choicelabels key on canonicals only.
set choices [list]
set choicealiases [dict create]
set choiceinfo [dict create]
set choicelabels [dict create]
dict for {topic tinfo} $topics {
foreach name [list $topic {*}[dict get $tinfo aliases]] {
lappend choices $name
dict set choiceinfo $name [list [list doctype punkargs] [list subhelp [namespace current]::$topic]]
dict set choicelabels $name [dict get $tinfo summary]
lappend choices $topic
dict set choiceinfo $topic [list [list doctype punkargs] [list subhelp [namespace current]::$topic]]
dict set choicelabels $topic [dict get $tinfo summary]
foreach al [dict get $tinfo aliases] {
dict set choicealiases $al $topic
}
}
#keep help-text lines manually folded (~70 cols) - the usage tables don't yet
@ -8488,7 +8504,7 @@ namespace eval punk {
command info (type and synopsis) is shown for a
resolvable command, or the resolved path for an
external executable.}
set topichelp "Help topic, or command words for basic command info.\n(topics must match exactly - no prefix matching)"
set topichelp "Help topic, or command words for basic command info.\nTopics accept their aliases and unique prefixes\n(some short words deliberately fall through to command lookup)."
set specs [list]
lappend specs ::punk::help help "Punk shell help system." ""
lappend specs ::punk::help_chunks punk::help_chunks "Punk shell help system - content as {channel text} chunks." "\n\nhelp_chunks returns the help content as a list of\n{channel text} chunks rather than emitting it."
@ -8497,7 +8513,7 @@ namespace eval punk {
append def "@id -id $id" \n
append def "@cmd -name $name -summary \"$summary\" -help \"$basehelp$extra\"" \n
append def "@leaders -min 0 -max -1" \n
append def "topic -optional 1 -multiple 1 -type string -choiceprefix 0 -choicerestricted 0 -choicecolumns 2 -choices {$choices} -choiceinfo {$choiceinfo} -choicelabels {$choicelabels} -help \"$topichelp\"" \n
append def "topic -optional 1 -multiple 1 -type string -choiceprefix 1 -choicerestricted 0 -choicecolumns 2 -choices {$choices} -choicealiases {$choicealiases} -choiceprefixdenylist {$topic_prefixdenylist} -choiceprefixreservelist {$topic_prefixreservelist} -choiceinfo {$choiceinfo} -choicelabels {$choicelabels} -help \"$topichelp\"" \n
append def "@values -min 0 -max 0"
if {[punk::args::id_exists $id]} {
#quiet undefine - redefinition on registry change is expected, not noteworthy

3
src/modules/punk-buildversion.txt

@ -1,6 +1,7 @@
0.2.2
0.2.3
#First line must be a semantic version number
#all other lines are ignored.
#0.2.3 - punk::help topic definition adopts punk::args -choicealiases (G-040): choices are the four canonical topics with registry aliases folded ('i help' shows one entry per topic with an (alias:...) note), unique prefixes of topics and aliases accepted, minimum-prefix policy per user decision recorded in ::punk::helptopic (denylist {help}: h/he/hel stay command words; reservelist {c to tc}: fall through to command lookup); unrecognised words still fall through to basic command info; argless 'help' overview unchanged
#0.2.2 - documentation-only: helptopic argdoc help texts manually folded (~70 cols) and the generated ::punk::help topic choices grid set to -choicecolumns 2 - 'i help'/'i help <topic>' tables now render at reasonable widths (61-68 cols, was ~160); usage tables don't yet wrap to terminal width so source line lengths set table width; argless 'help' overview output unchanged (strict 80-col layout preserved)
#0.2.1 - 'help tcl' warning scan extended to the has_libbug_* check family (bundled/vendored library bugs, e.g. the G-036 tcludp detection) alongside has_tclbug_*; buginfo 'url' key supported for reference links to non tcl-core trackers; fixed latent unset-indent error when a triggered check had a bugref/url but no description
#0.2.0 - help system restructured onto a topic registry (::punk::helptopic: register/resolve, per-topic handler procs each with a punk::args definition); ::punk::help and ::punk::help_chunks punk::args definitions (re)generated from the registry so 'i help' / 'i help <topic>' render documented usage; 'help topics' derived from the registry; command-fallthrough and no-arg overview output unchanged; 'help env' degrades cleanly when punk::config is not initialised; help table objects destroyed after printing (leak fix)

371
src/modules/punk/args-999999.0a1.0.tm

@ -911,6 +911,25 @@ tcl::namespace::eval punk::args {
env, envi, ... environment all normalize to environment.
(To require a longer minimum, reserve each shorter form:
reserving {en env} raises the minimum to envi.)
-choicealiases {<dict>}
Dictionary mapping alias names to canonical choice names.
An alias is accepted wherever its canonical choice is: an
exact alias match works regardless of -choiceprefix, and
when -choiceprefix is true alias names participate in
prefix calculation so a unique prefix of an alias also
matches. A matched alias is normalized: the parse result
contains the canonical choice, never the alias.
Aliases are not displayed as separate choice entries -
usage display folds them into the canonical entry as an
'(alias: name)' note on its label. -choicelabels keys
should be canonical names only.
An alias listed in -choiceprefixdenylist must be typed
in full (denial applies to the alias name; a canonical
reached via its alias is not subject to the canonical's
own denylist entry).
Each alias must map to an existing choice, and must not
itself collide with a choice (validated when the
definition is resolved).
-choicegroups {<dict>}
Generally this would be used instead of -choices to allow
usage display of choices grouped by some name (or the empty
@ -2642,6 +2661,17 @@ tcl::namespace::eval punk::args {
}
tcl::dict::set spec_merged -mash $specval
}
-choicealiases {
#dict of alias -> canonical choice (goal G-040).
#Aliases are accepted at parse (exact, and as prefix-calculation members when
#-choiceprefix applies) and normalize to their canonical choice in the parse
#result; usage display folds them into the canonical entry.
#Cross-validation against the final choice set happens after all specs merge.
if {![punk::args::lib::string_is_dict $specval]} {
error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a dictionary of alias -> canonical-choice @id:$DEF_definition_id"
}
tcl::dict::set spec_merged $spec $specval
}
-unindentedfields -
-solo -
-choices - -choicegroups - -choicemultiple - -choicecolumns -
@ -2761,7 +2791,7 @@ tcl::namespace::eval punk::args {
-minsize -maxsize -choices -choicegroups
-mincap -maxcap
-choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted
-choicelabels -choiceinfo
-choicelabels -choiceinfo -choicealiases
-unindentedfields
-nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help
-multipleunique -choicemultipleunique -choicemultipleuniqueset
@ -2774,6 +2804,21 @@ tcl::namespace::eval punk::args {
}
} ;# end foreach {spec specval} argdef_values
#cross-validate -choicealiases against the final merged choice set (goal G-040)
if {[tcl::dict::exists $spec_merged -choicealiases] && [tcl::dict::size [tcl::dict::get $spec_merged -choicealiases]]} {
set ca_allchoices [punk::args::system::Dict_getdef $spec_merged -choices {}]
foreach {_cagroup camembers} [punk::args::system::Dict_getdef $spec_merged -choicegroups {}] {
lappend ca_allchoices {*}$camembers
}
tcl::dict::for {ca_alias ca_canonical} [tcl::dict::get $spec_merged -choicealiases] {
if {$ca_canonical ni $ca_allchoices} {
error "punk::args::resolve - -choicealiases for argument '$argname' maps alias '$ca_alias' to '$ca_canonical' which is not in -choices/-choicegroups @id:$DEF_definition_id"
}
if {$ca_alias in $ca_allchoices} {
error "punk::args::resolve - -choicealiases for argument '$argname' alias '$ca_alias' collides with a defined choice @id:$DEF_definition_id"
}
}
}
if {$is_opt} {
#tcl::dict::set FDICT ARG_CHECKS $argname {*}{
@ -4811,12 +4856,15 @@ tcl::namespace::eval punk::args {
if {$has_choices} {
if {$help ne ""} {append help \n}
#G-040: alias names participate in prefix calculation (as at parse time)
set choicealiases_display [Dict_getdef $arginfo -choicealiases {}]
set choicealias_names [dict keys $choicealiases_display]
if {[dict get $arginfo -nocase]} {
set casemsg " (case insensitive)"
set allchoices_prefixcalc [list {*}[string tolower $allchoices_originalcase] {*}$choiceprefixreservelist]
set allchoices_prefixcalc [list {*}[string tolower $allchoices_originalcase] {*}[string tolower $choicealias_names] {*}$choiceprefixreservelist]
} else {
set casemsg " (case sensitive)"
set allchoices_prefixcalc [list {*}$allchoices_originalcase {*}$choiceprefixreservelist]
set allchoices_prefixcalc [list {*}$allchoices_originalcase {*}$choicealias_names {*}$choiceprefixreservelist]
}
if {[dict get $arginfo -choiceprefix]} {
set prefixmsg " (choice prefix allowed)"
@ -4825,6 +4873,22 @@ tcl::namespace::eval punk::args {
}
set choicelabeldict [Dict_getdef $arginfo -choicelabels {}]
set choiceinfodict [Dict_getdef $arginfo -choiceinfo {}]
#G-040: aliases are not displayed as separate choice entries - fold each
#canonical's alias names into its label so every display path shows them once
if {[dict size $choicealiases_display]} {
set aliasesbycanonical [dict create]
dict for {ca_al ca_cn} $choicealiases_display {
dict lappend aliasesbycanonical $ca_cn $ca_al
}
dict for {ca_cn ca_als} $aliasesbycanonical {
set aliasnote "(alias[expr {[llength $ca_als] > 1 ? {es} : {}}]: [join $ca_als |])"
if {[dict exists $choicelabeldict $ca_cn]} {
dict set choicelabeldict $ca_cn "$aliasnote\n[dict get $choicelabeldict $ca_cn]"
} else {
dict set choicelabeldict $ca_cn $aliasnote
}
}
}
set formattedchoices [dict create] ;#use dict rather than array to preserve order
if {$help eq ""} {
#first line of help - no included base of 4 indent is the normal state of first help lines in definitions scripts
@ -8074,6 +8138,155 @@ tcl::namespace::eval punk::args {
#rename get_dict
#
#G-040: the single implementation of choice-word matching - shared by argument parsing
#(get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so 'i <cmd> <word>'
#resolution can never diverge from what parsing accepts.
#Arguments:
# word - the supplied word (callers pass the check-form, e.g. ansistripped when applicable)
# nocase - value of -nocase for the argument
# allchoices - flat list of -choices plus all -choicegroups members (dups allowed)
# choicealiases - -choicealiases dict (alias -> canonical choice)
# choiceprefix - value of -choiceprefix
# denylist - -choiceprefixdenylist (full word required for these names)
# reservelist - -choiceprefixreservelist (phantom prefix-calculation members)
#Returns dict:
# matched - boolean: word identifies a choice (directly, by unique prefix, or via an alias)
# exact - boolean: the raw stored word already equals the resulting choice (no rewrite needed)
# canonical - the resulting choice value (empty when not matched)
proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} {
set aliasnames [tcl::dict::keys $choicealiases]
set has_choicealiases [expr {[llength $aliasnames] > 0}]
set choicealiases_nocase [tcl::dict::create]
if {$has_choicealiases} {
tcl::dict::for {ca_al ca_cn} $choicealiases {
tcl::dict::set choicealiases_nocase [tcl::string::tolower $ca_al] $ca_cn
}
}
if {$nocase} {
set choices_test [tcl::string::tolower $allchoices]
set v_test [tcl::string::tolower $word]
} else {
set choices_test $allchoices
set v_test $word
}
set chosen ""
set choice_in_list 0
set choice_exact_match 0
if {$choiceprefix} {
#can we handle empty string as a choice? It should just work - REVIEW/test
if {$word in $allchoices} {
#for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing
set chosen $word
set choice_in_list 1
set choice_exact_match 1
} elseif {$has_choicealiases && [tcl::dict::exists $choicealiases $word]} {
#exact alias match - normalize to the canonical choice
#(deliberately not 'exact' - the stored raw value must be overwritten with the canonical)
set chosen [tcl::dict::get $choicealiases $word]
set choice_in_list 1
} elseif {$nocase && $v_test in $choices_test} {
#full-length match except for case
#select the case from the choice list - not the supplied value
foreach avail [lsort -unique $allchoices] {
if {[tcl::string::match -nocase $word $avail]} {
set chosen $avail
}
}
#assert chosen will always get set
set choice_in_list 1
} elseif {$has_choicealiases && $nocase && [tcl::dict::exists $choicealiases_nocase $v_test]} {
#exact alias match differing only by case - normalize to the canonical choice
set chosen [tcl::dict::get $choicealiases_nocase $v_test]
set choice_in_list 1
} else {
#PREFIX check required - any match here is not an exact match or it would have matched above.
#in this block we can treat empty result from prefix match as a non-match
set prefix_via_alias 0 ;#set when the prefix match landed on an alias name (deny already applied to the alias)
if {$nocase} {
#nocase prefixing with case-dups: see the -choiceprefixdenylist nocase notes at the original
#get_dict site - counterintuitive DEL/delete/Delete edge cases are documented feature-not-bug
set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$aliasnames {*}$reservelist] $word]
if {$bestmatch eq "" || $bestmatch in $reservelist} {
set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}[tcl::dict::keys $choicealiases_nocase] {*}$reservelist] $v_test]
if {$chosen ne "" && [tcl::dict::exists $choicealiases_nocase $chosen]} {
#matched an alias (lowercased) - deny applies to the alias name, then normalize
if {[lsearch -nocase $denylist $chosen] >= 0} {
set chosen ""
} else {
set chosen [tcl::dict::get $choicealiases_nocase $chosen]
set prefix_via_alias 1
}
set choice_in_list [expr {$chosen ne ""}]
} else {
#now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing
set chosen [lsearch -inline -nocase $allchoices $chosen]
set choice_in_list [expr {$chosen ne ""}]
}
} else {
if {$bestmatch in $aliasnames} {
#prefix landed on an alias - deny applies to the alias name, then normalize
if {$bestmatch in $denylist} {
set chosen ""
set choice_in_list 0
} else {
set chosen [tcl::dict::get $choicealiases $bestmatch]
set choice_in_list 1
set prefix_via_alias 1
}
} else {
set chosen $bestmatch
set choice_in_list 1
}
}
} else {
set matchedname [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$aliasnames {*}$reservelist] $word]
if {$matchedname eq "" || $matchedname in $reservelist} {
set chosen ""
set choice_in_list 0
} elseif {$matchedname in $aliasnames} {
#prefix landed on an alias - deny applies to the alias name, then normalize
if {$matchedname in $denylist} {
set chosen ""
set choice_in_list 0
} else {
set chosen [tcl::dict::get $choicealiases $matchedname]
set choice_in_list 1
set prefix_via_alias 1
}
} else {
set chosen $matchedname
set choice_in_list 1
}
}
#don't allow prefixing for elements from -choiceprefixdenylist
#we still use all elements to calculate the prefixes though
#(a canonical reached via an alias is exempt - deny was already applied to the alias name)
#review - case difference edge cases in choiceprefixdenylist !todo
if {!$prefix_via_alias && $chosen in $denylist} {
set choice_in_list 0
set chosen ""
}
}
} else {
#-choiceprefix false - exact matching only
if {$v_test in $choices_test} {
#value as stored is ok (raw word kept - historical behaviour, including nocase raw retention)
set chosen $word
set choice_in_list 1
set choice_exact_match 1
} elseif {$has_choicealiases && [tcl::dict::exists $choicealiases $word]} {
#exact alias match - normalize to the canonical choice
set chosen [tcl::dict::get $choicealiases $word]
set choice_in_list 1
} elseif {$has_choicealiases && $nocase && [tcl::dict::exists $choicealiases_nocase $v_test]} {
#exact alias match differing only by case - normalize to the canonical choice
set chosen [tcl::dict::get $choicealiases_nocase $v_test]
set choice_in_list 1
}
}
return [tcl::dict::create matched $choice_in_list exact $choice_exact_match canonical $chosen]
}
#generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values
#If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags.
#only supports -flag val pairs, not solo options
@ -10023,6 +10236,10 @@ tcl::namespace::eval punk::args {
set choiceprefix [tcl::dict::get $thisarg -choiceprefix]
set choiceprefixdenylist [Dict_getdef $thisarg -choiceprefixdenylist {}]
set choiceprefixreservelist [Dict_getdef $thisarg -choiceprefixreservelist {}]
#-choicealiases (G-040): alias -> canonical choice. Accepted exact (any -choiceprefix
#setting) and as prefix-calculation members when -choiceprefix is true; matched aliases
#normalize to their canonical choice in the parse result (see choiceword_match).
set choicealiases [Dict_getdef $thisarg -choicealiases {}]
set choicerestricted [tcl::dict::get $thisarg -choicerestricted]
set choicemultiple [tcl::dict::get $thisarg -choicemultiple]
if {[string is integer -strict $choicemultiple]} {
@ -10097,122 +10314,60 @@ tcl::namespace::eval punk::args {
set choice_in_list 0
set matches_default [expr {$has_default && $c eq $defaultval}] ;# defaultval could be a list when -choicemultiple?
if {!$matches_default} {
if {$choiceprefix} {
#can we handle empty string as a choice? It should just work - REVIEW/test
set choice_exact_match 0
if {$c_check in $allchoices} {
#for case when there are case-differenced duplicates - allow exact match to avoid selecting earlier match of another casing
set chosen $c_check
set choice_in_list 1
set choice_exact_match 1
} elseif {$v_test in $choices_test} {
#assert - if we're here, nocase must be true
#we know choice is present as full-length match except for case
#now we want to select the case from the choice list - not the supplied value
#we don't set choice_exact_match - because we will need to override the optimistic existing val below
#review
foreach avail [lsort -unique $allchoices] {
if {[string match -nocase $c $avail]} {
set chosen $avail
}
}
#assert chosen will always get set
set choice_in_list 1
} else {
#puts ">>>> choiceprefixreservelist: $choiceprefixreservelist"
#PREFIX check required - any 'chosen' here is not an exact match or it would have matched above.
#assert - if empty string was a provided choice and empty string was a provided arg - we would have matched above.
#in this block we can treat empty result from prefix match as a non-match
if {$nocase} {
#nocase implies that our entered value doesn't have to match case of choices -
#but we would still like to select the best match if there are case-dups.
#e.g arg -choices {delete Delete} -nocase 1 -choiceprefixdenylist delete
# selecting Del will find Delete, del will match delete (and raise error)
# but DEL will also match delete rather than Delete - so again an error is raised.
#This is counterintuitive with -nocase
#This is probably such an edge case that best served with documentation as a feature-not-bug
#Rationale being that in a nocase situation it's arbitrary/counterintuitive to consider DEL, or DeL a better match for Delete than delete?
#The choice of the user to use -choiceprefixdenylist along with case-dups is the issue.
set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check]
if {$bestmatch eq "" || $bestmatch in $choiceprefixreservelist} {
set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}$choiceprefixreservelist] $v_test]
#now pick the earliest match in the actually defined list so that case of chosen always matches a defined entry with casing
set chosen [lsearch -inline -nocase $allchoices $chosen]
set choice_in_list [expr {$chosen ne ""}]
#G-040: choice-word matching delegated to the shared resolver
#(punk::args::choiceword_match - also consumed by the punk::ns doc-lookup
# walk, so 'i <cmd> <word>' resolution cannot diverge from parsing)
set matchinfo [choiceword_match $c_check $nocase $allchoices $choicealiases $choiceprefix $choiceprefixdenylist $choiceprefixreservelist]
set choice_in_list [tcl::dict::get $matchinfo matched]
set choice_exact_match [tcl::dict::get $matchinfo exact]
set chosen [tcl::dict::get $matchinfo canonical]
#override the optimistic existing val (prefix-normalized or alias-normalized matches)
#our existing values in $dname are not list-protected - so we need to check clause_size
if {$choice_in_list && !$choice_exact_match} {
set existing [tcl::dict::get [set $dname] $argname_or_ident]
if {$choicemultiple_max != -1 && $choicemultiple_max < 2} {
#single choice allowed per clause-member
if {$is_multiple} {
if {$clause_size == 1} {
#no list wrapping of single element in $dname dict - so don't index into it with element_index
#lset existing $element_index $chosen ;#wrong - test::punk::args test: choice_multiple_with_choiceprefix.
lset existing $clause_index $chosen
} else {
set chosen $bestmatch
set choice_in_list 1
lset existing $clause_index $element_index $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
} else {
set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$choiceprefixreservelist] $c_check]
if {$chosen eq "" || $chosen in $choiceprefixreservelist} {
set choice_in_list 0
} else {
set choice_in_list 1
}
}
#override choice_in_list if in deny list
#don't allow prefixing for elements from -choiceprefixdenylist
#we still use all elements to calculate the prefixes though
#review - case difference edge cases in choiceprefixdenylist !todo
if {$chosen in $choiceprefixdenylist} {
set choice_in_list 0
set chosen ""
#test: choice_multielement_clause
lset existing $element_index $chosen
tcl::dict::set $dname $argname_or_ident $existing
}
}
#override the optimistic existing val
#our existing values in $dname are not list-protected - so we need to check clause_size
if {$choice_in_list && !$choice_exact_match} {
set existing [tcl::dict::get [set $dname] $argname_or_ident]
if {$choicemultiple_max != -1 && $choicemultiple_max < 2} {
#single choice allowed per clause-member
if {$is_multiple} {
if {$clause_size == 1} {
#no list wrapping of single element in $dname dict - so don't index into it with element_index
#lset existing $element_index $chosen ;#wrong - test::punk::args test: choice_multiple_with_choiceprefix.
lset existing $clause_index $chosen
} else {
lset existing $clause_index $element_index $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
} else {
if {$is_multiple} {
#puts ">>> existing $existing $choice_idx"
if {$clause_size == 1} {
#no list wrapping of single element in $dname dict - so don't index into it with element_index
lset existing $clause_index $choice_idx $chosen
} else {
#test: choice_multielement_clause
lset existing $element_index $chosen
tcl::dict::set $dname $argname_or_ident $existing
lset existing $clause_index $element_index $choice_idx $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
} else {
if {$is_multiple} {
#puts ">>> existing $existing $choice_idx"
if {$clause_size == 1} {
#no list wrapping of single element in $dname dict - so don't index into it with element_index
lset existing $clause_index $choice_idx $chosen
} else {
lset existing $clause_index $element_index $choice_idx $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
#test required.
# punk::args::parse {{read write w}} withdef @values {mode -type list -choices {read write} -choicemultiple {1 -1}}
#puts ">>> clause_size $clause_size"
#puts ">>> existing $existing"
#puts ">>> lset existing $element_index $choice_idx $chosen"
if {$clause_size == 1} {
#e.g -type list
#we have multiple choices allowed for a single element clause because that clause type is a list.
lset existing $choice_idx $chosen
} else {
#test required.
# punk::args::parse {{read write w}} withdef @values {mode -type list -choices {read write} -choicemultiple {1 -1}}
#puts ">>> clause_size $clause_size"
#puts ">>> existing $existing"
#puts ">>> lset existing $element_index $choice_idx $chosen"
if {$clause_size == 1} {
#e.g -type list
#we have multiple choices allowed for a single element clause because that clause type is a list.
lset existing $choice_idx $chosen
} else {
#e.g -type {any any}
lset existing $element_index $choice_idx $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
#e.g -type {any any}
lset existing $element_index $choice_idx $chosen
}
tcl::dict::set $dname $argname_or_ident $existing
}
}
} else {
#value as stored in $dname is ok
set choice_in_list [expr {$v_test in $choices_test}]
}
}

3
src/modules/punk/args-buildversion.txt

@ -1,5 +1,6 @@
0.2.3
0.3.0
#First line must be a semantic version number
#all other lines are ignored.
#0.3.0 - new: -choicealiases {alias canonical ...} argument option (G-040) - aliases accepted exact under any -choiceprefix/-nocase setting, join the prefix-calculation pool when -choiceprefix is true, and normalize to their canonical choice in parse results; deny applies to the matched name (alias deny requires full alias; canonical reached via alias exempt from the canonical's own deny); usage display folds aliases into the canonical entry's label as an (alias:...) note and includes alias names in display prefix calculation; alias->existing-choice and no-collision validated at definition resolve. new public helper punk::args::choiceword_match - the single choice-word matching implementation extracted from get_dict, also consumed by punk::ns cmd_traverse for doc-lookup parity
#0.2.3 - fixed "@opts -any 1" adhoc option passthrough: an option not in the definition crashed get_dict ("can't read argname") at the name-mapping stage instead of passing through - adhoc opts now fall back to their raw supplied name (argstate/arg_checks already carried them); commented out a debug puts stderr (">>>_get_dict_can_assign_value NOT alloc_ok...") that fired on every failed clause type assignment (e.g. any multiform parse miss)
#0.2.2 - documentation-only: define doc clarifies -choicerestricted 0 passthrough (non-matching/ambiguous/denied/reserved words accepted as ordinary values), -choiceprefix result normalization to the canonical choice + minimal-prefix display highlighting, -choiceprefixdenylist behaviour for shorter prefixes in both restricted modes, and -choiceprefixreservelist phantom-entry idiom for per-choice minimum-prefix control

24
src/modules/punk/ns-999999.0a1.0.tm

@ -5154,16 +5154,26 @@ y" {return quirkykeyscript}
return [list 3 $origin $resolvedargs [list {*}$eparams {*}$queryargs_untested] $docid]
break
}
set resolved_q [tcl::prefix::match -error "" $allchoices $q]
if {$resolved_q eq ""} {
#G-040: resolve the subcommand word with the same shared resolver argument
#parsing uses (punk::args::choiceword_match) - so aliases normalize to their
#canonical (choiceinfo is keyed on canonicals), -choiceprefixdenylist and
#-choiceprefixreservelist are honoured, and 'i <cmd> <word>' can never accept
#a word that parsing would reject.
set ct_matchinfo [punk::args::choiceword_match $q\
[punk::args::system::Dict_getdef $arginfo -nocase 0]\
$allchoices\
[punk::args::system::Dict_getdef $arginfo -choicealiases {}]\
[dict get $arginfo -choiceprefix]\
[punk::args::system::Dict_getdef $arginfo -choiceprefixdenylist {}]\
[punk::args::system::Dict_getdef $arginfo -choiceprefixreservelist {}]\
]
if {![dict get $ct_matchinfo matched]} {
#no match under parse rules (covers: unknown word, ambiguous prefix,
#reserved word, denied prefix, and non-exact word when -choiceprefix 0)
return [list 4 $origin $resolvedargs $queryargs_untested $docid]
break
}
if {![dict get $arginfo -choiceprefix] && $resolved_q ne $q} {
#a unique prefix is not sufficient for this arg
return [list 5 $origin $resolvedargs $queryargs_untested $docid]
break
}
set resolved_q [dict get $ct_matchinfo canonical]
#if {$resolved_q ne $q} {

3
src/modules/punk/ns-buildversion.txt

@ -1,4 +1,5 @@
0.1.1
0.1.2
#First line must be a semantic version number
#all other lines are ignored.
#0.1.2 - cmd_traverse subcommand walk resolves choice words via the shared punk::args::choiceword_match resolver (G-040 parity): -choiceprefixdenylist and -choiceprefixreservelist are now honoured in doc lookup (previously ignored - 'i <cmd> <word>' could accept words parsing rejects), -choicealiases normalize to their canonical before choiceinfo lookup, and -nocase is honoured in the walk
#0.1.1 - commented out five development trace puts in the doc-lookup machinery: "PROC auto def"/"ENSEMBLE auto def" (generate_autodef - emitted on STDOUT, polluting 'i'/'s' output in script/exec contexts), "cmd_traverse - skipping to documented subcommand" (space-form id path), "---> cmd_traverse ensembleparam" (ensemble -parameters traversal), and "cmd_traverse 10 ... - review" (fallthrough return). No functional change.

133
src/tests/modules/punk/args/testsuites/args/choices.test

@ -352,6 +352,139 @@ namespace eval ::testspace {
topics\
lindex\
]
# ---- -choicealiases (G-040) ----
test choicealiases_exact_and_prefix_normalize {alias exact match and unique alias prefix normalize to the canonical choice}\
-setup $common -body {
set docid ::testspace::choicealiases_exact_and_prefix_normalize
punk::args::define [list @id -id $docid] @values {mode -choices {read write} -choicealiases {rd read wr write}}
set argd [punk::args::parse {rd} withid $docid]
lappend result [dict get $argd values]
#canonical exact and canonical prefix still work
set argd [punk::args::parse {write} withid $docid]
lappend result [dict get $argd values]
set argd [punk::args::parse {rea} withid $docid]
lappend result [dict get $argd values]
#'w' is a prefix of both the choice 'write' and the alias 'wr' (same target) - currently ambiguous
if {[catch {punk::args::parse {w} withid $docid}]} {
lappend result w-ambiguous-rejected
} else {
lappend result w-UNEXPECTEDLY-accepted
}
}\
-cleanup {
punk::args::undefine $docid 1
}\
-result [list\
{mode read}\
{mode write}\
{mode read}\
w-ambiguous-rejected\
]
test choicealiases_noprefix_exact_only {with -choiceprefix 0 an exact alias still matches and normalizes; alias prefixes do not}\
-setup $common -body {
set docid ::testspace::choicealiases_noprefix_exact_only
punk::args::define [list @id -id $docid] @values {mode -choiceprefix 0 -choices {read write} -choicealiases {rd read}}
set argd [punk::args::parse {rd} withid $docid]
lappend result [dict get $argd values]
set argd [punk::args::parse {read} withid $docid]
lappend result [dict get $argd values]
if {[catch {punk::args::parse {r} withid $docid}]} {
lappend result r-prefix-rejected
} else {
lappend result r-prefix-UNEXPECTEDLY-accepted
}
}\
-cleanup {
punk::args::undefine $docid 1
}\
-result [list\
{mode read}\
{mode read}\
r-prefix-rejected\
]
test choicealiases_nocase {aliases participate in -nocase matching and normalize to the canonical}\
-setup $common -body {
set docid ::testspace::choicealiases_nocase
punk::args::define [list @id -id $docid] @values {mode -nocase 1 -choices {read write} -choicealiases {rd read}}
set argd [punk::args::parse {RD} withid $docid]
lappend result [dict get $argd values]
set argd [punk::args::parse {READ} withid $docid]
lappend result [dict get $argd values]
}\
-cleanup {
punk::args::undefine $docid 1
}\
-result [list\
{mode read}\
{mode read}\
]
test choicealiases_denylist_applies_to_alias_name {an alias in -choiceprefixdenylist requires the full alias; exact alias still normalizes}\
-setup $common -body {
set docid ::testspace::choicealiases_denylist_applies_to_alias_name
punk::args::define [list @id -id $docid] {@leaders -min 0 -max -1} {topic -optional 1 -multiple 1 -choicerestricted 0 -choiceprefix 1 -choices {topics tcl env console} -choicealiases {help topics environment env term console terminal console} -choiceprefixdenylist {help} -choiceprefixreservelist {c to tc}} {@values -min 0 -max 0}
#the punk::help topic policy matrix (user-decided 2026-07-08: c/to/tc fall through)
foreach word {help hel h environment environ env en term terminal termi ter c to tc co top tcl lindex} {
set argd [punk::args::parse [list $word] withid $docid]
lappend result [dict get [dict get $argd leaders] topic]
}
set result
}\
-cleanup {
punk::args::undefine $docid 1
}\
-result [list\
topics\
hel\
h\
env\
env\
env\
en\
console\
console\
console\
ter\
c\
to\
tc\
console\
topics\
tcl\
lindex\
]
test choicealiases_resolve_validation {alias to a missing canonical or colliding with a choice fails at definition resolve time}\
-setup $common -body {
set docid ::testspace::choicealiases_resolve_validation_a
punk::args::define [list @id -id $docid] @values {v -choices {a b} -choicealiases {x q}}
#define is lazy - the validation error surfaces at first resolve/parse
if {[catch {punk::args::parse {a} withid $docid} errmsg]} {
lappend result [string match "*is not in -choices/-choicegroups*" $errmsg]
} else {
lappend result missing-canonical-UNEXPECTEDLY-accepted
}
set docid2 ::testspace::choicealiases_resolve_validation_b
punk::args::define [list @id -id $docid2] @values {v -choices {a b} -choicealiases {a b}}
if {[catch {punk::args::parse {b} withid $docid2} errmsg]} {
lappend result [string match "*collides with a defined choice*" $errmsg]
} else {
lappend result collision-UNEXPECTEDLY-accepted
}
}\
-cleanup {
punk::args::undefine $docid 1
punk::args::undefine $docid2 1
}\
-result [list 1 1]
}
tcltest::cleanupTests ;#needed to produce test summary line.

78
src/tests/modules/punk/ns/testsuites/ns/cmdflow.test

@ -2,13 +2,15 @@ package require tcltest
package require punk::args
package require punk::ns
package require punk::ansi
#Tests for the command help/doc-lookup flow: punk::ns::cmdwhich -> cmdinfo -> cmd_traverse
#docid resolution, including punk::args choiceinfo subhelp mapping and the -choiceprefix /
#-choiceprefixdenylist / -choiceprefixreservelist handling of subcommand words.
#Characterization added 2026-07-08 ahead of G-040 (choice aliasing + parse/doc-lookup parity):
#tests marked GAP pin the CURRENT divergence between punk::args::parse and the cmd_traverse
#doc walk (deny/reserve lists honoured by parse, ignored by the walk). G-040 flips those pins.
#-choiceprefixdenylist / -choiceprefixreservelist / -choicealiases handling of subcommand words.
#Characterization added 2026-07-08 ahead of G-040; the parity GAP pins (deny/reserve honoured
#by parse but ignored by the doc walk) were flipped to agreement the same day when G-040
#landed: cmd_traverse now resolves subcommand words via the shared punk::args::choiceword_match
#resolver, so the walk and the parser cannot diverge.
namespace eval ::testspace {
namespace import ::tcltest::*
@ -95,6 +97,19 @@ namespace eval ::testspace {
@values -min 0 -max 0
}
#as flowparent but with a -choicealiases entry (first -> alpha) - G-040
proc flowaliased {args} {}
punk::args::define {
@id -id ::testspace::flowaliased
@cmd -name testspace::flowaliased -summary "parent aliased" -help "parent aliased"
@leaders -min 1 -max 1
subcmd -choices {alpha beta} -choicealiases {first alpha} -choiceinfo {
alpha {{doctype punkargs} {subhelp ::testspace::flowsub_alpha}}
beta {{doctype punkargs} {subhelp ::testspace::flowsub_beta}}
}
@values -min 0 -max 0
}
#undocumented command whose subcommand is documented via a space-delimited id
proc flowgap {args} {}
punk::args::define {
@ -181,12 +196,10 @@ namespace eval ::testspace {
}\
-result [list {::testspace::flowgap deep}]
#--- parse vs doc-walk parity (GAP pins pending G-040) -----------------------------------
#punk::args::parse honours -choiceprefixdenylist/-choiceprefixreservelist; the cmd_traverse
#doc walk (punk::ns) consults only -choiceprefix and ignores both lists. These tests pin
#the CURRENT divergent behaviour; G-040's parity work flips them to agreement.
#--- parse vs doc-walk parity (G-040 - previously pinned as GAPs, flipped when the walk
#adopted the shared punk::args::choiceword_match resolver) ------------------------------
test cmdinfo_parity_denylist_GAP {GAP (G-040): parse rejects a denied prefix but the doc walk still resolves it}\
test cmdinfo_parity_denylist {parse and the doc walk agree: a denied prefix resolves for neither}\
-setup $common -body {
#parse side: denied prefix is an error (restricted choices)
if {[catch {punk::args::parse {al} withid ::testspace::flowdeny}]} {
@ -197,15 +210,18 @@ namespace eval ::testspace {
#full word accepted by parse
set argd [punk::args::parse {alpha} withid ::testspace::flowdeny]
lappend result [dict get [dict get $argd leaders] subcmd]
#doc-walk side: the same denied prefix currently resolves to the subhelp docid
#doc-walk side: the denied prefix no longer resolves - docid stays at the parent
set cinfo [punk::ns::cmdinfo ::testspace::flowdeny al]
lappend result [dict get $cinfo docid]
#doc-walk side: the full denied word still resolves (deny only blocks prefixes)
set cinfo [punk::ns::cmdinfo ::testspace::flowdeny alpha]
lappend result [dict get $cinfo docid]
}\
-cleanup {
}\
-result [list parse-rejects-denied-prefix alpha ::testspace::flowsub_alpha]
-result [list parse-rejects-denied-prefix alpha ::testspace::flowdeny ::testspace::flowsub_alpha]
test cmdinfo_parity_reservelist_GAP {GAP (G-040): parse blocks a reserved prefix but the doc walk still resolves it}\
test cmdinfo_parity_reservelist {parse and the doc walk agree: a reserved word resolves for neither}\
-setup $common -body {
#parse side: reserved word blocks itself and shorter prefixes
if {[catch {punk::args::parse {en} withid ::testspace::flowreserve}]} {
@ -219,13 +235,31 @@ namespace eval ::testspace {
#doc-walk side agreement for the beyond-phantom prefix
set cinfo [punk::ns::cmdinfo ::testspace::flowreserve env]
lappend result [dict get $cinfo docid]
#doc-walk side: the reserved word currently resolves anyway
#doc-walk side: the reserved word no longer resolves - docid stays at the parent
set cinfo [punk::ns::cmdinfo ::testspace::flowreserve en]
lappend result [dict get $cinfo docid]
}\
-cleanup {
}\
-result [list parse-rejects-reserved-word environment ::testspace::flowsub_env ::testspace::flowsub_env]
-result [list parse-rejects-reserved-word environment ::testspace::flowsub_env ::testspace::flowreserve]
test cmdinfo_parity_choicealiases {an alias word (and alias prefix) resolves to the canonical's subhelp docid in both parse and the doc walk}\
-setup $common -body {
#fixture defined below the standard fixtures: choices {alpha beta} with alias first -> alpha
#parse side: alias normalizes to canonical
set argd [punk::args::parse {first} withid ::testspace::flowaliased]
lappend result [dict get [dict get $argd leaders] subcmd]
set argd [punk::args::parse {fir} withid ::testspace::flowaliased]
lappend result [dict get [dict get $argd leaders] subcmd]
#doc-walk side: alias and alias prefix resolve to the CANONICAL's choiceinfo subhelp
set cinfo [punk::ns::cmdinfo ::testspace::flowaliased first]
lappend result [dict get $cinfo docid]
set cinfo [punk::ns::cmdinfo ::testspace::flowaliased fir]
lappend result [dict get $cinfo docid]
}\
-cleanup {
}\
-result [list alpha alpha ::testspace::flowsub_alpha ::testspace::flowsub_alpha]
#--- cmdhelp smoke ------------------------------------------------------------------------
@ -240,6 +274,22 @@ namespace eval ::testspace {
}\
-result [list 1 1]
test cmdhelp_choicealiases_display_folded {usage display folds an alias into its canonical choice entry rather than listing it separately}\
-setup $common -body {
set out [punk::ns::cmdhelp -return string ::testspace::flowaliased]
#the alias is shown as a note on the canonical entry
lappend result [string match "*(alias: first)*" $out]
#and does not appear as its own choice row (a standalone 'first' line)
set standalone 0
foreach ln [split [punk::ansi::ansistrip $out] \n] {
if {[string trim $ln] eq "first"} {set standalone 1}
}
lappend result $standalone
}\
-cleanup {
}\
-result [list 1 0]
#--- ensemble -parameters / -ensembleparameter --------------------------------------------
#Real Tcl ensembles created with 'namespace ensemble create -parameters {...}' take their
#parameter(s) BEFORE the subcommand (the punk::netbox::man pattern: 'man <apicontextid>

Loading…
Cancel
Save