Browse Source
- 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.commaster
7 changed files with 486 additions and 1 deletions
@ -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. |
||||
@ -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 |
||||
|
||||
@ -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. |
||||
Loading…
Reference in new issue