From e45fea9b16287c9fcffc59ee28a96b507316c8d2 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Wed, 8 Jul 2026 16:00:09 +1000 Subject: [PATCH] G-040 proposed (punk::args -choicealiases); choice-flow characterization tests; -choice* doc clarifications - GOALS.md: G-040 punk::args choice aliasing with parse normalization, display folding, and doc-lookup parity (user-approved); detail file records the prefix/deny/reserve collapse analysis from the punk::help work, the cmd_traverse divergence, the c/to/tc minimum-prefix policy question, and the test-first sequencing - new punk/ns cmdflow.test (10 tests): cmdwhich/cmdinfo/cmd_traverse docid flow - direct, via-alias and subhelp-choiceinfo resolution (exact + unique prefix), unknown-word fallback to parent docid, -choiceprefix 0 exact-only, space-delimited ids, cmdhelp -return string smoke; the parse-vs-doc-walk divergence (deny/reserve lists honoured by punk::args::parse, ignored by the cmd_traverse walk) pinned as GAP tests pending G-040 - choices.test: 4 new unrestricted-mode (-choicerestricted 0) tests - prefix normalization to the canonical choice, unknown/ambiguous passthrough, denylist and reservelist passthrough, and the punk::help collapse recipe (prefix+deny+reserve on a -multiple leader) - punk::args 0.2.2 (doc-only): define doc -choice* fields now state the -choicerestricted 0 passthrough semantics, -choiceprefix result normalization + minimal-prefix display highlighting, denylist behaviour for shorter prefixes in both restricted modes, and the reservelist phantom-entry idiom for per-choice minimum-prefix control - suites: punk/args 74 pass + 1 skip (was 70+1), punk/ns 18 pass (was 8) Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 1 + GOALS.md | 6 + goals/G-040-punkargs-choicealiases.md | 102 ++++++++ src/modules/punk/args-999999.0a1.0.tm | 27 ++ src/modules/punk/args-buildversion.txt | 3 +- .../punk/args/testsuites/args/choices.test | 105 ++++++++ .../punk/ns/testsuites/ns/cmdflow.test | 243 ++++++++++++++++++ 7 files changed, 486 insertions(+), 1 deletion(-) create mode 100644 goals/G-040-punkargs-choicealiases.md create mode 100644 src/tests/modules/punk/ns/testsuites/ns/cmdflow.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2e2202..eddb4078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Entries are newest-first; one bullet per notable change. See the root `AGENTS.md ## [0.4.4] - 2026-07-08 +- punk::args 0.2.2 (doc-only): the `punk::args::define` documentation for the -choice* fields now spells out the verified interplay semantics — `-choicerestricted 0` passthrough of non-matching/ambiguous/denied/reserved words, `-choiceprefix` normalization of accepted prefixes to the canonical choice in parse results (with minimal-prefix highlighting in usage display), and the `-choiceprefixreservelist` phantom-entry idiom for per-choice minimum-prefix control. Characterization tests added (choices.test unrestricted-mode tests; new punk/ns cmdflow.test for the cmdwhich/cmdinfo/cmd_traverse docid flow, pinning the parse-vs-doc-walk deny/reserve divergence as GAP pending G-040). - help system restructured onto a topic registry (punk module 0.2.0, `::punk::helptopic`): each topic (`topics|help`, `tcl`, `env|environment`, `console|term|terminal`) is a handler proc with its own punk::args definition, and the `::punk::help`/`::punk::help_chunks` definitions are (re)generated from the registry — `i help` now renders a documented usage table with topic choices and summaries, and `i help ` shows per-topic documented usage (previously an autogenerated stub). `help topics` is derived from the registry (lists all aliases, fits 80 columns). No-arg overview and command-fallthrough (`help `) output byte-identical to before; 80-column layout preserved; verified on both generations, script and shell subcommands. `help env` without an initialised punk::config (e.g. script contexts) degrades to a one-line notice instead of an error stack. The registry is the intended seam for future subshell-declared topics (punk::config-gated — not yet a goal). ## [0.4.3] - 2026-07-08 diff --git a/GOALS.md b/GOALS.md index 9f39fecb..c176ef0c 100644 --- a/GOALS.md +++ b/GOALS.md @@ -284,3 +284,9 @@ Scope: src/modules/punk/repl-999999.0a1.0.tm (console reader/event loop and EOF/ 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 +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. +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). diff --git a/goals/G-040-punkargs-choicealiases.md b/goals/G-040-punkargs-choicealiases.md new file mode 100644 index 00000000..2dfbaf23 --- /dev/null +++ b/goals/G-040-punkargs-choicealiases.md @@ -0,0 +1,102 @@ +# G-040 punk::args choice aliasing (-choicealiases) with parse normalization, display folding, and doc-lookup parity + +Status: proposed +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). + +## 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. + +## 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. diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index f3a87dab..60c6c1fd 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -871,19 +871,46 @@ tcl::namespace::eval punk::args { -choicerestricted Whether values not specified in -choices or -choicegroups are allowed. Defaults to true. + When false, an input that matches no choice - including an + ambiguous prefix, a prefix of a -choiceprefixdenylist entry, + or a -choiceprefixreservelist word - is accepted unchanged as + an ordinary value (subject to -type validation) instead of + raising an error. This passthrough is the pattern for + arguments that mix a known choice set with free-form values + (e.g. a help topic word falling through to command lookup). -choiceprefix This specifies whether unique prefixes are able to be used instead of the complete string. This is calculated using tcl::prefix::match - and will display in the autogenerated usage output. Defaults to true. + A matching prefix is normalized: the parse result contains + the full choice string, never the typed prefix - so callers + switch on canonical choice values only. The usage display + highlights each choice's minimal accepted prefix (a choice + in -choiceprefixdenylist displays fully highlighted - the + whole word is required). -choiceprefixdenylist {} These choices should match exactly a choice entry in one of the settings -choices or -choicegroups. These will still be used in prefix calculation - but the full choice argument must be entered to select the choice. + A shorter prefix of a denied choice is an error when + -choicerestricted is true, and passes through as an ordinary + (non-choice) value when -choicerestricted is false - use this + to keep short words available for other purposes (e.g. with + 'help' denied, h/he/hel remain free-form values). -choiceprefixreservelist {} These choices are additional values used in prefix calculation. The values will not be added to the list of available choices. + A reserved word never matches: entered exactly it is an error + when -choicerestricted is true and passes through when false, + as do any shorter words it shadows into ambiguity. This gives + per-choice control of the minimum accepted prefix by reserving + phantom entries: e.g. -choices {environment} + -choiceprefixreservelist {en} means e/en do not match while + env, envi, ... environment all normalize to environment. + (To require a longer minimum, reserve each shorter form: + reserving {en env} raises the minimum to envi.) -choicegroups {} Generally this would be used instead of -choices to allow usage display of choices grouped by some name (or the empty diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index 21362c18..8454cdbe 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,3 +1,4 @@ -0.2.1 +0.2.2 #First line must be a semantic version number #all other lines are ignored. +#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 diff --git a/src/tests/modules/punk/args/testsuites/args/choices.test b/src/tests/modules/punk/args/testsuites/args/choices.test index a0ff501f..e228c74d 100644 --- a/src/tests/modules/punk/args/testsuites/args/choices.test +++ b/src/tests/modules/punk/args/testsuites/args/choices.test @@ -247,6 +247,111 @@ namespace eval ::testspace { {leftright {heavy x}}\ {leftright {arc heavy}}\ ] + + # ---- unrestricted (-choicerestricted 0) interplay with prefix/deny/reserve ---- + # Characterization added 2026-07-08 ahead of G-040 (choice aliasing). + # Key properties pinned here: + # - a matching prefix is NORMALIZED to the full (canonical) choice in the parse result + # - non-matching words (unknown, ambiguous prefix, denied prefix, reserved word) pass + # through unchanged instead of erroring - the pattern used by ::punk::help's topic + # leader where unrecognised words fall through to command lookup + + test choiceprefix_unrestricted_normalizes_to_canonical {prefix input normalized to the full choice in the parse result when -choicerestricted 0}\ + -setup $common -body { + #unique prefix -> canonical choice in result + set argd [punk::args::parse {env} withdef @values {topic -choices {topics environment console} -choiceprefix 1 -choicerestricted 0}] + lappend result [dict get $argd values] + #unknown word passes through unchanged + set argd [punk::args::parse {lindex} withdef @values {topic -choices {topics environment console} -choiceprefix 1 -choicerestricted 0}] + lappend result [dict get $argd values] + #ambiguous prefix passes through unchanged (no error when unrestricted) + set argd [punk::args::parse {t} withdef @values {topic -choices {topics tcl environment} -choiceprefix 1 -choicerestricted 0}] + lappend result [dict get $argd values] + }\ + -cleanup { + }\ + -result [list\ + {topic environment}\ + {topic lindex}\ + {topic t}\ + ] + + test choiceprefix_denylist_unrestricted_passthrough {denied prefix falls through as an ordinary value when -choicerestricted 0}\ + -setup $common -body { + set docid ::testspace::choiceprefix_denylist_unrestricted_passthrough + punk::args::define [list @id -id $docid] @values {action -choices {delete describe} -choiceprefixdenylist {delete} -choicerestricted 0} + + #denied prefix -> passthrough, not an error and not a match + set argd [punk::args::parse {del} withid $docid] + lappend result [dict get $argd values] + #full denied word still matches + set argd [punk::args::parse {delete} withid $docid] + lappend result [dict get $argd values] + #undenied choice still prefix-matches and normalizes + set argd [punk::args::parse {des} withid $docid] + lappend result [dict get $argd values] + }\ + -cleanup { + punk::args::undefine $docid 1 + }\ + -result [list\ + {action del}\ + {action delete}\ + {action describe}\ + ] + + test choiceprefix_reservelist_unrestricted_passthrough {reserved word and shorter prefixes fall through when -choicerestricted 0; longer prefixes normalize}\ + -setup $common -body { + set docid ::testspace::choiceprefix_reservelist_unrestricted_passthrough + punk::args::define [list @id -id $docid] @values {state -choices {environment} -choiceprefixreservelist {en} -choicerestricted 0} + + #shorter than the reserved phantom: ambiguous -> passthrough + set argd [punk::args::parse {e} withid $docid] + lappend result [dict get $argd values] + #the reserved word itself: not a selectable choice -> passthrough + set argd [punk::args::parse {en} withid $docid] + lappend result [dict get $argd values] + #beyond the phantom: unique prefix -> canonical + set argd [punk::args::parse {env} withid $docid] + lappend result [dict get $argd values] + set argd [punk::args::parse {environment} withid $docid] + lappend result [dict get $argd values] + }\ + -cleanup { + punk::args::undefine $docid 1 + }\ + -result [list\ + {state e}\ + {state en}\ + {state environment}\ + {state environment}\ + ] + + test choiceprefix_collapse_recipe_multi_leader {combined prefix+deny+reserve on an unrestricted -multiple leader (the punk::help topic pattern)}\ + -setup $common -body { + set docid ::testspace::choiceprefix_collapse_recipe_multi_leader + punk::args::define [list @id -id $docid] {@leaders -min 0 -max -1} {topic -optional 1 -multiple 1 -type string -choicerestricted 0 -choiceprefix 1 -choiceprefixdenylist {help} -choiceprefixreservelist {en ter} -choices {topics help tcl environment console terminal}} {@values -min 0 -max 0} + + foreach word {env environment en term ter help hel to 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\ + environment\ + environment\ + en\ + terminal\ + ter\ + help\ + hel\ + topics\ + lindex\ + ] } tcltest::cleanupTests ;#needed to produce test summary line. diff --git a/src/tests/modules/punk/ns/testsuites/ns/cmdflow.test b/src/tests/modules/punk/ns/testsuites/ns/cmdflow.test new file mode 100644 index 00000000..54d68668 --- /dev/null +++ b/src/tests/modules/punk/ns/testsuites/ns/cmdflow.test @@ -0,0 +1,243 @@ +package require tcltest + +package require punk::args +package require punk::ns + +#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. + +namespace eval ::testspace { + namespace import ::tcltest::* + variable common { + set result "" + } + + #--- fixtures: commands + argdocs exercised by the flow tests --------------------------- + + proc flowcmd {args} {} + punk::args::define { + @id -id ::testspace::flowcmd + @cmd -name testspace::flowcmd -summary "flowcmd summary" -help "flowcmd help" + @opts + -flag -type none + @values -min 0 -max -1 + item -type string -optional 1 -multiple 1 + } + interp alias {} ::testspace::flowalias {} ::testspace::flowcmd + + #documented parent with subhelp choiceinfo mapping subcommand words to child docids + proc flowparent {args} {} + punk::args::define { + @id -id ::testspace::flowparent + @cmd -name testspace::flowparent -summary "parent" -help "parent" + @leaders -min 1 -max 1 + subcmd -choices {alpha beta} -choiceinfo { + alpha {{doctype punkargs} {subhelp ::testspace::flowsub_alpha}} + beta {{doctype punkargs} {subhelp ::testspace::flowsub_beta}} + } + @values -min 0 -max 0 + } + punk::args::define { + @id -id ::testspace::flowsub_alpha + @cmd -name "testspace::flowparent alpha" -summary "alpha subcommand" -help "alpha subcommand" + @values -min 0 -max 0 + } + punk::args::define { + @id -id ::testspace::flowsub_beta + @cmd -name "testspace::flowparent beta" -summary "beta subcommand" -help "beta subcommand" + @values -min 0 -max 0 + } + + #as flowparent but prefixes disallowed on the subcommand word + proc flownoprefix {args} {} + punk::args::define { + @id -id ::testspace::flownoprefix + @cmd -name testspace::flownoprefix -summary "parent noprefix" -help "parent noprefix" + @leaders -min 1 -max 1 + subcmd -choiceprefix 0 -choices {alpha beta} -choiceinfo { + alpha {{doctype punkargs} {subhelp ::testspace::flowsub_alpha}} + beta {{doctype punkargs} {subhelp ::testspace::flowsub_beta}} + } + @values -min 0 -max 0 + } + + #as flowparent but 'alpha' must be typed in full (denylist) + proc flowdeny {args} {} + punk::args::define { + @id -id ::testspace::flowdeny + @cmd -name testspace::flowdeny -summary "parent deny" -help "parent deny" + @leaders -min 1 -max 1 + subcmd -choices {alpha beta} -choiceprefixdenylist {alpha} -choiceinfo { + alpha {{doctype punkargs} {subhelp ::testspace::flowsub_alpha}} + beta {{doctype punkargs} {subhelp ::testspace::flowsub_beta}} + } + @values -min 0 -max 0 + } + + #reserved phantom 'en' raises environment's minimum accepted prefix to 'env' + proc flowreserve {args} {} + punk::args::define { + @id -id ::testspace::flowreserve + @cmd -name testspace::flowreserve -summary "parent reserve" -help "parent reserve" + @leaders -min 1 -max 1 + subcmd -choices {environment} -choiceprefixreservelist {en} -choiceinfo { + environment {{doctype punkargs} {subhelp ::testspace::flowsub_env}} + } + @values -min 0 -max 0 + } + punk::args::define { + @id -id ::testspace::flowsub_env + @cmd -name "testspace::flowreserve environment" -summary "environment subcommand" -help "environment subcommand" + @values -min 0 -max 0 + } + + #undocumented command whose subcommand is documented via a space-delimited id + proc flowgap {args} {} + punk::args::define { + @id -id "::testspace::flowgap deep" + @cmd -name "testspace::flowgap deep" -summary "deep subcommand of undocumented parent" -help "deep subcommand" + @values -min 0 -max 0 + } + + #--- cmdwhich --------------------------------------------------------------------------- + + test cmdwhich_proc_alias_notfound {cmdwhich whichtype/origin for proc, alias and missing command}\ + -setup $common -body { + set w [punk::ns::cmdwhich ::testspace::flowcmd] + lappend result [dict get $w whichtype] + set w [punk::ns::cmdwhich ::testspace::flowalias] + lappend result [dict get $w whichtype] [dict get $w origin] + set w [punk::ns::cmdwhich ::testspace::no_such_cmd] + lappend result [dict get $w whichtype] + }\ + -cleanup { + }\ + -result [list proc alias ::testspace::flowcmd notfound] + + #--- cmdinfo docid resolution ----------------------------------------------------------- + + test cmdinfo_docid_direct {cmdinfo returns the explicit punk::args id for a documented proc}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flowcmd] + lappend result [dict get $cinfo docid] [dict get $cinfo args_remaining] + }\ + -cleanup { + }\ + -result [list ::testspace::flowcmd {}] + + test cmdinfo_docid_via_alias {cmdinfo resolves an alias to its target's docid}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flowalias] + lappend result [dict get $cinfo docid] + }\ + -cleanup { + }\ + -result [list ::testspace::flowcmd] + + test cmdinfo_subhelp_choiceinfo {a subcommand word maps to its subhelp docid; unique prefix accepted when -choiceprefix 1 (default)}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flowparent alpha] + lappend result [dict get $cinfo docid] + set cinfo [punk::ns::cmdinfo ::testspace::flowparent beta] + lappend result [dict get $cinfo docid] + #unique prefix of a choice resolves to the same subhelp docid + set cinfo [punk::ns::cmdinfo ::testspace::flowparent al] + lappend result [dict get $cinfo docid] + }\ + -cleanup { + }\ + -result [list ::testspace::flowsub_alpha ::testspace::flowsub_beta ::testspace::flowsub_alpha] + + test cmdinfo_unknown_subword_returns_parent {a word matching no choice leaves docid at the parent with the word in args_remaining}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flowparent zzz] + lappend result [dict get $cinfo docid] [dict get $cinfo args_remaining] + }\ + -cleanup { + }\ + -result [list ::testspace::flowparent zzz] + + test cmdinfo_choiceprefix0_requires_exact {with -choiceprefix 0 a prefix is not accepted by the doc walk - docid stays at the parent}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flownoprefix alpha] + lappend result [dict get $cinfo docid] + set cinfo [punk::ns::cmdinfo ::testspace::flownoprefix al] + lappend result [dict get $cinfo docid] [dict get $cinfo args_remaining] + }\ + -cleanup { + }\ + -result [list ::testspace::flowsub_alpha ::testspace::flownoprefix al] + + test cmdinfo_spaceform_id {a space-delimited id documents a subcommand of an undocumented parent}\ + -setup $common -body { + set cinfo [punk::ns::cmdinfo ::testspace::flowgap deep] + lappend result [dict get $cinfo docid] + }\ + -cleanup { + }\ + -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. + + test cmdinfo_parity_denylist_GAP {GAP (G-040): parse rejects a denied prefix but the doc walk still resolves it}\ + -setup $common -body { + #parse side: denied prefix is an error (restricted choices) + if {[catch {punk::args::parse {al} withid ::testspace::flowdeny}]} { + lappend result parse-rejects-denied-prefix + } else { + lappend result parse-UNEXPECTEDLY-accepted-denied-prefix + } + #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 + set cinfo [punk::ns::cmdinfo ::testspace::flowdeny al] + lappend result [dict get $cinfo docid] + }\ + -cleanup { + }\ + -result [list parse-rejects-denied-prefix alpha ::testspace::flowsub_alpha] + + test cmdinfo_parity_reservelist_GAP {GAP (G-040): parse blocks a reserved prefix but the doc walk still resolves it}\ + -setup $common -body { + #parse side: reserved word blocks itself and shorter prefixes + if {[catch {punk::args::parse {en} withid ::testspace::flowreserve}]} { + lappend result parse-rejects-reserved-word + } else { + lappend result parse-UNEXPECTEDLY-accepted-reserved-word + } + #parse side: a prefix beyond the reserved phantom resolves + set argd [punk::args::parse {env} withid ::testspace::flowreserve] + lappend result [dict get [dict get $argd leaders] subcmd] + #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 + 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] + + #--- cmdhelp smoke ------------------------------------------------------------------------ + + test cmdhelp_string_smoke {cmdhelp -return string renders usage for a documented proc and a subhelp subcommand}\ + -setup $common -body { + set out [punk::ns::cmdhelp -return string ::testspace::flowcmd] + lappend result [string match "*flowcmd summary*" $out] + set out [punk::ns::cmdhelp -return string ::testspace::flowparent alpha] + lappend result [string match "*alpha subcommand*" $out] + }\ + -cleanup { + }\ + -result [list 1 1] +} +tcltest::cleanupTests ;#needed to produce test summary line.