Browse Source

G-073 achieved: punk::args unavailable choices (-choiceunavailable) + G-166 drafted (punk::args 0.16.0, tclcore moduledoc 0.4.0, punk::ns 0.9.3)

New per-argument key -choiceunavailable <list>: names recognised but not
available in the current runtime/context. Entries display among the
choices under a dedicated "Unavailable (recognised, not selectable
here)" group (one rendering-side injection - both renderers,
choicelabels notes, choiceinfo markers, trie prefix marking and
value-in-effect exclusion all inherited) and join the prefix pools;
synopsis literals and goodchoice marking never include them. Resolve
-time validation: no collision with choices/alias names/reservelist,
-default may not be unavailable, requires -choices/-choicegroups.

choiceword_match gains an optional 8th argument and an 'unavailable'
result key (matched stays 0 - matched-only consumers unaffected):
exact, nocase and unique-prefix landings carry the name as canonical; a
prefix shared with an available choice stays plain-ambiguous.
Validation rejects a landing with the tailored PUNKARGS VALIDATION
{choiceunavailable <name> choices <list>} error (-badarg/-badval;
private::unavailable_choice_msg names the supplied word, the resolved
entry, its choicelabel note and the available values; shared by final
validation and both G-082 overflow raise sites via rejection kind
'unavailable' from the allocation screen). Ambiguity messages annotate
unavailable longer matches '(recognised but unavailable)';
-choicerestricted 0 keeps full passthrough.

tclcore 'string is' adoption: curated forward list {dict} ('unicode'
deliberately excluded) diffed against the define-time harvest; labels
and per-class virtual ids extend to unavailable classes. On Tcl 8.6:
dict displays with its "(class not present in Tcl 8.6)" note,
'string is dict' rejects informatively, 'string is di' is ambiguous
(deliberately stricter than real 8.6 - recorded as a user-sanctioned
parity exemption; full words parity-true), and 'i string is dict'
documents via the virtual id. punk::ns cmd_traverse passes the key at
both resolver sites (doc-walk parity; the choice-traverse comment now
documents the choice-resolution vs doc-addressing axes split). tclcore
moduledoc requires punk::args 0.16.0-.

Tests: new choiceunavailable.test (10 tests); usagemarking.test gains
the goodchoice-exclusion pin; tclcoreparity.test gains forward-class
adoption invariants and the sanctioned 'di' strictness exemption, with
the version_notes pin deliberately flipped (dict's label now renders on
every runtime). Green: the three suites 37/37 on native Tcl 8.6.10
(luck runtime), 8.7a6 and 9.0.3; punk/args subtree 293/290/3 skips/0
fail on 9.0.3 AND 8.7; modules/punk sweep green except the three
documented pre-existing box-baseline failures (grepstr x2, nslist x1);
ns cmdhelp/corp 58/58 after the comment clarification.

Goals: G-073 flipped achieved 2026-08-05 + archived (index record
moved, detail file to goals/archive with Progress/verification and 3
follow-ons). G-166 drafted [proposed] at user direction (approved
wording): cmdinfo availability key + cmdhelp marking + tclcore leading
note + prefix-landing decision; the enacted G-073 follow-on flipped
=> goal G-166, the G-055 pattern follow-on => goal G-055 with a Notes
pointer added there. goals_lint clean. punkshell 0.54.0 + CHANGELOG.
Bootsupport/vfscommon snapshots unchanged until promotion + bake.

Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 5 days ago
parent
commit
69927531a4
  1. 20
      CHANGELOG.md
  2. 4
      GOALS-archive.md
  3. 8
      GOALS.md
  4. 7
      goals/G-055-tclcore-regen-workflow.md
  5. 84
      goals/G-166-cmdinfo-unavailable-surfacing.md
  6. 67
      goals/archive/G-073-punkargs-unavailable-choices.md
  7. 2
      punkproject.toml
  8. 232
      src/modules/punk/args-999999.0a1.0.tm
  9. 3
      src/modules/punk/args-buildversion.txt
  10. 29
      src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm
  11. 3
      src/modules/punk/args/moduledoc/tclcore-buildversion.txt
  12. 18
      src/modules/punk/ns-999999.0a1.0.tm
  13. 3
      src/modules/punk/ns-buildversion.txt
  14. 272
      src/tests/modules/punk/args/testsuites/args/choiceunavailable.test
  15. 76
      src/tests/modules/punk/args/testsuites/args/tclcoreparity.test
  16. 33
      src/tests/modules/punk/args/testsuites/args/usagemarking.test

20
CHANGELOG.md

@ -5,6 +5,26 @@ 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.54.0] - 2026-08-05
- G-073 achieved: punk::args unavailable choices (punk::args 0.16.0, tclcore
moduledoc 0.4.0, punk::ns 0.9.3, source modules). A definition can declare
-choiceunavailable names - recognised but not available in the current
runtime/context. They display among the choices under a dedicated
"Unavailable (recognised, not selectable here)" group with their ordinary
choicelabels notes, reserve prefixes (a prefix shared with an available
choice is ambiguous), and are rejected with a tailored choiceunavailable
error naming the entry and its note instead of the generic listed-values
error; synopsis literals and value-in-effect marking never include them.
The tclcore 'string is' model adopts the key via a curated forward-class
list: on Tcl 8.6 the dict class now displays with its "(class not present
in Tcl 8.6)" note, 'string is dict' is rejected informatively,
'string is di' is ambiguous (deliberately stricter than real 8.6,
preparing users for 9.x - recorded as a sanctioned parity exemption), and
'i string is dict' documents the class via its per-class virtual id. The
punk::ns doc walk honours unavailable names identically to parsing. Kits
pick this up at the next bootsupport/vfscommon refresh and bake.
## [0.53.1] - 2026-08-05
- G-082 follow-on landed (punk::args 0.15.2, source module): a REQUIRED typed

4
GOALS-archive.md

@ -20,6 +20,10 @@ Do not edit archived records here except to fix a broken path. Archived detail f
## Archived goals
### G-073 [achieved 2026-08-05] punk::args unavailable choices: displayed with notes and prefix-reserving, but rejected with a tailored message → detail: goals/archive/G-073-punkargs-unavailable-choices.md
Scope: src/modules/punk/args-999999.0a1.0.tm (spec key, choiceword_match pool, choices rendering in arg_error table+string renderers, validation message); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm ('string is' forward-class adoption + per-class virtual id); src/tests/modules/punk/args/testsuites/args/ (new suite + tclcoreparity.test exemption)
Acceptance: a definition using the new key renders unavailable entries distinguishably among the choices (mechanism decided in the work: dedicated group heading, dim/warn styling, or marker) with their -choicelabels notes, in both the table and string renderers, and synopsis choice-literal display excludes them; parse/parse_status reject an unavailable choice word - exact, or a unique prefix resolving only to it - with a message identifying it as recognised-but-unavailable rather than the generic choice error; unavailable entries participate in prefix disambiguation via choiceword_match (a prefix shared between an available and an unavailable choice is rejected as ambiguous) and are honoured identically by the punk::ns doc walk and the G-071 allocation screen (parity free via the shared resolver - no second matching rule); value-in-effect/goodarg highlighting never marks an unavailable entry; the tclcore 'string is' definition adopts the key on runtimes lacking known forward classes (dict on 8.6; the forward list is explicitly curated in the definition - unicode deliberately excluded as never-released), with a per-class virtual id generated from the static description so 'i string is dict' documents the class with its note on 8.6; the deliberate strictness divergence (real 8.6 accepts 'string is di' as digit; the model rejects it as ambiguous) is recorded in tclcoreparity.test as a user-sanctioned exemption class rather than silently special-cased, with full-word behaviour staying parity-true on both versions; definitions without the key behave unchanged (full punk::args and punk::ns suites pass).
### G-165 [achieved 2026-08-04] Driver-invariant kit assembly: single zip writer path with punkzip acceleration, file-relative by explicit opt-in only → detail: goals/archive/G-165-driver-invariant-kit-assembly.md
Scope: src/make.tcl (zip-kit assembly branch); src/modules/punk/zip-999999.0a1.0.tm (mkzip write-path acceleration seam); src/tools/punkzip/ (re-vendor after upstream enabling changes); src/runtime/mapvfs.toml (per-kit offsetstyle opt-in); src/tests/ (accelerated/floor parity + driver-vintage characterization); src/AGENTS.md; bin/AGENTS.md
Acceptance: a kit baked from the same source tree under all three driver classes on the reference machine - tclsh 8.7a6 (legacy file-relative mkimg, the reported failure case), tclsh90 9.0.x (fixed mkimg), and a mkimg-less tclsh (the zipcat-only class) - probes offsetstyle=archive via punk::zip::archive_info with identical member sets and member CRCs across the three runs; with bin/punkzip present the bake assembles via punkzip (visible in bake output) and a write-path parity suite proves member-set, CRC and offsetstyle equivalence between the accelerated and pure-Tcl paths over the G-124 fixture set plus a real .vfs payload, while a no-punkzip bake of the same tree produces an equivalent archive; a kit declaring offsetstyle="file" in its mapvfs.toml entry probes file - the opt-in recorded, and the offsetstyle.test baseline exempts declared kits - while G-134's BAKE-WARNING never fires on default (non-opt-in) bakes under any driver class; upstream punkzip gains the root-strip and repeatable-exclude options the bake's call shape requires (zig build test green, entries rooted at the payload tree with mkzip's default exclusion set honoured identically), re-vendored with PROVENANCE.md updated and tools-tier publication recorded or explicitly user-deferred; make.tcl's kit assembly path contains no tcl::zipfs::mkimg call; full punkexe + punkboot utils suites green in both runner modes; and src/AGENTS.md plus bin/AGENTS.md state the single-writer contract (consistent kits regardless of driving interpreter; file-relative by explicit opt-in only).

8
GOALS.md

@ -263,10 +263,6 @@ Detail: goals/G-069-splitter-tclparser-lint.md
Scope: src/modules/punk/args-999999.0a1.0.tm (type-expression parsing, clause allocation, synopsis/help renderers); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (::try as proving consumer; ::if/::switch as touched); src/tests/modules/punk/args/testsuites/args/
Detail: goals/G-072-punkargs-compound-clause-types.md
### G-073 [proposed] punk::args unavailable choices: displayed with notes and prefix-reserving, but rejected with a tailored message
Scope: src/modules/punk/args-999999.0a1.0.tm (spec key, choiceword_match pool, choices rendering in arg_error table+string renderers, validation message); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm ('string is' forward-class adoption + per-class virtual id); src/tests/modules/punk/args/testsuites/args/ (new suite + tclcoreparity.test exemption)
Detail: goals/G-073-punkargs-unavailable-choices.md
### G-075 [proposed] punk::args (package) ids: working lookup and a user-facing package documentation surface
Scope: src/modules/punk/args-999999.0a1.0.tm (id lookup/update_definitions prefix handling, usage/arg_error rendering of package-level ids); src/modules/punk/ns-999999.0a1.0.tm (cmdhelp/help-system surface); src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/modules/template_module-0.0.4.tm (template block as touched/verified); src/tests/modules/punk/args/testsuites/args/ + src/tests/modules/punk/ns/testsuites/ns/ (new coverage)
Detail: goals/G-075-punkargs-package-ids.md
@ -405,3 +401,7 @@ Scope: src/modules/punk/repl-999999.0a1.0.tm (exit/eof done paths, reader-window
Detail: goals/G-163-unix-exit-output-drain.md
### G-166 [proposed] cmdinfo/cmdhelp surface recognised-but-unavailable landings distinctly
Scope: src/modules/punk/ns-999999.0a1.0.tm (cmdinfo result key, cmd_traverse landing attribution, cmdhelp render marking + advisory-parse presentation); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (virtual ids lead with the unavailability note on affected runtimes); src/tests/modules/punk/ns/testsuites/ns/ (cmdinfo/cmdhelp pins incl the deliberate key-list flip); src/tests/modules/punk/args/testsuites/args/tclcoreparity.test (8.6-arm pins)
Detail: goals/G-166-cmdinfo-unavailable-surfacing.md

7
goals/G-055-tclcore-regen-workflow.md

@ -86,6 +86,13 @@ other commands including the multi-form ::after.
## Notes
- G-073 (achieved 2026-08-05, archived) proved the forward-class authoring pattern
this goal's workflow guidance should adopt: a curated forward-class list diffed
against the runtime harvest at define time, the missing classes declared
-choiceunavailable (punk::args 0.16.0) with their static descriptions as
choicelabels, and per-class virtual ids generated from the same descriptions -
see goals/archive/G-073-punkargs-unavailable-choices.md (the tclcore 'string is'
model is the in-tree exemplar, tclcore moduledoc 0.4.0).
- tkcore: a started moduledoc loads on 'package require tk'. It is to be treated
with the same workflow later, but relies heavily on shared documentation
sections as currently modeled by the (default)::punk::args::moduledoc::tkcore::tk_standardoptions

84
goals/G-166-cmdinfo-unavailable-surfacing.md

@ -0,0 +1,84 @@
# G-166 cmdinfo/cmdhelp surface recognised-but-unavailable landings distinctly
Status: proposed
Scope: src/modules/punk/ns-999999.0a1.0.tm (cmdinfo result key, cmd_traverse landing attribution, cmdhelp render marking + advisory-parse presentation); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (virtual ids lead with the unavailability note on affected runtimes); src/tests/modules/punk/ns/testsuites/ns/ (cmdinfo/cmdhelp pins incl the deliberate key-list flip); src/tests/modules/punk/args/testsuites/args/tclcoreparity.test (8.6-arm pins)
Goal: cmdinfo/cmdhelp surface recognised-but-unavailable landings: cmdinfo gains an availability key (empty, or the canonical unavailable word) populated wherever resolution addresses a -choiceunavailable word (exact space-form docid landings and the choice traverse), cmdhelp marks such subjects visibly in all return forms and its advisory parse no longer presents them as cleanly usable, the tclcore virtual ids lead with the unavailability note on affected runtimes, and the doc walk's treatment of a unique prefix landing on an unavailable name (currently: resolves no documentation) is decided and pinned within this goal - with the enacted G-073 follow-on items dispositioned accordingly.
Acceptance: on a runtime where the tclcore model declares dict unavailable (Tcl 8.6), cmdinfo for 'string is dict' (direct and decomposed invocation forms) reports the new availability key carrying 'dict' while 'string is true' reports it empty, with cmdtype remaining 'doconly' for both (the command-kind axis is not overloaded); the key is present on every cmdinfo result and the cmdhelp.test cmdinfo key-list pin is flipped deliberately; cmdhelp output for an unavailable subject is visibly distinguished in table, text and dict return forms (the dict form carries the key; table/text carry a marking naming the unavailability), and an argument tail such as 'string is dict 5' no longer renders as a cleanly usable command line; the walk's unique-prefix landing on an unavailable name ('i string is dic' on 8.6) has a decided, pinned behaviour consistent with the marking (either resolving the virtual docid WITH the marking, or documented-conservative non-resolution); tclcore virtual ids for unavailable classes lead their rendered help with the unavailability statement on affected runtimes (first description line, not buried mid-prose) while modern runtimes render those ids unchanged; definitions without -choiceunavailable and existing cmdinfo/cmdhelp consumers behave unchanged (full punk/ns and punk/args suites green apart from deliberately flipped pins); the enacted G-073 Follow-on lines carry => goal G-166 dispositions.
## Context
Found 2026-08-05 immediately after G-073 achieved (user probes): on Tcl 8.6,
'i ::tcl::string::is dict' (unavailable class) and 'i ::tcl::string::is true'
(real class) produce indistinguishable machine shapes - cmdinfo reports
cmdtype 'doconly' with identical key sets for both, and cmdhelp's advisory
parse of an argument tail ('string is dict 5') succeeds against the virtual id
so the render presents the unavailable class exactly like a usable one. The
only difference is the "(class not present in Tcl 8.6)" sentence mid-way
through the description prose.
Half of this is G-073's intent: documentation REACHABILITY for unavailable
words (via exact space-form virtual docids) is an acceptance requirement, and
doc addressing has always been deliberately broader than parse acceptance
(per-class ids like 'string is true' document argument words that were never
subcommands). The INDISTINGUISHABILITY is emergent, not specified: nothing
structural tells a consumer - human or machine - that the documented word is
rejected by this runtime's parse.
Axis decision (assessed 2026-08-05, to be confirmed in the work): a NEW
cmdinfo key, not a cmdtype refinement such as 'doconly_unavailable'. cmdtype
answers "what kind of executable thing did resolution land on"; availability
is a property of the resolved word at its level. Today they co-occur with
doconly only incidentally (unavailable entries never have real commands behind
them), and overloading the value breaks consumers switching on
cmdtype eq "doconly". The two-axes distinction is recorded in the cmd_traverse
choice-traverse comment (ns 0.9.3).
## Approach
- New cmdinfo result key (spelling decided in the work; candidate
'unavailable' - empty string normally, the canonical unavailable word when
the resolution addressed one). Population is nearly free: the choice
traverse already holds the resolver's matchinfo (its 'unavailable' key,
punk::args 0.16.0), and the exact space-form id landing branches can consult
the level's choices-bearing arginfo the same way the space-form prefix-parity
branch (G-051, achieved 2026-07-13 - see
goals/archive/G-051-cmdinfo-pseudo-and-prefix.md) already does
(Dict_getdef -choiceunavailable + membership check).
- cmdhelp derives the render marking from the same data: a visible
unavailability statement in table/text forms (warn styling; placement decided
in the work - title line marker and/or a leading line), the key in -return
dict, and the advisory-parse presentation adjusted so an accepted parse
against an unavailable subject's virtual id is not shown as a cleanly usable
command line.
- tclcore generator: on runtimes where a forward class is unavailable, the
per-class virtual id's help LEADS with the unavailability statement (the
static description follows); modern runtimes generate the id unchanged.
- Prefix-landing decision: either the walk resolves a unique prefix landing to
the virtual docid and relies on the marking to carry the rejection story
(parse gives the tailored "resolves to 'dict'" report for the same word), or
the current conservative non-resolution is kept and pinned as deliberate.
Decided in the work; the G-073 follow-on line flips accordingly.
## Notes
- Related: G-073 (achieved 2026-08-05, archived -
goals/archive/G-073-punkargs-unavailable-choices.md) - this goal enacts its
open follow-on on doc-walk prefix landings and resolves the
indistinguishability its acceptance did not specify; the -choiceunavailable
key, resolver indication and tailored parse rejection this goal consumes all
landed there (punk::args 0.16.0, tclcore moduledoc 0.4.0, ns 0.9.3).
- Related: G-050 (punk::ns::synopsis argument-validity marking and
status-aware returns) - sibling display-marking surface; whichever lands
second should reuse the first's marking vocabulary/styling decisions.
- Related: G-044 (completion/hinting) - a future consumer: completion over
choices should be able to exclude or annotate unavailable words from the
same data rather than re-deriving.
- Related: G-055 (tclcore moduledoc regeneration workflow) - the tclcore
virtual-id leading-note change lands in the moduledoc G-055's workflow
governs; its guidance gains the forward-class pattern per the G-073
follow-on already dispositioned => goal G-055.
- Overlap survey 2026-08-05: goals_xref paths src/modules/punk/ns-999999.0a1.0.tm
- G-044/G-050/G-052/G-075/G-085 surfaced; G-052 (TclOO autodef) and G-085
(cmdtrace) are same-file unrelated concerns; G-075 ((package) ids) is
sibling docid territory with no word-availability interaction - deliberate
non-overlap.

67
goals/G-073-punkargs-unavailable-choices.md → goals/archive/G-073-punkargs-unavailable-choices.md

@ -1,6 +1,6 @@
# G-073 punk::args unavailable choices: displayed with notes and prefix-reserving, but rejected with a tailored message
Status: proposed
Status: achieved 2026-08-05
Scope: src/modules/punk/args-999999.0a1.0.tm (spec key, choiceword_match pool, choices rendering in arg_error table+string renderers, validation message); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm ('string is' forward-class adoption + per-class virtual id); src/tests/modules/punk/args/testsuites/args/ (new suite + tclcoreparity.test exemption)
Goal: an argument definition can declare choices that are recognised but not available in the current runtime (new key, candidate spelling -choiceunavailable <list>): they display among the choices with their ordinary -choicelabels notes (visually distinguished), participate in prefix disambiguation like -choiceprefixreservelist phantoms, and are rejected at parse with a tailored message identifying the unavailability rather than the generic listed-values error - adopted by the tclcore 'string is' model so that Tcl 8.6 shows the dict class annotated "(class not present in Tcl 8.6)", rejects 'string is dict' informatively, treats 'string is di' as ambiguous (deliberately stricter than real 8.6, preparing 8.6 users for 9.x where dict/digit/double collide), and documents 'i string is dict' via a per-class virtual id generated from the static description.
Acceptance: a definition using the new key renders unavailable entries distinguishably among the choices (mechanism decided in the work: dedicated group heading, dim/warn styling, or marker) with their -choicelabels notes, in both the table and string renderers, and synopsis choice-literal display excludes them; parse/parse_status reject an unavailable choice word - exact, or a unique prefix resolving only to it - with a message identifying it as recognised-but-unavailable rather than the generic choice error; unavailable entries participate in prefix disambiguation via choiceword_match (a prefix shared between an available and an unavailable choice is rejected as ambiguous) and are honoured identically by the punk::ns doc walk and the G-071 allocation screen (parity free via the shared resolver - no second matching rule); value-in-effect/goodarg highlighting never marks an unavailable entry; the tclcore 'string is' definition adopts the key on runtimes lacking known forward classes (dict on 8.6; the forward list is explicitly curated in the definition - unicode deliberately excluded as never-released), with a per-class virtual id generated from the static description so 'i string is dict' documents the class with its note on 8.6; the deliberate strictness divergence (real 8.6 accepts 'string is di' as digit; the model rejects it as ambiguous) is recorded in tclcoreparity.test as a user-sanctioned exemption class rather than silently special-cased, with full-word behaviour staying parity-true on both versions; definitions without the key behave unchanged (full punk::args and punk::ns suites pass).
@ -70,3 +70,68 @@ riding the existing rendering pipeline.
alloc_rejections store is consulted at the overflow raise sites - the
tailored unavailable-choice message can extend the record's kind vocabulary
for recognised-but-unavailable words rather than adding a second mechanism.
(Outcome: exactly that landed - kind 'unavailable' records from the
singular-clause screen, preferred at both overflow raise sites.)
## Progress
- 2026-08-05 (achieved, one session from activation): punk::args 0.16.0 +
punk::args::moduledoc::tclcore 0.4.0 + punk::ns 0.9.3 (source modules).
Key spelling as proposed: -choiceunavailable <list> (per-argument only, like
-choicealiases; requires -choices/-choicegroups). Display mechanism decided:
DEDICATED GROUP HEADING "Unavailable (recognised, not selectable here)" -
a rendering-side injection into the choicegroups walk, so the table renderer
(sub-table title), the string renderer (Group heading + bullets), choicelabels
notes, choiceinfo markers, trie shortest-prefix marking and value-in-effect
wrapping all apply without new render paths; entries join the display
prefix-calc pool mirroring their parse-time reserving effect.
Resolver: choiceword_match gains an optional 8th argument (unavailablelist)
and an 'unavailable' result key - matched stays 0 so matched-only consumers
are unaffected; exact, nocase-exact and unique-prefix landings carry the
entry as canonical; a prefix shared with an available choice stays a plain
ambiguous non-match. Rejection: PUNKARGS VALIDATION
{choiceunavailable <name> choices <list>} with -badarg/-badval; the message
(private::unavailable_choice_msg, shared by every raise surface) names the
supplied word, the resolved entry, its -choicelabels note (the steering
"why" - e.g the version boundary) and the available values; ambiguous-prefix
messages annotate unavailable longer matches '(recognised but unavailable)'.
G-071 screen honours the list; optional-arg landings ride the G-082
rejection-record seam to both overflow raise sites; required args stay
unscreened (final validation reports identically); -choicerestricted 0
keeps full passthrough. Resolve-time validation: collision with
choices/alias-names/reservelist rejected; -default may not be unavailable.
tclcore: curated forward list {dict} ('unicode' deliberately excluded -
unreleased-8.7-only); unavailable = forward minus harvested-live at define
time; the choicelabels build and per-class virtual-id loop extend to
unavailable classes from the same static descriptions ('i string is dict'
documents on 8.6). punk::ns: both cmd_traverse resolver call sites pass the
key - doc-walk parity ('string is di' on 8.6 resolves no docs where it
previously resolved digit).
- Verification (runtests.tcl): new choiceunavailable.test (10 tests - resolver
matrix incl 7-arg compat, resolve-time validation errors, tailored rejection
in leader/value/option positions incl prefix landing and note content,
shared-prefix ambiguity + annotation, G-082 overflow seam, restricted-0
passthrough, parse_status classification, both renderers' group + note,
synopsis exclusion, parse-result purity); usagemarking.test +
usagemarking_goodchoice_unavailable_never_marked (goodchoice marking never
touches unavailable entries, both renderers); tclcoreparity.test + forward
-class adoption invariants and the USER-SANCTIONED 'di' strictness exemption
(live-derived; the one deliberate divergence arm on 8.6, full words
parity-true) with the pre-G-073 version_notes pin ("labels appear exactly
when the class is live") deliberately flipped (dict's label now renders on
every runtime). Green: the three suites 37/37 on native Tcl 8.6.10
(bin/runtime/win32-ix86/tclsh8.6.10-luck-zip.exe), 8.7a6 and 9.0.3; full
punk/args subtree 293 total / 290 passed / 3 intended skips / 0 failed on
9.0.3 AND 8.7; modules/punk/*** sweep on 9.0.3 green except the three
documented pre-existing box-baseline failures (ansi grepstr x2, ns nslist x1
- unrelated); direct 8.6 probes verified the harvest (unavailable={dict}),
the tailored rejection + note render, both renderers' group display, and the
doc walk ('di' resolves nothing, exact 'dict' resolves the virtual id).
Modern runtimes compute an empty unavailable list - suites pin behaviour
unchanged.
## Follow-ons
Follow-on: G-055's moduledoc workflow guidance gains the now-proven forward-class authoring pattern (-choiceunavailable + curated forward list + static-description labels, per the 'string is' adoption) => goal G-055
Follow-on: the doc walk resolves only the EXACT unavailable word to its virtual docid - a unique prefix landing (e.g 'i string is dic' on 8.6) conservatively resolves no documentation where parse gives the tailored "resolves to 'dict'" report => goal G-166
Follow-on: an optional -choicemultiple argument's overflow rejection keeps the generic choice record even when a member word landed on an unavailable name (only the singular-clause screen records kind 'unavailable') => open

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.53.1"
version = "0.54.0"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

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

@ -1104,6 +1104,31 @@ tcl::namespace::eval punk::args {
Each alias must map to an existing choice, and must not
itself collide with a choice (validated when the
definition is resolved).
-choiceunavailable {<names>}
Names that are recognised but not available in the
current runtime/context (e.g a choice a newer version
provides - the version-adaptive definition declares it
here on runtimes that lack it). Unavailable names are
displayed among the choices under a dedicated
'Unavailable' group heading with their ordinary
-choicelabels notes, and participate in prefix
calculation exactly like -choiceprefixreservelist
entries - so a prefix shared between an available and
an unavailable name is ambiguous, preparing users for
contexts where the name is real. A word landing on an
unavailable name (exactly, or as a unique prefix) is
rejected with a tailored 'choiceunavailable' error
naming the entry and its -choicelabels note, instead
of the generic listed-values error. Applies under
-choicerestricted 1 (the default); with
-choicerestricted 0 any word remains an acceptable
ordinary value. Requires -choices and/or -choicegroups;
entries must not collide with choices, alias names or
reservelist entries, and -default may not be an
unavailable name (validated when the definition is
resolved). Synopsis choice-literal display and
value-in-effect highlighting never include unavailable
entries.
-choicegroups {<dict>}
Generally this would be used instead of -choices to allow
usage display of choices grouped by some name (or the empty
@ -3362,6 +3387,20 @@ tcl::namespace::eval punk::args {
}
tcl::dict::set spec_merged $spec $specval
}
-choiceunavailable {
#list of recognised-but-unavailable choice names (goal G-073).
#Displayed among the choices (dedicated display group, with their ordinary
#-choicelabels notes), prefix-pool members like -choiceprefixreservelist -
#but a word landing on one (exact, or unique prefix) is rejected with a
#tailored 'choiceunavailable' error instead of the generic choice error.
#Applies under -choicerestricted 1 (the default) - with -choicerestricted 0
#any word is an acceptable ordinary value and no tailored rejection occurs.
#Cross-validation against the final choice set happens after all specs merge.
if {[catch {llength $specval}]} {
error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a list of recognised-but-unavailable choice names @id:$DEF_definition_id"
}
tcl::dict::set spec_merged $spec $specval
}
-unindentedfields -
-solo -
-choices - -choicegroups - -choicemultiple - -choicecolumns -
@ -3481,7 +3520,7 @@ tcl::namespace::eval punk::args {
-minsize -maxsize -choices -choicegroups
-mincap -maxcap
-choicemultiple -choicecolumns -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted
-choicelabels -choiceinfo -choicealiases
-choicelabels -choiceinfo -choicealiases -choiceunavailable
-unindentedfields
-nocase -optional -multiple -validate_ansistripped -allow_ansi -strip_ansi -help
-multipleunique -choicemultipleunique -choicemultipleuniqueset
@ -3510,6 +3549,33 @@ tcl::namespace::eval punk::args {
}
}
#cross-validate -choiceunavailable against the final merged choice set (goal G-073)
if {[tcl::dict::exists $spec_merged -choiceunavailable] && [llength [tcl::dict::get $spec_merged -choiceunavailable]]} {
set cu_allchoices [punk::args::system::Dict_getdef $spec_merged -choices {}]
foreach {_cugroup cumembers} [punk::args::system::Dict_getdef $spec_merged -choicegroups {}] {
lappend cu_allchoices {*}$cumembers
}
if {![llength $cu_allchoices]} {
error "punk::args::resolve - -choiceunavailable for argument '$argname' requires -choices and/or -choicegroups to also be declared (an argument whose every choice is unavailable is not expressible with this key) @id:$DEF_definition_id"
}
set cu_aliasnames [tcl::dict::keys [punk::args::system::Dict_getdef $spec_merged -choicealiases {}]]
set cu_reservelist [punk::args::system::Dict_getdef $spec_merged -choiceprefixreservelist {}]
foreach cu_entry [tcl::dict::get $spec_merged -choiceunavailable] {
if {$cu_entry in $cu_allchoices} {
error "punk::args::resolve - -choiceunavailable for argument '$argname' entry '$cu_entry' collides with a defined choice in -choices/-choicegroups @id:$DEF_definition_id"
}
if {$cu_entry in $cu_aliasnames} {
error "punk::args::resolve - -choiceunavailable for argument '$argname' entry '$cu_entry' collides with a -choicealiases alias name (the alias exact-match would silently accept the unavailable name) @id:$DEF_definition_id"
}
if {$cu_entry in $cu_reservelist} {
error "punk::args::resolve - -choiceunavailable for argument '$argname' entry '$cu_entry' also appears in -choiceprefixreservelist (the reservelist landing would mask the unavailable indication) @id:$DEF_definition_id"
}
}
if {[tcl::dict::exists $spec_merged -default] && [tcl::dict::get $spec_merged -default] in [tcl::dict::get $spec_merged -choiceunavailable]} {
error "punk::args::resolve - -default for argument '$argname' is a -choiceunavailable entry (an unavailable name cannot be the value in effect) @id:$DEF_definition_id"
}
}
if {$is_opt} {
#tcl::dict::set FDICT ARG_CHECKS $argname {*}{
# } [tcl::dict::remove $spec_merged -form -type -default -multiple -strip_ansi -validate_ansistripped -allow_ansi -choicecolumns -group -typesynopsis -help -ARGTYPE] ;#leave things like -range -minsize
@ -5816,6 +5882,18 @@ tcl::namespace::eval punk::args {
} else {
set choicegroups [dict merge [dict create "" $choices] $choicegroups]
}
#G-073: recognised-but-unavailable names render among the choices under a
#dedicated display group (the heading is the distinguishing mechanism),
#with their ordinary -choicelabels notes. Joining choicegroups here also
#puts them in allchoices_originalcase below and therefore the prefix-calc
#pool, so displayed shortest-prefix marking reflects their parse-time
#reserving effect. Display-side injection only: they are never in
#-choices/-choicegroups, so parse acceptance, synopsis choice-literals
#and value-in-effect marking cannot pick them up.
set choiceunavailable_display [Dict_getdef $arginfo -choiceunavailable {}]
if {[llength $choiceunavailable_display]} {
dict lappend choicegroups "Unavailable (recognised, not selectable here)" {*}$choiceunavailable_display
}
#review - does choiceprefixdenylist need to be added?
dict for {groupname clist} $choicegroups {
lappend allchoices_originalcase {*}$clist
@ -7201,6 +7279,7 @@ tcl::namespace::eval punk::args {
[Dict_getdef $ARG_INFO $argname -choiceprefix 1]\
[Dict_getdef $ARG_INFO $argname -choiceprefixdenylist {}]\
[Dict_getdef $ARG_INFO $argname -choiceprefixreservelist {}]\
[Dict_getdef $ARG_INFO $argname -choiceunavailable {}]\
]
if {[dict get $cwm matched]} {
set word_matched 1
@ -7435,6 +7514,7 @@ tcl::namespace::eval punk::args {
[Dict_getdef $ARG_INFO $argname -choiceprefix 1]\
[Dict_getdef $ARG_INFO $argname -choiceprefixdenylist {}]\
[Dict_getdef $ARG_INFO $argname -choiceprefixreservelist {}]\
[Dict_getdef $ARG_INFO $argname -choiceunavailable {}]\
]
return [expr {[dict get $cwm matched] ? "yes" : "no"}]
}
@ -8285,6 +8365,7 @@ tcl::namespace::eval punk::args {
set cw_aliases [Dict_getdef $ARG_INFO $thisname -choicealiases {}]
set cw_deny [Dict_getdef $ARG_INFO $thisname -choiceprefixdenylist {}]
set cw_reserve [Dict_getdef $ARG_INFO $thisname -choiceprefixreservelist {}]
set cw_unavail [Dict_getdef $ARG_INFO $thisname -choiceunavailable {}] ;#G-073
lassign [Dict_getdef $ARG_INFO $thisname -choicemultiple {1 1}] cw_cmmin cw_cmmax
}
}
@ -8304,12 +8385,14 @@ tcl::namespace::eval punk::args {
set member_satisfied 0
set member_choicechecked 0
set member_cwm {} ;#G-073: singular-screen matchinfo (consulted by the rejection builder)
if {$has_choices} {
if {$choicescreen_applies} {
#G-071 allocation screen (see block above the loop)
set member_choicechecked 1
if {$cw_cmmax == 1} {
set cwm [choiceword_match $v $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve]
set cwm [choiceword_match $v $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve $cw_unavail]
set member_cwm $cwm
set member_satisfied [tcl::dict::get $cwm matched]
} else {
#-choicemultiple: the word is itself a list of choices - screen
@ -8321,7 +8404,7 @@ tcl::namespace::eval punk::args {
} else {
set member_satisfied 1
foreach v_member $v {
set cwm [choiceword_match $v_member $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve]
set cwm [choiceword_match $v_member $cw_nocase $cw_allchoices $cw_aliases $cw_prefix $cw_deny $cw_reserve $cw_unavail]
if {![tcl::dict::get $cwm matched]} {
set member_satisfied 0
break
@ -8493,7 +8576,14 @@ tcl::namespace::eval punk::args {
#words like lseq's 'to' - overflow remains the clearer report).
if {[llength $thistype] == 1} {
if {$member_choicechecked} {
set rejection [tcl::dict::create kind choice argname $thisname word $v choices $cw_allchoices nocase $cw_nocase prefix $cw_prefix]
if {$member_cwm ne "" && [tcl::dict::get $member_cwm unavailable]} {
#G-073: the screen's non-match was a landing on a
#recognised-but-unavailable name - record the distinct kind so
#raise-time selection can emit the tailored report
set rejection [tcl::dict::create kind unavailable argname $thisname word $v name [tcl::dict::get $member_cwm canonical] choices $cw_allchoices]
} else {
set rejection [tcl::dict::create kind choice argname $thisname word $v choices $cw_allchoices nocase $cw_nocase prefix $cw_prefix]
}
} elseif {[llength $ctg_other]} {
set rejection [tcl::dict::create kind type argname $thisname word $v type $tp]
}
@ -9308,6 +9398,30 @@ tcl::namespace::eval punk::args {
#rename get_dict
#
#G-073: assemble the tailored recognised-but-unavailable rejection message.
#Shared by final validation and the G-082 allocation-rejection raise sites so the
#report is identical wherever the unavailable landing surfaces. supplied = the
#check-form word as received; name = the resolved -choiceunavailable entry;
#choicelabeldict provides the entry's documentation note when present (the wording
#steers the user: the note names the why - e.g a version boundary).
proc private::unavailable_choice_msg {argclass argname supplied name choicelabeldict allchoices} {
if {$supplied eq $name} {
set msg "$argclass '$argname' for %caller%: '$name' is a recognised name here, but is not available in this runtime/context."
} else {
set msg "$argclass '$argname' for %caller%: '$supplied' resolves to '$name' - a recognised name that is not available in this runtime/context."
}
if {[tcl::dict::exists $choicelabeldict $name]} {
append msg "\n note:"
foreach ln [split [tcl::dict::get $choicelabeldict $name] \n] {
append msg "\n [string trim $ln]"
}
}
if {[llength $allchoices]} {
append msg "\nAvailable values:\n [join $allchoices "\n "]"
}
return $msg
}
#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.
@ -9327,8 +9441,18 @@ tcl::namespace::eval punk::args {
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)"
@values -min 7 -max 7
canonical - the resulting choice value (empty when not matched,
EXCEPT when unavailable is 1 - it then carries the
matched unavailable name)
unavailable - boolean (G-073): the word landed - exactly, or as a
unique prefix - on a -choiceunavailable entry
(recognised but not available). matched stays 0;
callers wanting the tailored unavailability report
consult this key, existing matched-only consumers
treat it as an ordinary non-match. A prefix shared
between an available and an unavailable name is a
plain ambiguous non-match (unavailable 0)."
@values -min 7 -max 8
word -type string -help\
"The supplied word.
Callers pass the check-form, e.g ansistripped when applicable"
@ -9344,8 +9468,12 @@ tcl::namespace::eval punk::args {
"-choiceprefixdenylist (full word required for these names)"
reservelist -type list -help\
"-choiceprefixreservelist (phantom prefix-calculation members)"
unavailablelist -type list -optional 1 -default {} -help\
"-choiceunavailable (recognised-but-unavailable names - prefix-pool
members like the reservelist, but a word landing on one reports
unavailable 1 with the name as canonical) (G-073)"
}]
proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} {
proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist {unavailablelist {}}} {
set aliasnames [tcl::dict::keys $choicealiases]
set has_choicealiases [expr {[llength $aliasnames] > 0}]
set choicealiases_nocase [tcl::dict::create]
@ -9364,6 +9492,7 @@ tcl::namespace::eval punk::args {
set chosen ""
set choice_in_list 0
set choice_exact_match 0
set unavailable_hit 0 ;#G-073 recognised-but-unavailable landing
if {$choiceprefix} {
#can we handle empty string as a choice? It should just work - REVIEW/test
if {$word in $allchoices} {
@ -9390,6 +9519,14 @@ tcl::namespace::eval punk::args {
#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
} elseif {[llength $unavailablelist] && ($word in $unavailablelist || ($nocase && [lsearch -exact -nocase $unavailablelist $word] >= 0))} {
#G-073: exact match (case per -nocase) on a recognised-but-unavailable name
set unavailable_hit 1
if {$word in $unavailablelist} {
set chosen $word
} else {
set chosen [lsearch -inline -exact -nocase $unavailablelist $word]
}
} 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
@ -9397,10 +9534,19 @@ tcl::namespace::eval punk::args {
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]} {
set bestmatch [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$aliasnames {*}$reservelist {*}$unavailablelist] $word]
if {$bestmatch ne "" && $bestmatch in $unavailablelist} {
#G-073: prefix landed uniquely on an unavailable name
set unavailable_hit 1
set chosen $bestmatch
} elseif {$bestmatch eq "" || $bestmatch in $reservelist} {
set unavailablelist_lc [string tolower $unavailablelist]
set chosen [tcl::prefix::match -error "" [list {*}[lsort -unique $choices_test] {*}[tcl::dict::keys $choicealiases_nocase] {*}$reservelist {*}$unavailablelist_lc] $v_test]
if {$chosen ne "" && $chosen in $unavailablelist_lc} {
#G-073: nocase prefix landed uniquely on an unavailable name - report with its defined casing
set unavailable_hit 1
set chosen [lsearch -inline -exact -nocase $unavailablelist $chosen]
} elseif {$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 ""
@ -9431,10 +9577,15 @@ tcl::namespace::eval punk::args {
}
}
} else {
set matchedname [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$aliasnames {*}$reservelist] $word]
set matchedname [tcl::prefix::match -error "" [list {*}[lsort -unique $allchoices] {*}$aliasnames {*}$reservelist {*}$unavailablelist] $word]
if {$matchedname eq "" || $matchedname in $reservelist} {
set chosen ""
set choice_in_list 0
} elseif {$matchedname in $unavailablelist} {
#G-073: prefix landed uniquely on an unavailable name
set unavailable_hit 1
set chosen $matchedname
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} {
@ -9452,9 +9603,11 @@ tcl::namespace::eval punk::args {
}
#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)
#(a canonical reached via an alias is exempt - deny was already applied to the alias name;
# an unavailable landing is exempt - deny applies to choices, and the unavailable
# indication must survive to drive the tailored report)
#review - case difference edge cases in choiceprefixdenylist !todo
if {!$prefix_via_alias && $chosen in $denylist} {
if {!$unavailable_hit && !$prefix_via_alias && $chosen in $denylist} {
set choice_in_list 0
set chosen ""
}
@ -9474,9 +9627,17 @@ tcl::namespace::eval punk::args {
#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
} elseif {[llength $unavailablelist] && ($word in $unavailablelist || ($nocase && [lsearch -exact -nocase $unavailablelist $word] >= 0))} {
#G-073: exact match (case per -nocase) on a recognised-but-unavailable name
set unavailable_hit 1
if {$word in $unavailablelist} {
set chosen $word
} else {
set chosen [lsearch -inline -exact -nocase $unavailablelist $word]
}
}
}
return [tcl::dict::create matched $choice_in_list exact $choice_exact_match canonical $chosen]
return [tcl::dict::create matched $choice_in_list exact $choice_exact_match canonical $chosen unavailable $unavailable_hit]
}
#generally we expect values to contain leading dashes only if -- specified. Otherwise no reliable way determine difference between bad flags and values
@ -11100,6 +11261,12 @@ tcl::namespace::eval punk::args {
set rj_choices [dict get $rj choices]
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '$ldr'"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $ldr choices $rj_choices] -badarg $rj_argname -badval $ldr -argspecs $argspecs]] $msg
} elseif {[dict get $rj kind] eq "unavailable"} {
#G-073: recognised-but-unavailable landing recorded by the
#allocation screen - the tailored report, as final validation gives
set rj_name [dict get $rj name]
set msg [private::unavailable_choice_msg $rj_argclass $rj_argname $ldr $rj_name [Dict_getdef $argstate $rj_argname -choicelabels {}] [dict get $rj choices]]
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceunavailable $rj_name choices [dict get $rj choices]] -badarg $rj_argname -badval $ldr -argspecs $argspecs]] $msg
} else {
set rj_type [dict get $rj type]
set msg "$rj_argclass '$rj_argname' for %caller% requires type '$rj_type'. Received: '$ldr'"
@ -11351,6 +11518,13 @@ tcl::namespace::eval punk::args {
set rj_choices [dict get $rj choices]
set msg "$rj_argclass '$rj_argname' for %caller% must be one of the listed values:\n [join $rj_choices "\n "]\n$rj_casemsg$rj_prefixmsg. Received: '$val'"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $val choices $rj_choices] -badarg $rj_argname -badval $val -argspecs $argspecs]] $msg
} elseif {[dict get $rj kind] eq "unavailable"} {
#G-073: recognised-but-unavailable landing recorded by the
#allocation screen - the tailored report, as final validation gives
#(MAINTENANCE - same selection logic as leaders loop above)
set rj_name [dict get $rj name]
set msg [private::unavailable_choice_msg $rj_argclass $rj_argname $val $rj_name [Dict_getdef $argstate $rj_argname -choicelabels {}] [dict get $rj choices]]
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceunavailable $rj_name choices [dict get $rj choices]] -badarg $rj_argname -badval $val -argspecs $argspecs]] $msg
} else {
set rj_type [dict get $rj type]
set msg "$rj_argclass '$rj_argname' for %caller% requires type '$rj_type'. Received: '$val'"
@ -11759,6 +11933,9 @@ tcl::namespace::eval punk::args {
#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 {}]
#-choiceunavailable (G-073): recognised-but-unavailable names - prefix-pool members;
#a word landing on one earns the tailored choiceunavailable rejection below.
set choiceunavailable [Dict_getdef $thisarg -choiceunavailable {}]
set choicerestricted [tcl::dict::get $thisarg -choicerestricted]
set choicemultiple [tcl::dict::get $thisarg -choicemultiple]
if {[string is integer -strict $choicemultiple]} {
@ -11836,7 +12013,7 @@ tcl::namespace::eval punk::args {
#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 matchinfo [choiceword_match $c_check $nocase $allchoices $choicealiases $choiceprefix $choiceprefixdenylist $choiceprefixreservelist $choiceunavailable]
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]
@ -11913,6 +12090,16 @@ tcl::namespace::eval punk::args {
} else {
#unhappy path
#G-073: the word landed (exact, or unique prefix) on a
#recognised-but-unavailable name - tailored report naming the
#entry and its -choicelabels note instead of the generic
#listed-values error.
if {[tcl::dict::get $matchinfo unavailable]} {
set cu_name [tcl::dict::get $matchinfo canonical]
set msg [private::unavailable_choice_msg $argclass $argname $c_check $cu_name [Dict_getdef $thisarg -choicelabels {}] $allchoices]
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceunavailable $cu_name choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg
}
#if prefixes allowed, first see if c_check is an ambiguous prefix
#This is preferable to listing all (possibly many) choices in the error message.
if {$choiceprefix} {
@ -11920,11 +12107,20 @@ tcl::namespace::eval punk::args {
#review - case
if {$nocase} {
set longermatches [lsearch -all -inline -nocase $allchoices "$c_check*"]
set longermatches_unavail [lsearch -all -inline -nocase $choiceunavailable "$c_check*"]
} else {
set longermatches [lsearch -all -inline $allchoices "$c_check*"]
set longermatches_unavail [lsearch -all -inline $choiceunavailable "$c_check*"]
}
#G-073: unavailable names participate in prefix ambiguity -
#display them annotated so the ambiguity is explicable, but
#never as selectable suggestions
set longermatches_display $longermatches
foreach cu_lm $longermatches_unavail {
lappend longermatches_display "$cu_lm (recognised but unavailable)"
}
if {[llength $longermatches]} {
set msg "$argclass '$argname' for %caller% seems to be an ambiguous prefix. Try one of:\n [join $longermatches "\n "]\n$casemsg$prefixmsg. Received: '$c_check'"
if {[llength $longermatches] || [llength $longermatches_unavail] > 1} {
set msg "$argclass '$argname' for %caller% seems to be an ambiguous prefix. Try one of:\n [join $longermatches_display "\n "]\n$casemsg$prefixmsg. Received: '$c_check'"
return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list choiceviolation $c choices $allchoices] -badarg $argname -badval $c_check -argspecs $argspecs]] $msg
}
} else {

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

@ -1,6 +1,7 @@
0.15.2
0.16.0
#First line must be a semantic version number
#all other lines are ignored.
#0.16.0 - G-073 unavailable choices: new per-argument key -choiceunavailable <list> - names that are recognised but not available in the current runtime/context. Display: entries render among the choices under a dedicated 'Unavailable (recognised, not selectable here)' group heading (table title / string-renderer Group heading - the distinguishing mechanism) with their ordinary -choicelabels notes, and join the prefix-calculation pool (display shortest-prefix marking reflects their reserving effect); synopsis choice-literal display and value-in-effect/goodarg marking never include them (structural: they are not in -choices/-choicegroups, and -default may not name one). Parse: choiceword_match gains an optional 8th argument unavailablelist and an 'unavailable' result key - a word landing on an unavailable name (exact, or unique prefix; case per -nocase) reports unavailable 1 with the name as canonical while matched stays 0, so matched-only consumers treat it as an ordinary non-match; a prefix shared between an available and an unavailable name is a plain ambiguous non-match. Validation rejects such a landing with the tailored PUNKARGS VALIDATION {choiceunavailable <name> choices <list>} error (-badarg/-badval; message names the entry, its -choicelabels note - e.g a version boundary - and the available values; private::unavailable_choice_msg shared by all raise surfaces). The G-071 allocation screen honours the list (optional-arg landings recorded as G-082 rejection kind 'unavailable' and preferred at the overflow raise sites); required args are unscreened as before, so their landing reports via final validation identically. Ambiguous-prefix messages annotate unavailable longer matches '(recognised but unavailable)'. -choicerestricted 0 keeps full passthrough (no tailored rejection). Resolve-time validation: requires -choices/-choicegroups; entries must not collide with choices, -choicealiases alias names, or -choiceprefixreservelist entries; -default may not be an unavailable name. form_literal_affinity/formcheck screens pass the list through (uniform pool semantics). In-tree adopter: punk::args::moduledoc::tclcore 'string is' forward classes (dict on 8.6). New testsuite choiceunavailable.test; tclcoreparity.test gains forward-class pins + the user-sanctioned 'di' strictness exemption; define -help documents the key.
#0.15.2 - G-082 follow-on: a REQUIRED typed value or leader whose present word fails the basic-type allocation screen (int/double/bool/number/dict) now reports the pointed typemismatch (message + errorcode with -badarg/-badval, same shape as the G-082 overflow-site selection) instead of the generic missingrequiredvalue/missingrequiredleader shortfall - the in-loop required-arg raise sites consult the rejection record returned by get_dict_can_assign_value for the current word. Unchanged by design: end-of-input shortages (valuemissing/count classes), a word failing a required literal-typed member, and multi-member clause partials (no rejection record - conservatism preserved); required-arg choice words were never screened (G-071 asymmetry) so choiceviolation reporting is untouched. parse_status consequences: this shape's top-level status flips incomplete->invalid with failureclass typemismatch (the formstatus verdict was already 'invalid' - the G-152 probe confirmed each such 'incomplete' away before reporting; one probe re-parse saved); multiform noformmatch classes unchanged (already invalid), per-form failureclass/message now the pointed typemismatch. parse_status argdoc status-key note updated. Pins flipped deliberately: parsestatus.test typed-value allocation failure, formviability.test single-form record (top-status/formstatus divergence demonstration re-anchored on a required-option shape), punk/ns cmdhelp.test badarg message match; new errorselection.test required-position coverage (value + leader) with unchanged-shape guards (end-of-input, literal-typed, clause partial).
#0.15.1 - G-164 viability-probe alternative-allocation re-probe: when the valmin reservation seats a word that literally names a declared required option into a required value slot at end-of-input, and the resulting optionmissing stands in the G-152 viability probe (a value word consumed, defeating the position guard), the probe now re-parses with the stolen word(s) consumed as the options they name (private::get_dict_form's viabilitycheck argument carries {1 reseat_words} on the re-probe pass) and grants 'incomplete' (viable) on pure end-of-input exhaustion after that re-seating. Cures the 'i hash -t' false negative: the test form classes viable since {-t foo} parses; sibling forms stay invalid. Options still missing after the re-seating are ordinary exhaustion in the re-probe. Runtime seating is unchanged - the re-probe runs only inside the probe, only via reservation-seated words (the trailing-flag-as-value rule is load-bearing; flagvalues_plain_then_dash and the runtime optionmissing path pinned unchanged). A stolen word naming a value-TAKING required option stays conservative (invalid) - re-seated, it seats as a value again at end-of-input. Verdict contract documentation updated in the parse/parse_status argdoc. formviability.test gains 7 pins (re-probe, delete-form, other-missing-options exhaustion, unnamed stolen word, value-taking conservatism, runtime-unchanged, single-form formstatus); the cmdhelp.test GAP pin cmdhelp_GAP_multiform_flagled_testform_false_notviable flips to cmdhelp_multiform_flagled_testform_viable.
#0.15.0 - @values -flagvalues (G-150 follow-on, user-directed): a @values section may declare -flagvalues 1 - an unmatched dash-led word in option position is then consumed as a trailing value instead of raising invalidoption (declared options still match first; the '--' end-of-options marker is unaffected; off by default). Motivation: '--help'-style forms robustly swallowing ignored trailing words of any shape - previously such a word seated as a value only positionally (at end-of-input via the valmin reservation, or within a bounded valmax) and errored 'Unexpected option' otherwise ('{--help -t foo}' on the unconstrained shape). Mechanism: VAL_FLAGVALUES on the form dict (template default false; @values compiler key, boolean-validated at resolve time) short-circuits the unmatched-dash-word fallback in the parse engine. In-tree consumer: punk::auto_exec::hash's help form. New testsuite flagvalues.test (9 tests: seating matrix, declared-option precedence, off-by-default contrast, optionmissing after value seating, key validation). Documented in the module header notes.

29
src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm

@ -46,7 +46,7 @@
#[list_begin itemized]
package require Tcl 8.6-
package require punk::args
package require punk::args 0.16.0- ;#G-073: 'string is' model uses -choiceunavailable
package require punk::ansi
package require textblock
#*** !doctools
@ -9884,6 +9884,21 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
set string_is_classes {alnum alpha ascii boolean control dict digit double entier false graph integer list lower print punct space true upper wideinteger wordchar xdigit}
}
set string_is_classes [lsort $string_is_classes] ;#display order (as the previous hand-written list)
#G-073: curated forward-class list - classes later Tcl versions provide that THIS
#runtime may lack. On such runtimes (8.6 for dict) the model declares them
#-choiceunavailable: displayed among the choices with their version-note label,
#reserving prefixes (so an 8.6 user's 'string is di' habit is broken before 9.x
#makes it genuinely ambiguous), and rejected with the tailored unavailability
#message. The list is explicitly curated, never derived: 'unicode' is deliberately
#EXCLUDED - it exists only in the unreleased 8.7 series (removed in Tcl 9) and is
#never a forward target.
set string_is_forward_classes {dict}
set string_is_unavailable [list]
foreach _sis_c $string_is_forward_classes {
if {$_sis_c ni $string_is_classes} {
lappend string_is_unavailable $_sis_c
}
}
#hand-written class descriptions (man-page derived, verbatim) - applied below only for
#classes the running interpreter accepts; accepted classes without an entry get a
#generic label. tstr here resolves the ${$A_WARN}/${$A_RST} highlights as before.
@ -10011,6 +10026,17 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
append string_is_choicelabels [list $_sis_c] " " [list " (class accepted by this Tcl\n runtime - not yet described\n in the punk tclcore docs)"] \n
}
}
#G-073: unavailable (forward) classes carry their static description as an ordinary
#choicelabel - it renders in the Unavailable display group, feeds the tailored
#rejection message's note, and drives the per-class virtual id loop below (so
#'i string is dict' documents the class on 8.6 from the same static description).
foreach _sis_c $string_is_unavailable {
if {[dict exists $string_is_class_descriptions $_sis_c]} {
append string_is_choicelabels [list $_sis_c] " " [list [dict get $string_is_class_descriptions $_sis_c]] \n
} else {
append string_is_choicelabels [list $_sis_c] " " [list " (recognised forward class -\n not present in this Tcl\n runtime)"] \n
}
}
unset -nocomplain _sis_msg _sis_csv _sis_c
punk::args::define [punk::args::lib::tstr -return string {
@ -10024,6 +10050,7 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
@leaders -min 1 -max 1
class -type string\
-choices {${$string_is_classes}}\
-choiceunavailable {${$string_is_unavailable}}\
-choicelabels {${$string_is_choicelabels}}\
-help\
"character class

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

@ -1,6 +1,7 @@
0.3.4
0.4.0
#First line must be a semantic version number
#all other lines are ignored.
#0.4.0 - G-073 'string is' forward-class adoption: a curated forward-class list (dict; 'unicode' deliberately excluded - unreleased-8.7-only, removed in Tcl 9) is diffed against the harvested live class set at define time, and classes this runtime lacks are declared -choiceunavailable (punk::args 0.16.0 - the require gains that floor). Effect on 8.6: dict displays among the classes under the Unavailable group with its static description ("class not present in Tcl 8.6"), 'string is dict' is rejected with the tailored choiceunavailable error naming that note, 'string is di' is ambiguous (deliberately stricter than real 8.6, which accepts 'di' as digit - preparing users for 9.x where dict makes it genuinely ambiguous; recorded as a user-sanctioned parity exemption in tclcoreparity.test, full words parity-true), and 'i string is dict' documents the class via its per-class virtual id (the choicelabels build and the virtual-id loop now cover unavailable classes from the same static descriptions). Modern runtimes (8.7/9.x - dict live) compute an empty unavailable list and behave identically to 0.3.4.
#0.3.4 - G-074: the documented cancelid/cancelscript overlap on ::after cancel is sanctioned via the new @form -overlapallowed key (punk::args 0.12.0) - punk::args::formcheck now reports it as an acknowledged (sanctioned) structural overlap rather than an actionable finding, leaving ::after with zero unsanctioned findings. Parse behaviour unchanged: an id-shaped 'after cancel' word still raises multipleformmatches (the runtime-liveness ambiguity real Tcl resolves by trying the id first - 0.3.0 record). ::lseq deliberately NOT sanctioned: its range/start_count and range/count overlaps are type-weakness findings (the expr-typed end slot swallows the 'count'/'by' discriminator words) - kept visible pending an expr syntax-validating type (G-069/G-070 territory, G-055 operand-typing record).
#0.3.3 - removed the parse-inert @dynamic tags from ::split, ::array and ::join (user-directed; identified by the 0.3.2 sweep). None has round-2 substitution content, and their ${...} placeholders are all display-field styling/examples - the definitions now resolve as ordinary static definitions with display expansion cached per raw definition (previously @dynamic forced display re-expansion on every render). No bad-@dynamic warnings remain across the module's 462 registered ids; renders and parses verified unchanged for all three.
#0.3.2 - fixed malformed ::tcl_startOfNextWord definition: its -help was double-quoted but the embedded man-page example contains inner double quotes (set theString "The quick brown fox" / puts "Word start index: ..."), so the value terminated early and the definition failed to resolve at all (punk::args::resolve 'bad optionspecs line' error; 'i tcl_startOfNextWord' broken). The -help is now braced (fully literal per the define quoting rules - inner quotes and brackets safe, text verbatim, example braces balance). Found by a bad-@dynamic sweep across all 462 registered ids (punk::args 0.11.2 work); the same sweep identified the parse-inert @dynamic tags on ::split, ::array and ::join (each now warns once per interp) - left in place pending a decision on their display-refresh semantics (@dynamic still forces display-field re-expansion per render).

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

@ -4924,6 +4924,7 @@ y" {return quirkykeyscript}
[punk::args::system::Dict_getdef $pf_arginfo -choiceprefix 1]\
[punk::args::system::Dict_getdef $pf_arginfo -choiceprefixdenylist {}]\
[punk::args::system::Dict_getdef $pf_arginfo -choiceprefixreservelist {}]\
[punk::args::system::Dict_getdef $pf_arginfo -choiceunavailable {}]\
]
if {[dict get $pf_matchinfo matched]} {
set pf_canonical [dict get $pf_matchinfo canonical]
@ -5067,9 +5068,19 @@ y" {return quirkykeyscript}
}
#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.
#canonical (choiceinfo is keyed on canonicals), -choiceprefixdenylist,
#-choiceprefixreservelist and -choiceunavailable (G-073) are honoured.
#Scope of that parity claim: CHOICE-WORD RESOLUTION - this traverse never
#treats a word as identifying a choice that parsing would reject (unknown,
#ambiguous, denied, reserved and unavailable words all resolve no choice
#here). Documentation ADDRESSING is a separate, deliberately broader axis:
#the exact space-form docid checks (id_exists, earlier in the walk) reach
#doc-only ids without implying the word parses - that is how per-class ids
#like 'string is true' have always documented argument words, and how an
#UNAVAILABLE word's virtual id ('string is dict' on 8.6) documents since
#G-073. Consequence: cmdinfo classes both landings 'doconly' with no
#availability distinction - surfacing that (cmdinfo axis / render marking)
#is deliberately not decided here.
set ct_matchinfo [punk::args::choiceword_match $q\
[punk::args::system::Dict_getdef $arginfo -nocase 0]\
$allchoices\
@ -5077,6 +5088,7 @@ y" {return quirkykeyscript}
[dict get $arginfo -choiceprefix]\
[punk::args::system::Dict_getdef $arginfo -choiceprefixdenylist {}]\
[punk::args::system::Dict_getdef $arginfo -choiceprefixreservelist {}]\
[punk::args::system::Dict_getdef $arginfo -choiceunavailable {}]\
]
if {![dict get $ct_matchinfo matched]} {
#no match under parse rules (covers: unknown word, ambiguous prefix,

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

@ -1,6 +1,7 @@
0.9.2
0.9.3
#First line must be a semantic version number
#all other lines are ignored.
#0.9.3 - G-073 doc-walk parity: both cmd_traverse choiceword_match call sites (the G-051 space-form prefix-parity branch and the choice-traverse subcommand resolution) now pass the argument's -choiceunavailable list (punk::args 0.16.0 8th resolver argument), so the doc walk honours recognised-but-unavailable names identically to parsing: a prefix shadowed into ambiguity by an unavailable name resolves no documentation, and an unavailable word is not accepted as a choice (its documentation remains reachable via the exact space-form virtual docid, checked before the choice traverse - e.g 'i string is dict' on Tcl 8.6 via the tclcore moduledoc 0.4.0 per-class ids). No behaviour change for definitions without the key.
#0.9.2 - documentation-only PUNKARGS update: cmdhelp's 'subcommand' argument help now states the multi-form narrowing contract explicitly (argument words resolving to exactly one form narrow the usage display to that form's argument table, marked in the synopsis; no words, an ambiguous match, or words no form accepts keep the whole-command render). No behaviour change - the G-041 advisory-parse candidacy already implements it; G-150 pins the contract in cmdhelp.test against the now selection-sound punk::auto_exec::hash declaration (auto_exec 0.1.1).
#0.9.1 - get_ns_dicts fix: packagetails/packageprefixes initialization moved inside the per-namespace loop - with multiple matched namespaces (path globs, trailing **) every dict after the first carried the union of all earlier namespaces' entries, mis-marking non-package children in nslist/n// underline styling and in the returned data; a child with several deeper packages was also appended to packageprefixes once per matching package (now once). Single-namespace queries (the nslist.test tier A pins) unaffected; the multi-namespace reset is pinned in nslist.test (nslist_classification_multins_package_buckets_reset). Documentation-only PUNKARGS added for get_ns_dicts (documents the returned per-namespace dict keys and -allbelow/-nspathcommands; parsing stays manual) and nslist_dict (caller-relative wrapper). Motivated by exposing the dict data layer to agent tooling: scriptlib/developer/nslist.tcl backing the new tcl-nslist agent skill.
#0.9.0 - cmdhelp -return text: plain-text renderer for machine/LLM consumption - no ANSI codes, no table layout, and the argument section of EVERY command form (or of each form in an explicit -form selection) rendered in a single output, unlike the interactive displays which render only the G-041 best-matching form's argument table ('i -return text after' shows all six after forms' argument sections under one common header). Implementation: resolution is delegated to the existing -return dict recursion (aliases/ensembles/docid/form candidacy all inherited); each form's argument section renders via arg_error -return string -scheme nocolour restricted with -form, is ansistripraw'd, and multiform outputs join the per-form sections (headed ' --ARGUMENTS (form <name>)-- ') below the common header+synopsis emitted once. Argument words still restrict the rendered forms per G-041 semantics ('i -return text after cancel xx' renders the matched cancelscript form only); no advisory-parse error preamble is included (the text form is documentation, not validation - use -return dict for parse status). -grepstr applies to the final text as for other return forms. Supported by the punk::args 0.12.6 arg_error changes (message below usage once; single-line 'name TYPE:... ?DEFAULT:...? ?MULTI:yes?' argument rows in the string renderer). Tests: cmdhelp.test cmdhelp_return_text_all_forms / _argword_form_selection / _single_form_plain_marker.

272
src/tests/modules/punk/args/testsuites/args/choiceunavailable.test

@ -0,0 +1,272 @@
package require tcltest
package require punk::args
package require punk::ansi
#added 2026-08-05 (agent, G-073) - -choiceunavailable: recognised-but-unavailable choice
#names (goals/G-073-punkargs-unavailable-choices.md). Covers the shared resolver's
#distinct 'unavailable' indication (punk::args::choiceword_match 8th argument),
#definition-resolve validation, the tailored choiceunavailable rejection at final
#validation and via the G-082 allocation-rejection seam, prefix-ambiguity
#participation, -choicerestricted 0 passthrough, parse_status classification/marking,
#renderer display (dedicated Unavailable group in table and string forms), synopsis
#choice-literal exclusion and value-in-effect exclusion. The in-tree adopter (tclcore
#'string is' forward classes) is pinned in tclcoreparity.test.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
#SGR colour generation is gated by punk::console colour state (e.g NO_COLOR env) -
#force colour on for deterministic value-in-effect marking output (as usagemarking.test)
catch {package require punk::console}
if {[info commands ::punk::console::colour] ne ""} {
punk::console::colour on
}
#slim an errorcode: classification triple plus -badarg/-badval extras (drop -argspecs)
proc cu_ecinfo {opts} {
set ec [dict get $opts -errorcode]
set extras [dict create]
foreach {k v} [lrange $ec 3 end] {
if {$k eq "-argspecs"} {break}
dict set extras $k $v
}
return [list [lrange $ec 0 2] $extras]
}
test cu_resolver_indication {choiceword_match reports unavailable landings distinctly: exact/unique-prefix/nocase land, shared prefixes stay plain ambiguous non-matches, 7-arg calls are unchanged}\
-setup $common -body {
#exact landing
lappend result [punk::args::choiceword_match dict 0 {digit double} {} 1 {} {} {dict}]
#unique prefix landing
lappend result [punk::args::choiceword_match dic 0 {digit double} {} 1 {} {} {dict}]
#prefix shared between available (digit) and unavailable (dict): plain ambiguous
lappend result [punk::args::choiceword_match di 0 {digit double} {} 1 {} {} {dict}]
#available word matching is unaffected by the pool addition
lappend result [punk::args::choiceword_match do 0 {digit double} {} 1 {} {} {dict}]
#nocase exact + nocase prefix landings report the defined casing
lappend result [punk::args::choiceword_match DICT 1 {digit double} {} 1 {} {} {dict}]
lappend result [punk::args::choiceword_match DIC 1 {digit double} {} 1 {} {} {dict}]
#-choiceprefix 0: exact landing only - a prefix word is a plain non-match
lappend result [punk::args::choiceword_match dict 0 {digit double} {} 0 {} {} {dict}]
lappend result [punk::args::choiceword_match dic 0 {digit double} {} 0 {} {} {dict}]
#backward-compat: 7-argument call shape (no unavailablelist)
lappend result [punk::args::choiceword_match digit 0 {digit double} {} 1 {} {}]
}\
-cleanup {
}\
-result [list\
{matched 0 exact 0 canonical dict unavailable 1}\
{matched 0 exact 0 canonical dict unavailable 1}\
{matched 0 exact 0 canonical {} unavailable 0}\
{matched 1 exact 0 canonical double unavailable 0}\
{matched 0 exact 0 canonical dict unavailable 1}\
{matched 0 exact 0 canonical dict unavailable 1}\
{matched 0 exact 0 canonical dict unavailable 1}\
{matched 0 exact 0 canonical {} unavailable 0}\
{matched 1 exact 1 canonical digit unavailable 0}\
]
test cu_resolve_validation_errors {definition resolve rejects colliding/orphaned -choiceunavailable declarations and an unavailable -default}\
-setup $common -body {
#resolve is lazy - parsing a valid word forces it
foreach {label def} {
collision {{class -type string -choices {digit dict} -choiceunavailable {dict}}}
nochoices {{class -type string -choiceunavailable {dict}}}
default {{class -type string -choices {digit} -default dict -choiceunavailable {dict}}}
alias {{class -type string -choices {digit} -choicealiases {dict digit} -choiceunavailable {dict}}}
reserve {{class -type string -choices {digit} -choiceprefixreservelist {dict} -choiceunavailable {dict}}}
} {
set docid ::testspace::cu_badresolve_$label
punk::args::define [list @id -id $docid] @values {*}$def
set err [catch {punk::args::parse {digit} withid $docid} msg]
lappend result $label:$err:[string match "*-choiceunavailable*" $msg][string match "*punk::args::resolve*" $msg]
punk::args::undefine $docid 1
}
set result
}\
-cleanup {
}\
-result [list collision:1:11 nochoices:1:11 default:1:11 alias:1:11 reserve:1:11]
test cu_required_tailored_rejection {a word landing on an unavailable name (exact or unique prefix; leader, value or option position) reports the tailored choiceunavailable error naming the entry and its choicelabel note}\
-setup $common -body {
set docid1 ::testspace::cu_reqleader
set docid2 ::testspace::cu_reqvalue
set docid3 ::testspace::cu_reqopt
set docids [list $docid1 $docid2 $docid3]
punk::args::define [list @id -id $docid1] {@leaders -min 1 -max 1} {class -type string -choices {digit double integer} -choicelabels {dict " (class not present in\n this runtime)"} -choiceunavailable {dict}} {@values -min 1 -max 1} {str -type string -optional 0}
punk::args::define [list @id -id $docid2] {@values -min 1 -max 1} {kind -type string -choices {alpha beta} -choicelabels {gamma "(requires the frobnitz build)"} -choiceunavailable {gamma}}
punk::args::define [list @id -id $docid3] @opts {-mode -type string -choices {fast safe} -choiceunavailable {turbo}} {@values -min 0 -max 0}
#exact landing - leader position
set err [catch {punk::args::parse {dict xxx} withid $docid1} msg opts]
lappend result $err {*}[cu_ecinfo $opts]
lappend result [string match "*'dict' is a recognised name here, but is not available*" $msg]
lappend result [string match "*class not present in*" $msg]
#unique-prefix landing - message names both the supplied word and the resolved entry
set err [catch {punk::args::parse {dic xxx} withid $docid1} msg opts]
lappend result $err {*}[cu_ecinfo $opts]
lappend result [string match "*'dic' resolves to 'dict'*not available*" $msg]
#value position, with the note steering to the why
set err [catch {punk::args::parse {gamma} withid $docid2} msg opts]
lappend result $err {*}[cu_ecinfo $opts]
lappend result [string match "*requires the frobnitz build*" $msg]
#option-value position
set err [catch {punk::args::parse {-mode turbo} withid $docid3} msg opts]
lappend result $err {*}[cu_ecinfo $opts]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list\
1 {PUNKARGS VALIDATION {choiceunavailable dict choices {digit double integer}}} {-badarg class -badval dict} 1 1\
1 {PUNKARGS VALIDATION {choiceunavailable dict choices {digit double integer}}} {-badarg class -badval dic} 1\
1 {PUNKARGS VALIDATION {choiceunavailable gamma choices {alpha beta}}} {-badarg kind -badval gamma} 1\
1 {PUNKARGS VALIDATION {choiceunavailable turbo choices {fast safe}}} {-badarg -mode -badval turbo}\
]
test cu_shared_prefix_ambiguous {a prefix shared between an available and an unavailable name is rejected as an ambiguous choiceviolation, with the unavailable match annotated in the message}\
-setup $common -body {
set docid ::testspace::cu_ambig
set docids [list $docid]
punk::args::define [list @id -id $docid] {@leaders -min 1 -max 1} {class -type string -choices {digit double integer} -choiceunavailable {dict}} {@values -min 1 -max 1} {str -type string -optional 0}
set err [catch {punk::args::parse {di xxx} withid $docid} msg opts]
lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0]
lappend result [string match "*ambiguous prefix*" $msg]
lappend result [string match "*dict (recognised but unavailable)*" $msg]
#an unknown word keeps the plain listed-values choiceviolation
set err [catch {punk::args::parse {zebra xxx} withid $docid} msg opts]
lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0]
lappend result [string match "*must be one of the listed values*" $msg]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list 1 choiceviolation 1 1 1 choiceviolation 1]
test cu_optional_overflow_seam {an OPTIONAL argument's unavailable landing surfaces the tailored error through the G-082 allocation-rejection seam instead of the generic overflow}\
-setup $common -body {
set docid ::testspace::cu_optseam
set docids [list $docid]
punk::args::define [list @id -id $docid] {@values -min 0 -max -1} {kind -type string -optional 1 -choices {alpha beta} -choiceunavailable {gamma}}
set err [catch {punk::args::parse {gamma} withid $docid} msg opts]
lappend result $err {*}[cu_ecinfo $opts]
lappend result [string match "*'gamma' is a recognised name here, but is not available*" $msg]
#a plain unknown word keeps the G-082 pointed choiceviolation
set err [catch {punk::args::parse {zebra} withid $docid} msg opts]
lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list\
1 {PUNKARGS VALIDATION {choiceunavailable gamma choices {alpha beta}}} {-badarg kind -badval gamma} 1\
1 choiceviolation\
]
test cu_choicerestricted0_passthrough {with -choicerestricted 0 an unavailable name passes through as an ordinary value - no tailored rejection}\
-setup $common -body {
set docid ::testspace::cu_unrestricted
set docids [list $docid]
punk::args::define [list @id -id $docid] {@values -min 1 -max 1} {word -type string -choices {alpha} -choicerestricted 0 -choiceunavailable {gamma}}
set argd [punk::args::parse {gamma} withid $docid]
lappend result [dict get $argd values]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list {word gamma}]
test cu_parse_status_classification {parse_status reports an unavailable landing as invalid/choiceunavailable with the argument badarg-marked}\
-setup $common -body {
set docid ::testspace::cu_pstat
set docids [list $docid]
punk::args::define [list @id -id $docid] {@values -min 1 -max 1} {kind -type string -choices {alpha beta} -choiceunavailable {gamma}}
set ps [punk::args::parse_status {gamma} withid $docid]
lappend result [dict get $ps ok] [dict get $ps status] [dict get $ps scheme]
lappend result [dict get $ps failureclass] [dict get $ps badarg]
lappend result [dict get $ps argstatus kind status] [dict get $ps argstatus kind failureclass]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list 0 invalid error choiceunavailable kind bad choiceunavailable]
test cu_render_group_and_note {both renderers display unavailable entries among the choices under the dedicated Unavailable group heading with their choicelabel notes}\
-setup $common -body {
set docid ::testspace::cu_render
set docids [list $docid]
punk::args::define [list @id -id $docid] {@values -min 1 -max 1} {kind -type string -choices {alpha beta} -choicelabels {gamma "(requires the frobnitz build)"} -choiceunavailable {gamma}}
set tbl [punk::ansi::ansistrip [punk::args::arg_error "" [punk::args::get_spec $docid] -aserror 0]]
lappend result [string match "*Unavailable (recognised, not selectable here)*" $tbl]
lappend result [string match "*gamma*" $tbl]
lappend result [string match "*requires the frobnitz build*" $tbl]
set str [punk::ansi::ansistrip [punk::args::arg_error "" [punk::args::get_spec $docid] -aserror 0 -return string]]
lappend result [string match "*Group 'Unavailable (recognised, not selectable here)':*" $str]
lappend result [string match "*- `gamma`*" $str]
lappend result [string match "*requires the frobnitz build*" $str]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list 1 1 1 1 1 1]
test cu_synopsis_literal_exclusion {synopsis choice-literal display excludes unavailable entries (a small available set still renders as literal alternates)}\
-setup $common -body {
set docid ::testspace::cu_syn
set docids [list $docid]
punk::args::define [list @id -id $docid] {@cmd -name testspace::cu_syn} {@values -min 1 -max 1} {kind -type string -choices {alpha beta} -choiceunavailable {gamma}}
set syn [punk::ansi::ansistrip [punk::args::synopsis $docid]]
lappend result [string match "*alpha|beta*" $syn]
lappend result [string match "*gamma*" $syn]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list 1 0]
#value-in-effect/goodchoice highlighting exclusion is pinned in usagemarking.test
#(usagemarking_goodchoice_unavailable_never_marked) - the SGR-marking helpers live
#there. The structural half of the guarantee is pinned here: an unavailable name can
#be neither a parsed value (tailored rejection above) nor a -default
#(cu_resolve_validation_errors 'default' case).
test cu_parse_result_never_carries_unavailable {successful parses of every non-rejecting shape yield values from the available set only}\
-setup $common -body {
set docid ::testspace::cu_novalue
set docids [list $docid]
punk::args::define [list @id -id $docid] {@values -min 0 -max -1} {kind -type string -optional 1 -choices {alpha beta} -default alpha -choiceunavailable {gamma}}
#unsupplied: default in effect
lappend result [dict get [punk::args::parse {} withid $docid] values kind]
#supplied available choice
lappend result [dict get [punk::args::parse {beta} withid $docid] values kind]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list alpha beta]
}
tcltest::cleanupTests ;#needed to produce test summary line.

76
src/tests/modules/punk/args/testsuites/args/tclcoreparity.test

@ -126,16 +126,20 @@ namespace eval ::testspace {
}\
-result [list {}]
test tclcoreparity_stringis_version_notes {version-difference labels appear exactly when the class is present in this runtime}\
#flipped 2026-08-05 (agent, G-073) - previously pinned "labels appear exactly when the
#class is present in this runtime" (dict's label ABSENT on 8.6). The forward-class
#adoption deliberately changes that: dict is declared -choiceunavailable on 8.6 and
#its static description (carrying the version note) now renders there in the
#Unavailable group - so the dict label is present on EVERY runtime. unicode is not a
#forward class (unreleased-8.7-only) and keeps the old live-only rule.
test tclcoreparity_stringis_version_notes {version-difference labels appear when the class is live - or a declared forward unavailable class (dict labels on every runtime since G-073)}\
-constraints have_tclcoredocs\
-setup $common -body {
set labels [dict get [lrange [punk::args::resolved_def -types leaders ::tcl::string::is class] 1 end] -choicelabels]
set classes [live_classes]
if {"dict" in $classes} {
lappend result [string match "*not present in*8.6*" [dict get $labels dict]]
} else {
lappend result [dict exists $labels dict]
}
#dict: live on 8.7/9.x, forward-declared unavailable on 8.6 - the static
#description with its 8.6 version note is present either way
lappend result [string match "*not present in*8.6*" [dict get $labels dict]]
if {"unicode" in $classes} {
lappend result [string match "*unreleased Tcl 8.7*" [dict get $labels unicode]]
} else {
@ -146,7 +150,65 @@ namespace eval ::testspace {
}\
-cleanup {
}\
-result [list [expr {"dict" in [live_classes]}] [expr {"unicode" in [live_classes]}] 1]
-result [list 1 [expr {"unicode" in [live_classes]}] 1]
#added 2026-08-05 (agent, G-073) - forward-class adoption: classes the curated
#forward list carries (dict; unicode deliberately excluded as unreleased-8.7-only)
#are declared -choiceunavailable on runtimes that lack them. All expectations are
#live-derived invariants that hold on 8.6, 8.7 and 9.x.
test tclcoreparity_stringis_forward_class_adoption {forward classes this runtime lacks are declared unavailable with label + virtual id; full-word outcome parity holds}\
-constraints have_tclcoredocs\
-setup $common -body {
set spec [lrange [punk::args::resolved_def -types leaders ::tcl::string::is class] 1 end]
set unavail [expr {[dict exists $spec -choiceunavailable] ? [dict get $spec -choiceunavailable] : ""}]
set classes [live_classes]
#the unavailable set is exactly the curated forward list minus the live set
lappend result [expr {$unavail eq [expr {"dict" in $classes ? {} : {dict}}]}]
#'string is dict x': error-vs-ok outcome parity on every runtime (both accept
#where dict is live, both reject where it is not - the model's rejection is
#the tailored one)
lappend result [expr {[real_ok {dict x}] == [doc_ok {dict x}]}]
#where dict is unavailable the model's rejection classifies choiceunavailable
set ps [punk::args::parse_status {dict x} withid ::tcl::string::is]
lappend result [expr {"dict" in $classes ? 1 : ([dict get $ps failureclass] eq "choiceunavailable")}]
#the dict label (with its static description) and per-class virtual id exist
#on every runtime - unavailable classes document via the same static source
set labels [dict get $spec -choicelabels]
lappend result [dict exists $labels dict]
lappend result [punk::args::id_exists "::tcl::string::is dict"]
}\
-cleanup {
}\
-result [list 1 1 1 1 1]
#added 2026-08-05 (agent, G-073) - USER-SANCTIONED PARITY EXEMPTION (the one
#deliberate divergence class): real Tcl 8.6 accepts 'string is di' as a unique
#prefix of digit; the model deliberately rejects it as ambiguous because the
#forward class dict reserves the 'di' prefix - preparing 8.6 users for 9.x where
#dict makes 'di' genuinely ambiguous. Recorded here as a sanctioned exemption
#rather than silently special-cased; full words and unshadowed prefixes stay
#parity-true on every runtime.
test tclcoreparity_stringis_prefix_strictness_exemption {sanctioned divergence: 'di' is model-rejected on runtimes lacking dict while real accepts; full words and unshadowed prefixes keep outcome parity}\
-constraints have_tclcoredocs\
-setup $common -body {
set classes [live_classes]
if {"dict" in $classes} {
#modern runtime: 'di' is genuinely ambiguous - parity, no exemption in play
lappend result [expr {[real_ok {di 1}] == 0 && [doc_ok {di 1}] == 0}]
} else {
#THE EXEMPTION: real 8.6 accepts 'di' (digit); the model rejects (ambiguous
#with the reserved forward class dict) - deliberate, user-sanctioned (G-073)
lappend result [expr {[real_ok {di 1}] == 1 && [doc_ok {di 1}] == 0}]
}
#full-word parity everywhere
lappend result [expr {[real_ok {dict x}] == [doc_ok {dict x}]}]
lappend result [expr {[real_ok {digit x}] == [doc_ok {digit x}]}]
#an unshadowed unique prefix stays parity-true everywhere
lappend result [expr {[real_ok {dig x}] == [doc_ok {dig x}]}]
}\
-cleanup {
}\
-result [list 1 1 1 1]
#--- ::after cancel-id discrimination (user-directed 2026-07-13; probe record in
#--- goals/G-055) - the cancelid/info forms' id is typed by the id shape harvested

33
src/tests/modules/punk/args/testsuites/args/usagemarking.test

@ -352,6 +352,39 @@ namespace eval ::testspace {
}\
-result [list 1 0 1 0]
#added 2026-08-05 (agent, G-073)
test usagemarking_goodchoice_unavailable_never_marked {a -choiceunavailable entry displays among the choices but is never goodchoice-highlighted, in table and string renderers}\
-setup {
set result ""
punk::args::define {
@id -id ::testspace::cufix
@cmd -name testspace::cufix -summary "cufix summary" -help "cufix help"
@leaders -min 1 -max 1
animal -choices {cat dog emu} -choiceunavailable {yeti}
@values -min 1 -max 1
volume -type int
}
}\
-body {
variable GOODCHOICE
set argd [punk::args::parse {cat 5} withid ::testspace::cufix]
set out [punk::args::arg_error "" [punk::args::get_spec ::testspace::cufix] -aserror 0 -scheme info -parsedargs $argd]
#the unavailable entry renders (in the dedicated Unavailable group)
lappend result [expr {[string first yeti [punk::ansi::ansistrip $out]] >= 0}]
#the received choice is highlighted; the unavailable entry never is (it can be
#neither a parsed value nor a -default - resolve-time validated)
lappend result [rendered_word_has_sgr_params $out cat $GOODCHOICE]
lappend result [rendered_word_has_sgr_params $out yeti $GOODCHOICE]
#string renderer parity
set out [punk::args::arg_error "" [punk::args::get_spec ::testspace::cufix] -aserror 0 -scheme info -return string -parsedargs $argd]
lappend result [rendered_word_has_sgr_params $out cat $GOODCHOICE]
lappend result [rendered_word_has_sgr_params $out yeti $GOODCHOICE]
}\
-cleanup {
punk::args::undefine ::testspace::cufix 1
}\
-result [list 1 1 0 1 0]
#--- scheme selection -------------------------------------------------------------------
test usagemarking_scheme_border {-scheme info renders the info border; default and explicit error do not}\

Loading…
Cancel
Save