Browse Source

G-084 achieved: settled cross-member -multiple rejection + value-parsekey integrity + storage-key attribution (punk::args 0.24.0, project 0.61.0)

G-084 increment 2 + achieved flip - parsekey completeness closed:

Cross-member -multiple on a shared-parsekey option group is now a DEFINE-TIME
error unless ALL members belong to one @opts group marked -parsekeymode error
(G-083 mutual exclusivity makes per-member collection well-defined; a single
-multiple member with its own parsekey is unrestricted). This settles the
G-084 cross-member accumulation decision as rejection rather than
accumulate-in-received-order: collection on one storage key is ambiguous
(collect-vs-replace undefined), and rejection eliminates the silent loss
without new collection semantics. The check runs AFTER the G-053
canonicalisation so range forms verdict correctly ({0 1} replace-shape
allowed; a pre-canonicalisation placement would raise a raw non-boolean
error for any range form).

Value -parsekey integrity settled at define time: duplicate value parsekeys
rejected (trailing values consume positionally - the shared slot silently
overwrote), dash-led value parsekeys rejected (they landed in the options
namespace of result/received and aborted parse with an internal error in
the option-shaped validation paths; every option storage key is dash-led,
so this one rule also removes all cross-class collisions).

parse_status/parse_report storage-key attribution: parse_status_build now
resolves a storage key claimed by exactly ONE argument (value -parsekey,
declared option -parsekey, '='-suffixed optset tail) back to that argument,
so argstatus/Source report received with the value-in-effect instead of
default/0. Shared-parsekey groups stay unresolved - the remaining recorded
display gap (engine-level per-member tracking; archived as the goal's open
Follow-on).

undefine/undefine_deflist now return "" (previously the whole internal
cache dict to any caller capturing the value). define -help documents
-parsekey for the first time (rename semantics for options and values,
leader rejection, shared groups and -parsekeymode, last-defined-member
defaults precedence, the -multiple rule, value-key uniqueness/non-dash).
Moduledoc tclcore comment corrected (leader -parsekey settled as rejection,
not pending). Internal sweep: dead values-loop identifiers removed
(val_ident/val_ident_is_parsekey/values-side seen_pks), leader reverse map
removed (VAL_NAMES-driven build), dead parsekey-from--default derivation
removed, stale pre-G-084 comments corrected.

Tests: parsekey_shared_key_multiple_rejected strengthened (message pins,
{0 1}/{0 4} range coverage, the -parsekeymode error escape hatch incl
collection and optionconflict); new parsekey_value_parsekey_integrity,
parsereport_storagekey_attribution, parsestatus_storagekey_attribution;
inc-1 comment damage repaired (duplicated block, eaten G-083 provenance
lines, stale GAP-era comment); 2026-08-08 future-dating normalized to
2026-08-07 in touched files (CHANGELOG 0.59.0/0.60.0 headers corrected
likewise; the underlying commits landed 2026-08-07 AEST).

Goal flip (user-approved): the Goal line's accumulate wording amended to
the settled define-time-rejection design; G-084 flipped achieved 2026-08-07
and archived (detail -> goals/archive/, index -> GOALS-archive.md,
reference sweep applied - G-167 survey note gains achieved markers for
G-053/G-083/G-084; Follow-on recorded: shared-group per-member received
attribution => open). Detail file gains ## Progress (inc 1 + inc 2 + flip
evidence). src/tests/modules/AGENTS.md punk/args index updated to the
settled-pin state.

Project 0.60.0 -> 0.61.0 + CHANGELOG (value -parsekey support and the
define-time integrity rules are user-visible shell behaviour; G-053
precedent). punk::args 0.23.0 -> 0.24.0.

Suites: punk/args 401/0; modules tree 1313 pass / 11 constraint-skipped /
0 fail (zig-built tclsh90s 9.0.5); testbody_lint clean; goals_lint clean;
punkboot version-parsing 81/81; make.tcl projectversion consistency OK.

Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
39e0ec2ba7
  1. 26
      CHANGELOG.md
  2. 4
      GOALS-archive.md
  3. 4
      GOALS.md
  4. 70
      goals/G-084-punkargs-parsekey-completeness.md
  5. 4
      goals/G-167-punkargs-version-delta-availability.md
  6. 120
      goals/archive/G-084-punkargs-parsekey-completeness.md
  7. 2
      punkproject.toml
  8. 281
      src/modules/punk/args-999999.0a1.0.tm
  9. 3
      src/modules/punk/args-buildversion.txt
  10. 5
      src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm
  11. 2
      src/tests/modules/AGENTS.md
  12. 101
      src/tests/modules/punk/args/testsuites/args/parsekey.test
  13. 43
      src/tests/modules/punk/args/testsuites/args/parsereport.test
  14. 37
      src/tests/modules/punk/args/testsuites/args/parsestatus.test
  15. 51
      src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test

26
CHANGELOG.md

@ -5,7 +5,29 @@ 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.60.0] - 2026-08-08
## [0.61.0] - 2026-08-07
- `punk::args` `-parsekey` completeness (G-084): a VALUE argument's
`-parsekey` now renames its result `values` slot and its `received` key
(previously accepted at define time but aborted parse), with `-multiple`
values collecting under the parsekey; a leader `-parsekey` is rejected at
define time (was silently ignored). Cross-member `-multiple` on a
shared-parsekey option group is a define-time error unless ALL members
share one `@opts -parsekeymode error` group whose mutual exclusivity makes
per-member collection well-defined - settling the cross-member accumulation
question as rejection rather than accumulate; a `{0 1}` range member
(replace-shape) stays allowed. Value parsekeys must be unique per form and
must not be dash-led (previously silent positional overwrite / internal
parse abort). The shared-group defaulted-members precedence rule is now
documented behaviour: the last-defined member's default wins.
`parse_status`/`parse_report` attribute received/value through unique
storage keys (value parsekeys, renamed options, `=`-suffixed optset tails);
shared-parsekey groups keep the recorded per-member attribution gap.
`undefine`/`undefine_deflist` return an empty string instead of a whole
internal cache dict. `define -help` now documents `-parsekey`.
(punk::args 0.23.0/0.24.0)
## [0.60.0] - 2026-08-07
- `punk::args` `-multiple` gains occurrence-arity ranges (G-053): `-multiple`
accepts a `{min max}` range (max `-1` unbounded) alongside the legacy
@ -25,7 +47,7 @@ Entries are newest-first; one bullet per notable change. See the root `AGENTS.md
distinguishes at-most-once (`?arg?`) from repeating (`arg...`).
`-multipleunique`/`-multipleuniqueset` compose unchanged.
## [0.59.0] - 2026-08-08
## [0.59.0] - 2026-08-07
- `punk::args` gains argument-relation vocabulary and parse-time
enforcement (G-083): per-argument `-conflicts <list>` declares that

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-084 [achieved 2026-08-07] punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence → detail: goals/archive/G-084-punkargs-parsekey-completeness.md
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Acceptance: the three punkargsKnownBug tests in testsuites/dev/parsekey-knownbugs.test are enabled and pass (or are amended to the settled design and pass); the corresponding _GAP pins in parsekey.test are flipped or retired; the '#todo - fix -parsekey for leaders and values' moduledoc comment is resolved; the full existing punk::args suite passes untouched.
### G-053 [achieved 2026-08-08] punk::args range-valued -multiple: occurrence arity with strict duplicate handling → detail: goals/archive/G-053-punkargs-multiple-ranges.md
Scope: src/modules/punk/args-999999.0a1.0.tm (spec compiler, parse, arg_error/synopsis renderers); src/tests/modules/punk/args/testsuites/args/
Acceptance: parse raises a usage-style arity error naming the argument for occurrences outside a declared range; boolean -multiple 0/1 behaviour is unchanged (full existing punk::args suite passes untouched); the -optional/range-min reconciliation rule is documented and enforced at define time; the usage table Multi column and synopsis reflect declared ranges; -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged; characterization tests cover the new forms and the value-shape rule.

4
GOALS.md

@ -285,10 +285,6 @@ Scope: src/modules/punk/mix/commandset/doc-999999.0a1.0.tm (doc.* commandset - n
Detail: goals/G-081-argdoc-build-pipeline.md
### G-084 [active] punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Detail: goals/G-084-punkargs-parsekey-completeness.md
### G-085 [proposed] punk::ns::cmdtrace: source-file vs live-proc line attribution option, and machine-parseable trace output
Scope: src/modules/punk/ns-999999.0a1.0.tm (cmdtrace, _cmdtrace_* trace callbacks, argdoc); src/tests/modules/punk/ns/testsuites/ns/cmdtrace.test
Detail: goals/G-085-cmdtrace-attribution-and-machine-output.md

70
goals/G-084-punkargs-parsekey-completeness.md

@ -1,70 +0,0 @@
# G-084 punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence
Status: active
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Goal: -parsekey behaves as a complete, position-independent result-keying mechanism: distinct -multiple members sharing a parsekey accumulate under the shared key in received order (no silent value loss), -parsekey on leaders and values renames their result/received keys the same way it does for options (or is rejected at define time everywhere it is unsupported - no silent ignore, no parse-time abort), and the precedence rule for multiple defaulted members of one group is documented behaviour rather than an in-code '? review'.
Acceptance: the three punkargsKnownBug tests in testsuites/dev/parsekey-knownbugs.test are enabled and pass (or are amended to the settled design and pass); the corresponding _GAP pins in parsekey.test are flipped or retired; the '#todo - fix -parsekey for leaders and values' moduledoc comment is resolved; the full existing punk::args suite passes untouched.
## Context
Probe findings from the 2026-07-14 parsekey coverage review, each pinned by a
_GAP test in src/tests/modules/punk/args/testsuites/args/parsekey.test:
- Cross-member -multiple value loss: with `-add` and `-sub` both `-multiple 1`
sharing parsekey `-op`, parsing `-add a -sub b -add c` returns `-op b` - the
collected `-add` occurrences `{a c}` vanish. Occurrences are collected per
member flag and the shared-key resolution then returns only one member's
list (parsekey_shared_key_multiple_collection_GAP).
- `-parsekey` on a value is accepted at define time but ABORTS parsing with an
internal error; on a leader it is silently ignored; the `@values` directive
line rejects it as an unrecognised key (parsekey_values_leaders_GAP). The
tclcore moduledoc carries `#todo - fix -parsekey for leaders and values`
next to a definition using it, and choices.test notes a leaders/values
parsekey mechanism would help multi-element clause choice grouping.
- When more than one member of a shared-parsekey group declares `-default`,
the last-defined member's default wins - the ordered_opts default handling
in the source carries a literal `? review`
(parsekey_shared_key_member_defaults_GAP).
## Approach
- Accumulate-in-received-order is the lean for cross-member -multiple
collection, pinned (disabled) in dev/parsekey-knownbugs.test as
parsekey_shared_key_multiple_accumulation. Open design decision: with
G-083's relations vocabulary now landed (achieved - see
goals/archive/G-083-punkargs-argument-relations.md), "error on cross-member
combination unless permitted" is a defensible alternative - decide before
activating.
- Leaders/values support should reuse the option path's keying rules
(result + received dicts, required-list satisfaction) rather than a parallel
mechanism; where support is deliberately not implemented, define time must
reject the key with a clear message instead of deferring failure to parse.
- The defaulted-members precedence rule (whatever is settled) gets stated in
the -parsekey documentation and the `? review` comment removed.
## Notes
- Desired-behaviour pins (disabled behind punkargsKnownBug):
src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test - enable
and flip the parsekey.test _GAP pins as fixes land.
- Related: G-083 (argument relations - shared design context for the
cross-member collection decision; achieved - see
goals/archive/G-083-punkargs-argument-relations.md), G-053 (occurrence arity
of a single argument; achieved - see
goals/archive/G-053-punkargs-multiple-ranges.md).
- Related: G-151 (achieved 2026-08-05 - goals/archive/G-151-punkargs-annotated-success-render.md) -
its parse_report landing report bridges the parse-result storage-key folds
AT DISPLAY LEVEL only (an argument's -parsekey, else an aliased optionset's
last |-member, tried when the direct ARG_INFO-name value-in-effect lookup
misses; pinned by parsereport.test parsereport_aliased_optionset_row_bridged).
A G-151 follow-on assigns the engine-level rationalization of storage keys
vs definition argument names to this goal - when that lands, the display
bridge in parse_report (and the matching miss noted in
private::parse_status_build's comment) can retire.
- Concrete display gap for the above: parse_report's Source column and
parse_status's per-arg `received` count both report `default`/0 for an
explicitly-supplied shared-parsekey member (e.g `parse_report {-exact ...}
withid ::lsearch` shows every MATCHSTYLE member as `(default)` though the
value is correctly bridged) - `parse_status_build` keys the positions dict by
the storage key, so no per-member received attribution survives the fold;
flip when the engine-level storage-key model lands (the same follow-on).

4
goals/G-167-punkargs-version-delta-availability.md

@ -132,4 +132,6 @@ deltas use -choiceunavailable; forms are reserved for true shape deltas.
reuse its status-aware return conventions if it lands first). G-151 since
achieved 2026-08-05 (goals/archive/G-151-punkargs-annotated-success-render.md) -
its parse_report landing table is another argument-row surface this goal's
unavailable marking would eventually render over.
unavailable marking would eventually render over. G-053 and G-083 (achieved
2026-08-08) and G-084 (achieved 2026-08-07) since archived likewise - see
goals/archive/.

120
goals/archive/G-084-punkargs-parsekey-completeness.md

@ -0,0 +1,120 @@
# G-084 punk::args -parsekey completeness: cross-member -multiple collection, leaders/values support, defined default precedence
Status: achieved 2026-08-07
Scope: src/modules/punk/args-999999.0a1.0.tm (parse paths, resolve directive handling for @leaders/@values); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (sites carrying '#todo - fix -parsekey for leaders and values'); src/tests/modules/punk/args/testsuites/args/parsekey.test + testsuites/dev/parsekey-knownbugs.test (GAP/known-bug flips)
Goal: -parsekey behaves as a complete, position-independent result-keying mechanism: distinct -multiple members sharing a parsekey are rejected at define time unless the whole group is mutually exclusive (-parsekeymode error), under which -multiple per member is well-defined (no silent value loss), -parsekey on leaders and values renames their result/received keys the same way it does for options (or is rejected at define time everywhere it is unsupported - no silent ignore, no parse-time abort), and the precedence rule for multiple defaulted members of one group is documented behaviour rather than an in-code '? review'.
Acceptance: the three punkargsKnownBug tests in testsuites/dev/parsekey-knownbugs.test are enabled and pass (or are amended to the settled design and pass); the corresponding _GAP pins in parsekey.test are flipped or retired; the '#todo - fix -parsekey for leaders and values' moduledoc comment is resolved; the full existing punk::args suite passes untouched.
## Context
Probe findings from the 2026-07-14 parsekey coverage review, each pinned by a
_GAP test in src/tests/modules/punk/args/testsuites/args/parsekey.test:
- Cross-member -multiple value loss: with `-add` and `-sub` both `-multiple 1`
sharing parsekey `-op`, parsing `-add a -sub b -add c` returns `-op b` - the
collected `-add` occurrences `{a c}` vanish. Occurrences are collected per
member flag and the shared-key resolution then returns only one member's
list (parsekey_shared_key_multiple_collection_GAP).
- `-parsekey` on a value is accepted at define time but ABORTS parsing with an
internal error; on a leader it is silently ignored; the `@values` directive
line rejects it as an unrecognised key (parsekey_values_leaders_GAP). The
tclcore moduledoc carries `#todo - fix -parsekey for leaders and values`
next to a definition using it, and choices.test notes a leaders/values
parsekey mechanism would help multi-element clause choice grouping.
- When more than one member of a shared-parsekey group declares `-default`,
the last-defined member's default wins - the ordered_opts default handling
in the source carries a literal `? review`
(parsekey_shared_key_member_defaults_GAP).
## Approach
- Cross-member -multiple collection SETTLED 2026-08-07 as define-time
rejection (the G-083-informed alternative): accumulate-in-received-order
was the original lean, but cross-member collection on one storage key is
ambiguous (collect-vs-replace undefined; one member's collected list would
silently displace another's) and rejection eliminates the silent loss
without inventing new collection semantics. -multiple per member remains
available when ALL members share one @opts group marked -parsekeymode
error (G-083 mutual exclusivity makes it well-defined); a single -multiple
member with its own parsekey is unrestricted. The check runs after the
G-053 canonicalisation so range forms verdict correctly ({0 1} is
replace-shape and allowed).
- Leaders/values support should reuse the option path's keying rules
(result + received dicts, required-list satisfaction) rather than a parallel
mechanism; where support is deliberately not implemented, define time must
reject the key with a clear message instead of deferring failure to parse.
- The defaulted-members precedence rule (whatever is settled) gets stated in
the -parsekey documentation and the `? review` comment removed.
## Notes
- Desired-behaviour pins: settled 2026-08-07 - testsuites/dev/parsekey-knownbugs.test
holds no disabled pins (retained as the home for future punkargsKnownBug
pins); the settled assertions live in testsuites/args/parsekey.test
(parsekey_value_result_key_settled, parsekey_values_line_rejects_parsekey,
parsekey_leader_parsekey_rejected, parsekey_shared_key_multiple_rejected,
parsekey_value_parsekey_integrity).
- Related: G-083 (argument relations - shared design context for the
cross-member collection decision; achieved - see
goals/archive/G-083-punkargs-argument-relations.md), G-053 (occurrence arity
of a single argument; achieved - see
goals/archive/G-053-punkargs-multiple-ranges.md).
- Related: G-151 (achieved 2026-08-05 - goals/archive/G-151-punkargs-annotated-success-render.md) -
its parse_report landing report bridges the parse-result storage-key folds
AT DISPLAY LEVEL (pinned by parsereport.test
parsereport_aliased_optionset_row_bridged). NARROWED 2026-08-07 (inc 2,
punk::args 0.24.0): private::parse_status_build now resolves UNIQUE storage
keys itself (a value's -parsekey, an option's declared -parsekey, an
optset's '='-suffixed trimmed tail) for received attribution AND
value-in-effect, so the parse_report bridge and the build's miss now apply
only to SHARED-parsekey groups.
- Remaining display gap (shared groups only, as of inc 2): parse_report's
Source column and parse_status's per-arg `received` count still report
`default`/0 for an explicitly-supplied SHARED-parsekey member (e.g
`parse_report {-exact ...} withid ::lsearch` shows every MATCHSTYLE member
as `(default)` though the group value is bridged onto each member row) -
per-member received attribution does not survive the storage fold, and
which member was received is genuinely unrecoverable from the parse result
dict alone. Retirement needs engine-level per-member tracking in the parse
result (get_dict's optsets_received exists internally but is not exposed).
Pinned as the remaining gap by parsereport.test
parsereport_storagekey_attribution (the shared-group default/default row).
## Progress
- 2026-08-07 (inc 1, commit abd8616f, punk::args 0.23.0): value -parsekey
supported (result values slot and received keyed by parsekey, -multiple
collection, VAL_DEFAULTS/values_dict parsekey keying, api_valnames_received
tracking, val_pk2name reverse map); leader -parsekey rejected at define
time; defaulted-members precedence documented as last-defined-member-wins
and the in-code `? review` removed; tclcore moduledoc todo resolved;
parsekey.test value/leader/@values-line/defaults GAPs flipped to settled
pins; knownbugs value/leader disabled pins retired.
- 2026-08-07 (inc 2, punk::args 0.24.0): cross-member -multiple decision
SETTLED as define-time rejection (allowed only when ALL members share one
@opts group marked -parsekeymode error; check placed after the G-053
canonicalisation so {0 1} range replace-shapes stay allowed - an earlier
pre-canonicalisation placement raised a raw non-boolean error for range
forms); value-parsekey integrity settled (duplicate value parsekeys and
dash-led value parsekeys rejected at define time - previously silent
positional overwrite / internal parse abort); parse_status/parse_report
unique-storage-key received+value attribution (shared groups remain the
recorded display gap); undefine/undefine_deflist return ""; define -help
-parsekey documentation added (first user-facing -parsekey doc, includes
the defaults-precedence rule per this goal's Approach); dead-code and
stale-comment sweep (val_ident/values-side seen_pks, leader reverse map,
pre-G-084 comment claims). Suites: punk/args 401/0, testbody_lint clean.
- 2026-08-07 (flip): acceptance verified met - knownbug pins settled and
passing, _GAP pins flipped/retired, moduledoc comment resolved, full suite
green (punk/args 401/0; modules tree 1313 pass / 11 constraint-skipped /
0 fail on the zig-built tclsh90s 9.0.5 runner; testbody_lint and
goals_lint clean). The Goal line's accumulate-in-received-order wording
was amended to the settled define-time-rejection design with the
-parsekeymode error escape at the user's explicit approval, and the goal
flipped achieved and archived. The shared-parsekey-group
received-attribution display gap is recorded below as a Follow-on
(=> open).
## Follow-ons
Follow-on: engine-level per-member received attribution for shared-parsekey groups - expose per-member tracking (e.g get_dict's internal optsets_received) in the parse result so parse_status/parse_report Source columns can attribute WHICH member of a shared-parsekey group was received; retires the parse_report display bridge and parse_status_build's shared-key miss (pinned today by parsereport.test parsereport_storagekey_attribution's shared-group default/default row) => open

2
punkproject.toml

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

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

@ -1044,6 +1044,38 @@ tcl::namespace::eval punk::args {
If true, when multiple values are stored against the same argument
name due to -multiple being true, the values must be unique.
If false, the same value can be stored multiple times.
-parsekey <key> (options & values; NOT supported on leaders -
rejected at define time)
Renames the argument's STORAGE key: the key under which the
parsed value appears in the result's opts/values dicts and
in the received dict. The definition argument name is
unchanged for display and ARG_INFO lookups. Without
-parsekey an option's storage key is the last |-member of
its name with any trailing '=' trimmed, and a value's is
the value's own name.
Distinct OPTIONS may deliberately share one -parsekey (a
shared-key group - e.g modelling lsearch's matching-style
flags): in the default mode the LAST received member wins
the shared slot; declare the group with
'@opts -group <g> -parsekey <key> -parsekeymode error' to
make distinct members mutually exclusive at parse instead.
When several members of one shared-parsekey group declare
-default and none is received, the LAST-defined member's
-default wins (deterministic in definition order).
-multiple on a member of a shared-parsekey group is a
define-time error unless ALL members belong to one @opts
group marked -parsekeymode error (mutual exclusivity makes
per-member collection well-defined); a single -multiple
member with its own parsekey is unrestricted.
A -parsekey matching the NAME of another defined argument
is a define-time error unless that argument declares the
same -parsekey (an explicit shared group).
VALUE parsekeys must be unique within a form and must not
begin with '-' (dash-led keys belong to options in parse
results); both are define-time errors. Values cannot form
shared-key groups - trailing values consume positionally,
so distinct values sharing one result slot would silently
overwrite each other.
-choices {<choicelist>}
A list of allowable values for an argument.
The -default value doesn't have to be in the list.
@ -1488,6 +1520,11 @@ tcl::namespace::eval punk::args {
puts stderr "punk::args::undefine unable to find id: '$id'"
}
}
#G-084: explicit empty return - without this the proc returns the last
#expression (a 'dict unset rawdef_cache_about ...' which yields the WHOLE
#rawdef_cache_about dict, ~hundreds of KB of cached about-renders), so any
#caller capturing undefine's return value got a giant dump instead of "".
return ""
}
#'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated
# In this case we don't see the autoid in order to delete it
@ -1519,6 +1556,9 @@ tcl::namespace::eval punk::args {
dict unset argdefcache_display $deflist
dict unset id_cache_rawdef $id
}
#G-084: explicit empty return - same fix as undefine (without this the proc
#returns the last 'dict unset id_cache_rawdef ...' result, the whole id_cache_rawdef dict).
return ""
}
@ -3725,11 +3765,10 @@ tcl::namespace::eval punk::args {
if {[tcl::dict::exists $spec_merged -default]} {
if {$is_opt} {
#JJJ
set parsekey [dict get $FDICT ARG_INFO $argname -default]
if {$parsekey eq ""} {
set parsekey $argname
}
#G-084: OPT_DEFAULTS stays keyed by the optset argname - the
#defaults application loop in get_dict resolves the storage
#key (parsekey else trimmed tail) per optset at parse time.
#(A dead parsekey-from--default derivation was removed here.)
dict set upd_OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default]
#tcl::dict::set F $fid OPT_DEFAULTS $argname [tcl::dict::get $spec_merged -default]
} else {
@ -3962,6 +4001,82 @@ tcl::namespace::eval punk::args {
}
}
#G-084 define-time -parsekey integrity (deliberately AFTER the G-053
#canonicalisation above - the -multiple truth-tests below need the
#canonical boolean; a raw {min max} range list is not a boolean).
#(d) value -parsekey rules. A dash-led value parsekey lands in the
#options namespace of the result/received dicts and gets mis-routed by
#the option-shaped validation paths (parse aborted with an internal
#error). Distinct values sharing one parsekey can never be alternatives -
#trailing values consume positionally, so both always consume and the
#shared result slot silently overwrites (the silent-loss class this goal
#eliminates). Reject both at define time.
set _g84_val_pks [dict create]
foreach _g84_vn [dict get $FDICT VAL_NAMES] {
set _g84_p [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g84_vn] -parsekey ""]
if {$_g84_p eq ""} continue
if {[string match -* $_g84_p]} {
error "punk::args::resolve - -parsekey '$_g84_p' for value argument '$_g84_vn' must not begin with '-' (dash-led keys belong to options in parse results and the received dict). Use a non-dash parsekey, or model the argument as an option. @id:$DEF_definition_id"
}
if {[dict exists $_g84_val_pks $_g84_p]} {
error "punk::args::resolve - -parsekey '$_g84_p' is declared by multiple value arguments ('[dict get $_g84_val_pks $_g84_p]' and '$_g84_vn'). Trailing values consume positionally, so distinct values sharing one result key silently overwrite each other. Give each value its own -parsekey. @id:$DEF_definition_id"
}
dict set _g84_val_pks $_g84_p $_g84_vn
}
unset -nocomplain _g84_val_pks _g84_vn _g84_p
#(e) cross-member -multiple on a shared-parsekey OPTION group is
#ambiguous in the default override mode (one member's collected list
#would silently displace another's, and collect-vs-replace on one key is
#undefined). Reject at define time. A single -multiple member with its
#own parsekey is fine. -multiple per member is well-defined only when at
#most one member can ever be received - i.e. ALL members belong to the
#SAME @opts group marked -parsekeymode error (parse-time exclusivity is
#enforced per group, so a member outside that group - or in a different
#group - escapes it). The stored -multiple is the G-053 canonical
#boolean here: a {0 1} range is replace-shape and allowed; collect
#shapes ({2 4}, {1 -1}, boolean 1) are rejected. (No live caller
#declares -multiple on a shared-parsekey group - lsearch's groups are
#all single-valued.)
set _g84_pk_to_optsets [dict create]
foreach _g84_optset [dict get $FDICT OPT_NAMES] {
set _g84_p [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g84_optset] -parsekey ""]
if {$_g84_p eq ""} {
set _g84_grp [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g84_optset] -group ""]
if {$_g84_grp ne "" && [dict exists $FDICT OPT_GROUPS $_g84_grp]} {
set _g84_p [punk::args::system::Dict_getdef [dict get $FDICT OPT_GROUPS $_g84_grp] -parsekey ""]
}
}
if {$_g84_p ne ""} {tcl::dict::lappend _g84_pk_to_optsets $_g84_p $_g84_optset}
}
dict for {_g84_pk _g84_members} $_g84_pk_to_optsets {
if {[llength $_g84_members] < 2} continue
set _g84_excl 1
set _g84_g0 ""
foreach _g84_m $_g84_members {
set _g84_grp [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g84_m] -group ""]
if {$_g84_grp eq "" || ![dict exists $FDICT OPT_GROUPS $_g84_grp]
|| [punk::args::system::Dict_getdef [dict get $FDICT OPT_GROUPS $_g84_grp] -parsekeymode override] ne "error"} {
set _g84_excl 0
break
}
if {$_g84_g0 eq ""} {
set _g84_g0 $_g84_grp
} elseif {$_g84_grp ne $_g84_g0} {
set _g84_excl 0
break
}
}
if {!$_g84_excl} {
foreach _g84_m $_g84_members {
if {[tcl::dict::get $FDICT ARG_INFO $_g84_m -multiple]} {
error "punk::args::resolve - -multiple is not supported on member '$_g84_m' of shared-parsekey group '$_g84_pk' in the default override mode: cross-member collection is ambiguous (one member's collected list would silently displace another's). Use a single -multiple member per parsekey, or put all members in one @opts group marked -parsekeymode error so they are mutually exclusive and -multiple per member is well-defined. @id:$DEF_definition_id"
}
}
}
}
unset -nocomplain _g84_pk _g84_members _g84_m _g84_grp _g84_p _g84_optset _g84_pk_to_optsets _g84_excl _g84_g0
#todo - document that ambiguities in API are likely if both @leaders and @values used
#todo - do some checks for obvious bad definitions involving a mix of @leaders and @values (e.g with optional options)
@ -7387,6 +7502,38 @@ tcl::namespace::eval punk::args {
dict set lookup_optset $o $optionset
}
}
#G-084: storage-key maps. The parse result keys received/opts/values by each
#argument's STORAGE key (declared -parsekey; else an optset's last |-member
#with any trailing '=' trimmed; else a value's own name), which the
#member-spelling lookup above cannot resolve for declared -parsekey renames,
#'='-suffixed optset tails, or value parsekeys. Where a storage key is
#claimed by exactly one argument the mapping is unambiguous - normalize
#through it. A key claimed by several arguments (a shared-parsekey group)
#stays unresolved: per-member received attribution does not survive the
#storage fold (the recorded G-084 display gap - needs engine-level
#per-member tracking in the parse result).
set _sk_claims [dict create]
foreach optionset [dict get $form_dict OPT_NAMES] {
set _sk [Dict_getdef [dict get $form_dict ARG_INFO $optionset] -parsekey ""]
if {$_sk eq ""} {
set _sk [string trimright [lindex [split $optionset |] end] =]
}
dict lappend _sk_claims $_sk $optionset
}
foreach vname [dict get $form_dict VAL_NAMES] {
set _sk [Dict_getdef [dict get $form_dict ARG_INFO $vname] -parsekey ""]
if {$_sk eq ""} {set _sk $vname}
dict lappend _sk_claims $_sk $vname
}
set lookup_storagekey [dict create] ;#storage key -> argname (unique claims only)
set arg_storagekey [dict create] ;#argname -> storage key (unique claims only)
dict for {_sk _sk_claimants} $_sk_claims {
if {[llength $_sk_claimants] == 1} {
dict set lookup_storagekey $_sk [lindex $_sk_claimants 0]
dict set arg_storagekey [lindex $_sk_claimants 0] $_sk
}
}
unset -nocomplain _sk _sk_claims _sk_claimants
#received argument names normalized to definition argument names.
#received is dict-like but may repeat keys for -multiple arguments; values are clause ordinals.
set receivednames [list]
@ -7394,6 +7541,8 @@ tcl::namespace::eval punk::args {
foreach {r rpos} [Dict_getdef $parsedargs received {}] {
if {[string match -* $r] && [dict exists $lookup_optset $r]} {
set r [dict get $lookup_optset $r]
} elseif {[dict exists $lookup_storagekey $r]} {
set r [dict get $lookup_storagekey $r]
}
if {$r ni $receivednames} {
lappend receivednames $r
@ -7437,11 +7586,19 @@ tcl::namespace::eval punk::args {
set argfailclass ""
}
#value-in-effect (includes values the parse filled from -default).
#Direct lookup by definition argument name, matching the display renderers:
#aliased optionsets store under their canonical name and are not resolved here.
#Direct lookup by definition argument name first; a miss then tries
#the argument's unique storage key (G-084: declared -parsekey rename,
#or an aliased optset's trimmed last |-member fold). A shared
#storage key (shared-parsekey group) has no unique owner and stays
#unresolved here - parse_report's display bridge shows the group
#value on each member row (the recorded G-084 display gap).
if {[dict exists $parsedvalues $arg]} {
set hasvalue 1
set value [dict get $parsedvalues $arg]
} elseif {[dict exists $arg_storagekey $arg]
&& [dict exists $parsedvalues [dict get $arg_storagekey $arg]]} {
set hasvalue 1
set value [dict get $parsedvalues [dict get $arg_storagekey $arg]]
} else {
set hasvalue 0
set value ""
@ -8546,10 +8703,13 @@ tcl::namespace::eval punk::args {
set hasvalue [tcl::dict::get $AST hasvalue]
set value [tcl::dict::get $AST value]
if {!$hasvalue} {
#bridge the storage-key folds the build's direct ARG_INFO-name lookup
#misses: -parsekey renames the storage key; aliased optionsets store
#under their last |-member (the finalopts fold in get_dict). G-084 owns
#the storage-key model - this is display-level bridging only.
#bridge the storage-key folds parse_status_build could not resolve.
#Since G-084 the build resolves UNIQUE storage keys itself (-parsekey
#renames, aliased optsets' trimmed last |-member), so this fires only
#for SHARED-parsekey group members: each member row shows the group's
#value-in-effect while received attribution stays with the raw shared
#key (the recorded G-084 display gap - engine-level per-member
#tracking in the parse result would retire this).
switch -- $class {
leader {set sectionkey leaders}
option {set sectionkey opts}
@ -10716,27 +10876,20 @@ tcl::namespace::eval punk::args {
set FORMDISPLAY [dict get $formdict FORMDISPLAY]
#G-084: parsekey->argname reverse maps. VAL_REQUIRED/LEADER_REQUIRED and the
#VAL_DEFAULTS/LEADER_DEFAULTS dicts are keyed by parsekey (so they match the
#parsekey-keyed result/received dicts), but ARG_INFO is keyed by argname - so
#any ARG_INFO lookup by a required/default key (e.g the valmin clause-length
#fallback below) must resolve the parsekey back to an argname. Built once per
#form from ARG_INFO; for an arg with no -parsekey the parsekey IS the argname.
#G-084: value parsekey->argname reverse map. VAL_REQUIRED and VAL_DEFAULTS
#are keyed by parsekey (so they match the parsekey-keyed result/received
#dicts), but ARG_INFO is keyed by argname - so any ARG_INFO lookup by a
#required/default key (e.g the valmin clause-length fallback below) must
#resolve the parsekey back to an argname. Iterates only VAL_NAMES (usually
#few) to keep the per-parse cost down; for a value with no -parsekey the
#parsekey IS the argname (self-entry). No leader map is needed - a leader
#-parsekey is rejected at define time, so leader keys are always argnames.
#Duplicate value parsekeys are rejected at resolve, so entries are unique.
set val_pk2name [dict create]
foreach {vn vspec} $ARG_INFO {
if {$vn in $VAL_NAMES} {
set pk [Dict_getdef $vspec -parsekey ""]
if {$pk eq ""} {set pk $vn}
if {![dict exists $val_pk2name $pk]} {dict set val_pk2name $pk $vn}
}
}
set leader_pk2name [dict create]
foreach {vn vspec} $ARG_INFO {
if {$vn in $LEADER_NAMES} {
set pk [Dict_getdef $vspec -parsekey ""]
if {$pk eq ""} {set pk $vn}
if {![dict exists $leader_pk2name $pk]} {dict set leader_pk2name $pk $vn}
}
foreach vn $VAL_NAMES {
set pk [Dict_getdef [dict get $ARG_INFO $vn] -parsekey ""]
if {$pk eq ""} {set pk $vn}
if {![dict exists $val_pk2name $pk]} {dict set val_pk2name $pk $vn}
}
#G-082 single-form error selection: words the allocation screens rejected for an
@ -11906,15 +12059,18 @@ tcl::namespace::eval punk::args {
#-unnamed fallback, and the G-082 alloc_rejections overflow error selection.
#Known asymmetries: interplay with the upstream leader/value split,
#LEADER_TAKEWHENARGSMODULO, missingrequiredleader vs missingrequiredvalue, and the
#values loop's api_valname (-parsekey) identifiers which this loop lacks (G-084).
#Refactor assessment (2026-07-14): folding both loops into one clause-assignment
#helper is indicated - drift is real (G-082 had to duplicate its selection block;
#parsekey idents exist only on the values side) - but it should ride the G-084
#leaders/values parsekey parity work, which must touch both loops anyway, and must
#respect the parse hot-path constraints established by G-046: naive proc extraction
#means heavy state plumbing (upvar/dict) for many loop-local variables. Until then,
#any edit to one loop must be mirrored in the other or the divergence justified in
#a comment at the point of divergence.
#values loop's api_valname (-parsekey) storage keying which this loop lacks - a
#PERMANENT asymmetry as of G-084: a leader -parsekey is rejected at define time,
#so no leaders-side parsekey keying is coming.
#Refactor assessment (2026-07-14, premise updated by G-084): folding both loops
#into one clause-assignment helper remains indicated on its own merits - drift is
#real (G-082 had to duplicate its selection block) - but no longer has a G-084
#vehicle (the planned leaders/values parsekey parity work resolved as define-time
#rejection instead). Any such refactor must respect the parse hot-path constraints
#established by G-046: naive proc extraction means heavy state plumbing
#(upvar/dict) for many loop-local variables. Until then, any edit to one loop must
#be mirrored in the other or the divergence justified in a comment at the point of
#divergence.
#-------------------------------------------------------------------------------
for {set ldridx 0} {$ldridx < [llength $leaders]} {incr ldridx} {
set leadername [lindex $LEADER_NAMES $nameidx]
@ -12133,7 +12289,7 @@ tcl::namespace::eval punk::args {
set values_dict [dict create]
foreach valname [lrange $VAL_NAMES 0 $num_values-1] {
#set ALL valnames to lock in positioning. G-084: key by parsekey so received
#values (stored under val_ident=parsekey) land on the same slot, and the
#values (stored under api_valname=parsekey) land on the same slot, and the
#VAL_DEFAULTS merge (also parsekey-keyed) overlays the default here.
set _vkey $valname
set _vpk [Dict_getdef [dict get $ARG_INFO $valname] -parsekey ""]
@ -12145,19 +12301,20 @@ tcl::namespace::eval punk::args {
#------------------------------------------
set nameidx 0
set start_position $positionalidx
set seen_pks [list]
#2026-07-14 Agent-Updated (was: "MAINTENANCE - (*nearly*?) same loop logic as for leaders")
#MAINTENANCE: near-clone of the leaders loop above - see the refactor assessment
#comment there (shared logic, known asymmetries, defer-to-G-084 decision).
#comment there (shared logic, known asymmetries).
#Any edit here must be mirrored in the leaders loop or justified at the divergence.
for {set validx 0} {$validx < [llength $values]} {incr validx} {
set valname [lindex $VAL_NAMES $nameidx]
set val [lindex $values $validx]
#----------------------------------
#2026-07-14 Agent-Updated (was a bare "#todo"): values/leaders -parsekey
#completeness is goal G-084 - this api_valname derivation is the values-side
#start of it (currently a value's -parsekey aborts parse further on, and the
#leaders loop has no equivalent).
#G-084: api_valname is the value's STORAGE key - its -parsekey when
#declared (unique per form, non-dash - both resolve-enforced), else its
#own name. values_dict storage, VAL_DEFAULTS/VAL_REQUIRED matching and
#the received dict all key by it; valnames_received stays argname-keyed
#for the -multiple first/continuation gate and G-053 occurrence
#enforcement. (Leaders deliberately have no equivalent - a leader
#-parsekey is rejected at define time.)
set api_valname ""
if {[tcl::dict::exists $argstate $valname -parsekey]} {
set api_valname [tcl::dict::get $argstate $valname -parsekey]
@ -12166,23 +12323,6 @@ tcl::namespace::eval punk::args {
#parsekey is the same as valname
set api_valname $valname
}
if {$api_valname eq $valname} {
#if parsekey is the same as valname, we can just use valname as the identifier for opts and values
set val_ident $valname
set val_ident_is_parsekey 0
} else {
#initially key our values on a long form allowing us to know which specific value position was used (for when multiple map to same parsekey)
#e.g -increasing|-SORTOPTION
set val_ident $valname|$api_valname
set val_ident_is_parsekey 1
}
lappend seen_pks $api_valname
#----------------------------------
#2026-07-14 Agent-Updated: a pasted reference copy of the opts-path
#flag_ident/-parsekey logic was removed here (the val_ident derivation
#above is its values-loop analogue). Values/leaders -parsekey support is
#incomplete - see goal G-084 (parse currently errors for a value's
#-parsekey and ignores a leader's).
#----------------------------------
if {$valname ne ""} {
set valtypelist [tcl::dict::get $argstate $valname -type]
@ -12698,14 +12838,13 @@ tcl::namespace::eval punk::args {
} else {
#leader or value.
set argname $argname_or_ident
#G-084: values_dict/leaders_dict are keyed by parsekey, but arg_checks/argstate
#are argname-keyed - resolve the parsekey back to its argname. (For a value
#or leader without -parsekey the parsekey IS the argname, so the maps hold a
#self-entry and this is a no-op.)
#G-084: values_dict is keyed by parsekey, but arg_checks/argstate are
#argname-keyed - resolve the parsekey back to its argname. (For a value
#without -parsekey the parsekey IS the argname, so the map holds a
#self-entry and this is a no-op. Leaders are always argname-keyed - a
#leader -parsekey is rejected at define time.)
if {[dict exists $val_pk2name $argname]} {
set argname [dict get $val_pk2name $argname]
} elseif {[dict exists $leader_pk2name $argname]} {
set argname [dict get $leader_pk2name $argname]
}
#set pkoverride [Dict_getdef $argstate $argname -parsekey ""]
#TODO?

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

@ -1,6 +1,7 @@
0.23.0
0.24.0
#First line must be a semantic version number
#all other lines are ignored.
#0.24.0 - G-084 increment 2 (parsekey completeness, part 2 - the settled cross-member design + integrity closes): -multiple on a member of a shared-parsekey OPTION group is now a DEFINE-TIME error unless ALL members belong to one @opts group marked -parsekeymode error (whose G-083 mutual exclusivity makes per-member collection well-defined; a single -multiple member with its own parsekey is unrestricted). This settles the G-084 cross-member accumulation design decision as rejection rather than accumulate-in-received-order: cross-member collection on one storage key is ambiguous (collect-vs-replace undefined; one member's collected list would silently displace another's). The check runs AFTER the G-053 -multiple canonicalisation so range forms get correct verdicts: {0 1} is replace-shape and allowed on a shared group, collect shapes (boolean 1, max>1, unbounded) are rejected. Value -parsekey integrity settled at define time: duplicate value parsekeys are rejected (trailing values consume positionally so distinct values sharing one result slot silently overwrote each other), and a dash-led value parsekey is rejected (it landed in the options namespace of the result/received dicts and ABORTED parse with an internal error in the option-shaped validation paths). parse_status/parse_report storage-key attribution: parse_status_build now resolves a storage key claimed by exactly ONE argument (a value's -parsekey, an option's declared -parsekey or its '='-suffixed trimmed tail) back to that argument, so argstatus/Source report received with the value-in-effect instead of default/0; shared-parsekey group members stay unresolved (per-member attribution does not survive the storage fold - the remaining recorded G-084 display gap, needs engine-level per-member tracking in the parse result; parse_report's display bridge still shows the group value on each member row). undefine/undefine_deflist now return "" (previously returned the final 'dict unset' expression - the WHOLE cache dict, hundreds of KB, to any caller capturing the value). define -help now documents -parsekey (storage-key rename for options and values, leader rejection, shared groups and -parsekeymode interaction, last-defined-member-wins defaults precedence, the -multiple rule, value parsekey uniqueness/non-dash rules). Internal: dead values-loop identifiers removed (val_ident/val_ident_is_parsekey/values-side seen_pks - assigned per word, never read since the api_valname keying landed), the per-parse parsekey reverse map now iterates only VAL_NAMES (leader map removed entirely - a leader -parsekey is rejected at define time), a dead parsekey-from--default derivation removed in resolve, stale pre-G-084 comments corrected. parsekey.test: parsekey_shared_key_multiple_rejected strengthened (message pins, {0 1}-allowed/{0 4}-rejected range coverage, the -parsekeymode error escape hatch incl collection and optionconflict), new parsekey_value_parsekey_integrity; parsereport.test parsereport_storagekey_attribution + parsestatus.test parsestatus_storagekey_attribution pin the attribution (incl the shared-group default/default remaining-gap row). Full punk/args suite 401/0.
#0.23.0 - G-084 increment 1 (parsekey completeness, part 1): value -parsekey is now supported (was: accepted at define time but aborted parse). A value record declaring -parsekey now renames its result `values` slot and its `received` key to the parsekey (e.g `@values {v1 -parsekey renamed -type string}` parses `hello` to `values={renamed hello}`, `received` keyed by `renamed`), and -multiple value members collect under the parsekey (e.g the `variable` setvalues form `?name value...?` pairs collect under `name_value`). The fix keys VAL_DEFAULTS and the values_dict init/storage by parsekey (matching the already-parsekey-keyed VAL_REQUIRED), tracks the parsekey in a new api_valnames_received list (the internal valnames_received stays argname-keyed for the -multiple first/continuation gate and G-053 occurrence enforcement), uses api_valnames_received for the required-satisfaction check and the received dict, and adds a per-form val_pk2name/leader_pk2name reverse map so the valmin clause-length fallback and the post-parse validation loop can resolve a parsekey back to its argname for ARG_INFO/arg_checks lookups. -parsekey on a leader is now REJECTED at define time with a clear message (was: silently ignored) - no live caller uses a leader -parsekey, so the goal's no-silent-ignore contract is met by rejection rather than parallel hot-path surgery; the dead req_name derivation in the leaders resolve branch is removed. The @values directive line still rejects -parsekey (a group-default parsekey is not a feature; per-arg value -parsekey is). The defaulted-members precedence rule for a shared-parsekey group (none received) is now documented as last-defined-member-wins (deterministic in definition order) and the in-code `? review` is removed. The tclcore moduledoc `#todo - fix -parsekey for leaders and values` is resolved (value supported, leader rejected). parsekey.test: the value GAP flipped to parsekey_value_result_key_settled, the leader GAP flipped to parsekey_leader_parsekey_rejected, the @values-line GAP split to parsekey_values_line_rejects_parsekey; the defaults GAP flipped to a settled pin. dev/parsekey-knownbugs.test: the value and leader disabled pins retired (settled in parsekey.test); the cross-member -multiple accumulation pin remains (increment 3). Full punk/args suite 399/0 (1 skipped), punk/ns 125/125, broader punk sweep 989/0.
#0.22.0 - G-053 range-valued -multiple (occurrence arity): -multiple now accepts a {min max} range (max -1 = unbounded) alongside the legacy booleans 0/1, so a definition can declare at most once ({0 1}, a second occurrence is a parse error), bounded repetition ({2 4}), or one-or-more ({1 -1}) instead of choosing between silent last-wins (0) and unbounded collection (1). The spec compiler canonicalises once at resolve: the stored -multiple becomes the computed boolean (list-shape collect: true for max>1 or -1, false for legacy 0 and max==1) so every existing collect-vs-replace / scalar-vs-list / leader-value-single-multiple hot-path truth-test stays correct, and the range companions (min/max/maxbounded) live in a separate per-form MULTIPLE_RANGES dict (NOT in ARG_INFO, so they do not ride along when ARG_INFO is round-tripped as a spec via resolved_def copyfrom). Legacy 0/1 (and boolean strings true/false/yes/no) are coerced to the boolean and stay byte-unchanged (no MULTIPLE_RANGES entry for unlimited cases). Resolve validation: max must be positive or -1, min <= max, and the -optional/range-min reconciliation - a non-zero min forces presence so -optional set is a contradiction (reject with a clear message; declare -optional 0). Parse enforcement: a new PUNKARGS VALIDATION occurrencecount failure class (payload <argname> count <n> min <m> | max <m>) fires in a single post-loop pass per section (opts/leaders/values); over-max is a hard contradiction (fires in both normal and viability-probe modes, parse_status_classify maps it to invalid), under-min is pure end-of-input exhaustion (SUPPRESSED in the G-152 viability probe and classified incomplete so a viable form reports incomplete, not invalid). The usage-table Multi column reflects the range (0-1 / 2-4 / 1+ for unbounded-with-floor; the greencheck stays for legacy 1), the string renderer emits MULTI:0-1 etc., and the synopsis distinguishes at-most-once (?arg?, no ellipsis) from repeating (arg...). -multipleunique/-multipleuniqueset compose with max>1 ranges unchanged. define -help documents the range form and the -optional/range-min rule. New testsuite multipleranges.test (28 tests: 13 define-time canonicalisation+validation, 12 parse-time enforcement incl parse_status verdicts and legacy-required-still-trailingvaluecount guard, 3 rendering); full punk/args suite 399/0, punk/ns 125/125.
#0.21.0 - G-083 increment 3 (argument-relations usage rendering + lsearch moduledoc adoption): the -conflicts and -parsekeymode error vocabulary now surfaces in usage/arg_error and synopsis output, and the lsearch tclcore moduledoc models its documented option incompatibilities with the new vocabulary. Rendering: (a) a per-arg -conflicts list appends a 'conflicts with: <targets>' hint to the argument's help text in the usage table (targets resolved to display names via lookup_optset, so a parsekey target shows its member flag, not the raw parsekey); (b) a named @opts group marked -parsekeymode error is annotated 'mutually exclusive (distinct members may not be combined)' in its group header, while override (default) groups carry no such annotation; (c) the synopsis one-line form carries no conflict detail (conflicts are a usage-table concern, not a synopsis-line one). Characterized in relations.test (conflicts_usage_hint, parsekeymode_error_usage_group_header, parsekeymode_override_no_header_annotation, conflicts_synopsis_no_hint). lsearch moduledoc: -sorted gains -conflicts {-glob -regexp} and -bisect gains -conflicts {-all -not} (per-arg conflicts, the whole group stays -parsekeymode override so -glob/-regexp remain last-wins); the '(documentation incomplete - punk::args fixes required for grouped mutually exclusive options and prefix calculation)' caveat is dropped from the @cmd -help. Pinned in relations.test (lsearch_sorted_conflicts_glob + lsearch_bisect_conflicts_all raise optionconflict, lsearch_glob_regexp_last_wins stays last-wins, lsearch_caveat_dropped). clock clicks unchanged - its active positional-choice definition already models exclusivity (exactly-one-of) and the acceptance clause names lsearch only. Legacy untouched by default (no -conflicts/parsekeymode-error definition carries no new rendering). Full punk/args suite green; tclcoreparity 10/10.

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

@ -11028,8 +11028,9 @@ tcl::namespace::eval punk::args::moduledoc::tclcore {
#@form -form "setvalues" -synopsis "variable ?name value...? ?name?"
@form -form "setvalues"
@values -min 0 -max -1
#G-084 (2026-08-08): value -parsekey now supported (name value pairs
#collect under the parsekey name_value). Leaders -parsekey remains pending.
#G-084 (2026-08-07): value -parsekey now supported (name value pairs
#collect under the parsekey name_value). A leader -parsekey is settled as
#a define-time rejection (no silent ignore).
"name value" -parsekey name_value -type {string any} -optional 1 -multiple 1
name -type string -optional 1 -multiple 0

2
src/tests/modules/AGENTS.md

File diff suppressed because one or more lines are too long

101
src/tests/modules/punk/args/testsuites/args/parsekey.test

@ -324,26 +324,53 @@ namespace eval ::testspace {
]
#added 2026-07-14 (agent) - parsekey coverage review.
#GAP: -multiple 1 collection does NOT accumulate across distinct members sharing a -parsekey.
#Occurrences are collected per member flag, and the shared-key resolution then returns only
#one member's collected list - the values of other members are silently lost
#(here -add received {a c} but the result under -op is just the -sub list {b}).
#Desired accumulate-in-received-order semantics pinned (disabled) in dev/parsekey-knownbugs.test.
test parsekey_shared_key_multiple_collection_GAP {GAP: shared-parsekey members with -multiple 1 lose values of all but one member}\
#added 2026-07-14 (agent) - parsekey coverage review; flipped 2026-08-07 (G-084).
#G-084 settled design: -multiple on a member of a shared-parsekey group is a
#DEFINE-TIME error unless ALL members belong to one @opts group marked
#-parsekeymode error (was: silently lost one member's collected list at parse).
#Cross-member accumulation is not supported - collect-vs-replace and cross-member
#collection are undefined on a shared key. A single -multiple member with its own
#parsekey is fine. Mutual exclusivity (-parsekeymode error, G-083) makes
#per-member -multiple well-defined - the escape hatch is pinned below. The G-053
#canonical boolean governs: a {0 1} range is replace-shape (allowed); collect
#shapes (boolean 1, ranges with max>1 or -1) are rejected.
#Tested via punk::args::resolve directly (the define-time check fires in resolve;
#parse wraps resolve errors context-dependently, so the raw resolve error is the
#stable surface to pin).
test parsekey_shared_key_multiple_rejected {settled (G-084): -multiple on a shared-parsekey group member is rejected at define time unless the whole group is -parsekeymode error}\
-setup $common -body {
set docids [list]
set argd [punk::args::parse {-incr -decr -incr} withdef {@opts -type none -parsekey -dir -multiple 1} {-incr -typedefaults u} {-decr -typedefaults d}]
#two -multiple members sharing -dir via per-member parsekeys -> resolve error
set err [catch {punk::args::resolve [list @id -id ::g84r_dir] @opts {-incr -type none -parsekey -dir -multiple 1} {-decr -type none -parsekey -dir -multiple 1}} msg]
lappend result [list $err [string match "*-multiple is not supported on member*shared-parsekey group '-dir'*" $msg]]
#value-taking members sharing -op -> resolve error
set err [catch {punk::args::resolve [list @id -id ::g84r_op] @opts {-add -type string -parsekey -op -multiple 1} {-sub -type string -parsekey -op -multiple 1}} msg]
lappend result [list $err [string match "*-multiple is not supported on member*" $msg]]
#G-053 range forms: {0 1} is replace-shape (at most once) - allowed on a
#shared group; a collect-shape range {0 4} is rejected like boolean 1
lappend result [catch {punk::args::resolve [list @id -id ::g84r_r01] @opts {-c -type none -parsekey -k01 -multiple {0 1}} {-d -type none -parsekey -k01 -multiple {0 1}}}]
lappend docids ::g84r_r01
set err [catch {punk::args::resolve [list @id -id ::g84r_r04] @opts {-e -type string -parsekey -k04 -multiple {0 4}} {-f -type string -parsekey -k04 -multiple {0 4}}} msg]
lappend result [list $err [string match "*-multiple is not supported on member*" $msg]]
#a SINGLE -multiple member with its own parsekey is fine (not a shared group)
set argd [punk::args::parse {-v a -v b} withdef [list @id -id ::g84r_single] @opts {-v -type string -parsekey -vals -multiple 1}]
lappend docids [dict get $argd id]
lappend result [dict get $argd opts]
lappend result [dict get $argd multis]
lappend result [dict get $argd received]
#value-taking members - the earlier member's collected values {a c} are lost entirely
set argd [punk::args::parse {-add a -sub b -add c} withdef {@opts -type string -parsekey -op -multiple 1} -add -sub]
lappend docids [dict get $argd id]
#the sanctioned escape hatch: ALL members in one @opts group marked
#-parsekeymode error (G-083 mutual exclusivity) - resolves, a repeated
#single member collects under the shared key, co-occurrence still raises
lappend result [catch {punk::args::resolve [list @id -id ::g84r_excl] {@opts -group G -parsekey -pk -parsekeymode error} {-a -type string -multiple 1} {-b -type string -multiple 1}}]
lappend docids ::g84r_excl
set argd [punk::args::parse {-a x -a y} withid ::g84r_excl]
lappend result [dict get $argd opts]
set err [catch {punk::args::parse {-a x -b y} withid ::g84r_excl} msg eopts]
lappend result [list $err [lindex [dict get $eopts -errorcode] 2 0]]
set result
}\
-cleanup {
foreach id $docids {
@ -351,13 +378,16 @@ namespace eval ::testspace {
}
}\
-result [list\
{-dir d}\
{-dir}\
{-dir 0 -dir 1 -dir 2}\
{-op b}
{1 1}\
{1 1}\
0\
{1 1}\
{-vals {a b}}\
0\
{-pk {x y}}\
{1 optionconflict}\
]
#added 2026-07-14 (agent) - parsekey coverage review; flipped 2026-08-07 (G-083).
#A -parsekey value colliding with a distinct defined option's name used to silently
#form an implicit shared-key group (last received wins). It is now a define-time error:
@ -396,13 +426,15 @@ namespace eval ::testspace {
]
#added 2026-07-14 (agent) - parsekey coverage review.
#GAP: -parsekey outside @opts. tclcore moduledoc carries '#todo - fix -parsekey for leaders and values'.
#Current behaviour:
# - per-arg -parsekey on a value is accepted at define time but ABORTS parsing with an internal error
# - the @values directive line rejects -parsekey outright (unrecognised key)
# - per-arg -parsekey on a leader is silently ignored (result keyed by the leader name)
#Desired rename semantics pinned (disabled) in dev/parsekey-knownbugs.test.
#added 2026-07-14 (agent) - parsekey coverage review; settled 2026-08-07 (G-084).
#-parsekey outside @opts, settled behaviour (pinned by the three tests below):
# - a value's -parsekey renames its result values slot and its received key
# (was: accepted at define time but ABORTED parsing with an internal error)
# - the @values directive line still rejects -parsekey (a group-default parsekey
# is not a value feature; per-arg value -parsekey is)
# - a leader's -parsekey is rejected at define time (was: silently ignored)
#The tclcore moduledoc '#todo - fix -parsekey for leaders and values' is resolved
#accordingly (value supported, leader rejected).
test parsekey_value_result_key_settled {settled (G-084): -parsekey on a value renames the values result key and the received key (no longer aborts parse)}\
-setup $common -body {
set argd [punk::args::parse {hello} withdef @values {v1 -parsekey renamed -type string}]
@ -435,5 +467,24 @@ namespace eval ::testspace {
-result [list\
1 1\
]
#added 2026-08-07 (agent, G-084) - value -parsekey integrity settled at define time:
#distinct values sharing one parsekey always both consume (positional), so the
#shared result slot silently overwrote (values cannot form shared-key groups);
#a dash-led value parsekey landed in the options namespace of the result/received
#dicts and aborted parse in the option-shaped validation paths. Both are now
#resolve errors.
test parsekey_value_parsekey_integrity {settled (G-084): duplicate value parsekeys and dash-led value parsekeys are define-time errors}\
-setup $common -body {
set err [catch {punk::args::resolve [list @id -id ::g84v_dup] @values {v1 -parsekey K -type string} {v2 -parsekey K -type string}} msg]
lappend result [list $err [string match "*-parsekey 'K' is declared by multiple value arguments*" $msg]]
set err [catch {punk::args::resolve [list @id -id ::g84v_dash] @values {v1 -parsekey -K -type string}} msg]
lappend result [list $err [string match "*must not begin with '-'*" $msg]]
set result
}\
-result [list\
{1 1}\
{1 1}\
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

43
src/tests/modules/punk/args/testsuites/args/parsereport.test

@ -305,5 +305,48 @@ namespace eval ::testspace {
}\
-result [list {parsed withid} 1 1]
#added 2026-08-07 (agent, G-084) - storage-key attribution: parse_status_build
#resolves UNIQUE storage keys (a value's -parsekey; an option's declared -parsekey
#or its '='-suffixed trimmed tail) so received/Source attribute correctly. A
#SHARED parsekey group stays unresolved - each member row bridges the group's
#value-in-effect with Source (default) (the recorded G-084 display gap:
#per-member received attribution needs engine-level tracking in the parse result).
test parsereport_storagekey_attribution {value -parsekey and renamed-option rows attribute as received; shared-group member rows keep the bridged (default) gap}\
-setup $common -body {
punk::args::define {@id -id ::testspace::prvalpk
@cmd -name testspace::prvalpk
@values -min 1 -max 1
v1 -parsekey renamed -type string
}
lappend result [punk::args::parse_report {hello} -return string withid ::testspace::prvalpk]
punk::args::define {@id -id ::testspace::proptpk
@cmd -name testspace::proptpk
@opts
-B|--before-context= -parsekey "--before-context" -default 0 -type integer
@values -min 0 -max 0
}
lappend result [punk::args::parse_report {-B 3} -return string withid ::testspace::proptpk]
punk::args::define {@id -id ::testspace::prsharedpk
@cmd -name testspace::prsharedpk
@opts -group MS -parsekey -STYLE
-exact -type none -typedefaults "-exact"
-glob -type none -typedefaults "-glob" -default "-glob"
@values -min 0 -max 0
}
set d [punk::args::parse_report {-exact} -return dict withid ::testspace::prsharedpk]
lappend result [list [dict get $d rows -exact source] [dict get $d rows -glob source]]
set result
}\
-cleanup {
punk::args::undefine ::testspace::prvalpk 1
punk::args::undefine ::testspace::proptpk 1
punk::args::undefine ::testspace::prsharedpk 1
}\
-result [list\
{v1 (received) = hello}\
{-B|--before-context= (received) = 3}\
{default default}\
]
}
tcltest::cleanupTests ;#needed to produce test summary.

37
src/tests/modules/punk/args/testsuites/args/parsestatus.test

@ -187,5 +187,42 @@ namespace eval ::testspace {
-cleanup {
}\
-result [list 1 1]
#--- storage-key attribution (G-084) -----------------------------------------------------
#added 2026-08-07 (agent, G-084) - the parse result keys received/opts/values by
#STORAGE key (declared -parsekey; an optset's trimmed '='-suffixed tail; a value
#parsekey). parse_status now resolves a storage key claimed by exactly ONE
#argument back to that argument, so its argstatus reports received/ok with the
#value-in-effect. Shared-parsekey group members stay unresolved (ambiguous fold -
#the recorded G-084 display gap).
test parsestatus_storagekey_attribution {argstatus attributes received/value through unique storage keys (value -parsekey, renamed option)}\
-setup $common -body {
set ps [punk::args::parse_status {hello} withdef {
@id -id ::testspace::psvalpk
@values -min 1 -max 1
v1 -parsekey renamed -type string
}]
set ast [dict get $ps argstatus v1]
lappend result [list [dict get $ast status] [dict get $ast received] [dict get $ast hasvalue] [dict get $ast value]]
lappend result [expr {"v1" in [dict get $ps receivednames]}]
set ps [punk::args::parse_status {-B 3} withdef {
@id -id ::testspace::psoptpk
@opts
-B|--before-context= -parsekey "--before-context" -default 0 -type integer
}]
set ast [dict get $ps argstatus -B|--before-context=]
lappend result [list [dict get $ast status] [dict get $ast received] [dict get $ast hasvalue] [dict get $ast value]]
set result
}\
-cleanup {
punk::args::undefine ::testspace::psvalpk 1
punk::args::undefine ::testspace::psoptpk 1
}\
-result [list\
{ok 1 1 hello}\
1\
{ok 1 1 3}\
]
}
tcltest::cleanupTests ;#needed to produce test summary line.

51
src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test

@ -1,16 +1,19 @@
package require tcltest
#added 2026-07-14 (agent) - parsekey coverage review.
#Desired-behaviour pins for known parsekey gaps. Each has a corresponding _GAP test in
#testsuites/args/parsekey.test pinning today's behaviour; when a fix lands, enable the
#test here and flip/remove the GAP pin there.
#G-084 (2026-08-08): value -parsekey support landed - the parsekey_value_result_key
#pin moved to testsuites/args/parsekey.test as parsekey_value_result_key_settled
#(now an enabled assertion). The leader case is settled as a define-time rejection
#(parsekey_leader_parsekey_rejected in parsekey.test) - no live caller, so -parsekey
#on a leader is rejected rather than silently ignored. The @values directive line
#still rejects -parsekey (parsekey_values_line_rejects_parsekey). The remaining open
#pin here is the cross-member -multiple accumulation design decision.
#Desired-behaviour pins for known parsekey gaps. Each had a corresponding _GAP test
#in testsuites/args/parsekey.test pinning the old behaviour; when a fix landed the
#pin moved/flipped there.
#G-084 (2026-08-07): all three parsekey-knownbug pins are now settled in
#testsuites/args/parsekey.test and this file holds no disabled pins:
# - value -parsekey rename -> parsekey_value_result_key_settled (supported)
# - leader -parsekey -> parsekey_leader_parsekey_rejected (define-time reject)
# - cross-member -multiple -> parsekey_shared_key_multiple_rejected (define-time
# reject: cross-member accumulation on a shared parsekey is not supported - it is
# ambiguous; a single -multiple member per parsekey is fine, and -multiple per
# member is allowed when ALL members share one @opts group marked -parsekeymode
# error, whose mutual exclusivity makes it well-defined).
#This file is retained as the home for future punkargsKnownBug pins.
namespace eval ::testspace {
namespace import ::tcltest::*
tcltest::testConstraint punkargsKnownBug 0
@ -18,31 +21,5 @@ namespace eval ::testspace {
variable common {
set result ""
}
#Design-pending rather than settled: the lean is that -multiple 1 members sharing a
#-parsekey accumulate under the shared key in received order (today only one member's
#collected list survives - see parsekey_shared_key_multiple_collection_GAP).
test parsekey_shared_key_multiple_accumulation {Design-pending: shared-parsekey members with -multiple 1 should accumulate in received order}\
-constraints punkargsKnownBug\
-setup $common -body {
set docids [list]
set argd [punk::args::parse {-incr -decr -incr} withdef {@opts -type none -parsekey -dir -multiple 1} {-incr -typedefaults u} {-decr -typedefaults d}]
lappend docids [dict get $argd id]
lappend result [dict get $argd opts]
set argd [punk::args::parse {-add a -sub b -add c} withdef @opts {-add -type string -parsekey -op -multiple 1} {-sub -type string -parsekey -op -multiple 1}]
lappend docids [dict get $argd id]
lappend result [dict get $argd opts]
}\
-cleanup {
foreach id $docids {
punk::args::undefine $id 1
}
}\
-result [list\
{-dir {u d u}}\
{-op {a b c}}
]
}
tcltest::cleanupTests ;#needed to produce test summary line.
tcltest::cleanupTests ;#needed to produce test summary line.

Loading…
Cancel
Save