diff --git a/CHANGELOG.md b/CHANGELOG.md index e3db3a6f..92bec3ef 100644 --- a/CHANGELOG.md +++ b/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 ` declares that diff --git a/GOALS-archive.md b/GOALS-archive.md index d25beb8f..75e7cbfb 100644 --- a/GOALS-archive.md +++ b/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. diff --git a/GOALS.md b/GOALS.md index e31b662d..f56ec4cd 100644 --- a/GOALS.md +++ b/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 diff --git a/goals/G-084-punkargs-parsekey-completeness.md b/goals/G-084-punkargs-parsekey-completeness.md deleted file mode 100644 index 63df5e83..00000000 --- a/goals/G-084-punkargs-parsekey-completeness.md +++ /dev/null @@ -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). diff --git a/goals/G-167-punkargs-version-delta-availability.md b/goals/G-167-punkargs-version-delta-availability.md index d3a62882..621d06e2 100644 --- a/goals/G-167-punkargs-version-delta-availability.md +++ b/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/. diff --git a/goals/archive/G-084-punkargs-parsekey-completeness.md b/goals/archive/G-084-punkargs-parsekey-completeness.md new file mode 100644 index 00000000..dcbc518e --- /dev/null +++ b/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 diff --git a/punkproject.toml b/punkproject.toml index 7bf1f6b3..d9447b28 100644 --- a/punkproject.toml +++ b/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 - diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index cb7c94f2..3c6a9aca 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/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 (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 -parsekey -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 {} 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? diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index fec7cd6a..e57f1348 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/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 count min | max ) 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: ' 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. diff --git a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm b/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm index 6094d820..cd1a6a77 100644 --- a/src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm +++ b/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 diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index 080881e4..b44ed274 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -43,7 +43,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` - `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0 + the 0.7.0 convenience removal forms + the 0.7.1 reload-contract state guards (G-160 follow-ons, 2026-08-04); usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, the 0.7.0 convenience removal forms (pop_rename - command form pops topmost-for-renamer and returns the removed record, bare form searches live stacks with a multi-command ambiguity error; remove_renamer - all of a renamer's entries across live stacks with correct re-linking, Rename_stack-parked stacks skipped, removed records returned keyed by command; restore_original - whole-stack unwind to the original returning the record count, deliberately registering stack-evidenced renamers so it survives a known_renamers reset while the renamer-explicit forms keep the gate; all silent with debug off), the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the reload contract (a module re-source preserves ALL state - stacks/token counters/token map/known_renamers/debug each info-exists guarded; delegation and exact-token removal keep working after reload), the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6) - `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack) - `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW; extended 2026-08-05 by the G-151 pre-modification coverage survey - VIEW -lf 0/1/2 single-line-ization modes + always-on NUL + silent unknown-option tolerance (load-bearing for overtype's '-nul 1' sites) + C1/zero-width visuals, RANGE end-relative/clamping plus the merged-code-stack re-emission and trailing-code-drop semantics a truncate primitive must respect, COUNT-vs-length combining-accent divergence, NEW object basics, and the KNOWN-DEFICIENCY pin that VIEWCODES/VIEWSTYLE raise invalid-command in a plain punk::ansi interp (bare 'a+'/'a' interactive-alias dependency; pinned in a fresh child interp); plus the TRUNCATE primitive pins landed with G-151 (punk::ansi 0.2.0) - fits-unchanged byte identity incl styled fixtures, grapheme-exact capping with marker budget participation and marker-alone truncation, SGR-reset-before-marker on styled prefixes, CJK/combining-cluster grapheme safety, and strict option errors (deliberately unlike VIEW's pinned silent tolerance)), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify), grepstr characterization (`grepstr.test`: return modes incl summarydict (linemap pinned as always-present - the -help says -n-only, reconciliation deferred to the planned hygiene pass), exact highlight SGR wrapping, -n line numbering, invert + empty-highlight strip, -C context/breaks, capture groups, and the tab deficiency: warns once per call on stderr, single-pass tab line survives - the multi-pass mangling is pinned at consumer level in punk/ns corp.test), and untabify characterization (`untabify.test`: -stops int/list/terminal, -with spaces/unicode/custom-pair, multiline, errors, plus the EXPERIMENTAL -plastic elastic-tabstop mode deliberately pinned-as-interim and retained for possible repl editbuf use). Console queries (get_tabstops/get_size + punk::console::tabwidth) are mocked per the overtype renderline.test pattern - they emit live terminal queries that block/error headless. ANSI codes in these tests are literal escape strings so results are colour-state independent -- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging, bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus GAP pins for last-defined-member default precedence, cross-member -multiple value loss, and values/leaders parsekey breakage - desired-behaviour pins disabled behind punkargsKnownBug in `testsuites/dev/parsekey-knownbugs.test`; the parsekey/optname collision GAP is flipped (G-083, 2026-08-07: -parsekey colliding with a distinct defined arg's name is now a resolve error unless that arg shares the parsekey), and the G-083 argument-relations define-time vocabulary is pinned in `relations.test` (per-arg -conflicts target validation, @opts -parsekeymode override|error storage and the requires-group/bad-value/requires-parsekey resolve errors), plus its increment-2 parse-time enforcement (optionconflict raise for -conflicts violations and -parsekeymode error co-occurrence, defaults-never-conflict, cross-group conflicts, parse_status invalid classification), and its increment-3 usage/synopsis rendering and lsearch moduledoc adoption (conflicts-with hints appended to per-arg help rows, -parsekeymode error groups annotated 'mutually exclusive' in group headers with override groups unannotated, synopsis one-line form carrying no conflict detail; and the lsearch tclcore moduledoc modelling -sorted/-glob and -bisect/-all incompatibilities via per-arg -conflicts while -glob/-regexp stay last-wins and the 'punk::args fixes required' caveat is dropped))), the G-053 range-valued -multiple occurrence arity (`multipleranges.test`, punk::args 0.22.0: {min max} range canonicalisation into a per-form MULTIPLE_RANGES companion dict while the stored -multiple boolean keeps its hot-path list-collect meaning so legacy 0/1 stay byte-unchanged, the -optional/range-min reconciliation resolve error, the {0 1}/{2 4}/{1 -1} scalar-vs-list value shapes, parse-time occurrencecount enforcement with min-under-supply suppressed in the G-152 viability probe and classified incomplete (pure exhaustion) while over-max is invalid, the usage-table Multi column showing 0-1/2-4/1+ with the synopsis ?arg? at-most-once ellipsis distinction, and -multipleunique composition), longopt style-distinction and mash-edge characterisation (`longopts.test`, 2026-08-07 gap-fill: the gnu (`--flag=` inline-only; spaced/solo usage = badoptionformat) vs plain (`--flag` spaced-only; inline `=` = invalidoption) vs mixed (`-f|--file|--file=` both forms incl longopt prefix abbreviation) definition-grammar distinctions, first-`=`-only value splitting and empty inline values, the two `=`-member resolve-time definition errors, single-dash `-flag=value` never split at the `=`, and `@opts -any 1` adhoc passthrough where `--flag=value` splits but `-flag=value` stays a whole adhoc flagname expecting a value; the same gap-fill added to `mashopts.test`: unknown-letter-in-mash invalidoption rejection and the `-any` interplay - defined flags still mash while undefined mash-lookalike tokens are adhoc flags, never mash attempts; error-shape pins use the first three -errorcode elements), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix; version-note labels appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) +- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms (incl the 2026-08-05 candidacy fencing/fall-through primitive pins ahead of G-168: -regexprefail on optional value slots is form-fatal with no re-landing, unknown-option/dangling-option/option-value-fence failures fall through to sibling forms as successful parses, and the documented per-word limit - a complete option+value+file line double-matches a fenced scriptfile form and a dash-tolerant stdin form), registered-namespace lazy definition loading (`docpackages.test`, G-169 pre-work: inert registration/scan-vs-load split, id_exists never triggers loading, real_id/usage lazy resolution incl tag-prefixed script-level id families, duplicate-definer last-loaded-wins characterization), the G-168 launch-definition model parity (`punkexemodel.test`, punkexe moduledoc 0.7.0, id loading updated for the G-169 handover - the script id resolves via app-punkscript-docs from src/lib (auto_path derived from the test file location) while the moduledoc carries the core-owned ids: script/tclsh selection matrices against the real (script)::punkexe ids with the real-side dispatch/app oracle recorded in goals/archive/G-168, the bare-'-e' viable-incomplete verdict, the ./-e fence message, -encoding fall-through statuses, and the sanctioned complete-'-encoding' multipleformmatches LIMIT pins; the formcheck.test punkexe GAPs flipped to discriminated/sanctioned/selection-sound pins in the same arc), the G-151 landing report (`parsereport.test`, punk::args 0.18.0 parse_report: the canonical flag-like-word-consumed-as-VALUE attribution row, parsed-result vs words+withid entry parity, machine dict shape with declaration-section row order and absent-optional row omission, received xN multiplicity for solos/-multiple opts, type-aware always-marked elision at the default width plus the caller > @cmd -reportvaluewidth > built-in width cascade and never-elided dict returns, VIEW-style single-line-ization of control-bearing values, aliased-optionset storage-key row bridging plus the G-084 storage-key attribution pin (value-parsekey and renamed-option rows attribute received; a shared-parsekey group member row keeps the bridged value with Source default - the recorded remaining gap), bordered-table/tableobject renders, words-form failure errorcode parity with parse, and the registered two-form definition's own render), rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping, and the G-084 unique-storage-key attribution pin - argstatus reports received/value through a value's -parsekey or a renamed option's declared parsekey), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus the G-084 settled pins (2026-08-07): value -parsekey rename support, @values-line -parsekey rejection, leader -parsekey define-time rejection, last-defined-member defaults precedence, cross-member -multiple define-time rejection (allowed only when ALL members share one @opts -parsekeymode error group; a {0 1} range replace-shape stays allowed) and value-parsekey integrity (duplicate/dash-led value parsekeys rejected at define time); `testsuites/dev/parsekey-knownbugs.test` holds no disabled pins (retained as the future punkargsKnownBug home); the parsekey/optname collision GAP is flipped (G-083, 2026-08-07: -parsekey colliding with a distinct defined arg's name is now a resolve error unless that arg shares the parsekey), and the G-083 argument-relations define-time vocabulary is pinned in `relations.test` (per-arg -conflicts target validation, @opts -parsekeymode override|error storage and the requires-group/bad-value/requires-parsekey resolve errors), plus its increment-2 parse-time enforcement (optionconflict raise for -conflicts violations and -parsekeymode error co-occurrence, defaults-never-conflict, cross-group conflicts, parse_status invalid classification), and its increment-3 usage/synopsis rendering and lsearch moduledoc adoption (conflicts-with hints appended to per-arg help rows, -parsekeymode error groups annotated 'mutually exclusive' in group headers with override groups unannotated, synopsis one-line form carrying no conflict detail; and the lsearch tclcore moduledoc modelling -sorted/-glob and -bisect/-all incompatibilities via per-arg -conflicts while -glob/-regexp stay last-wins and the 'punk::args fixes required' caveat is dropped))), the G-053 range-valued -multiple occurrence arity (`multipleranges.test`, punk::args 0.22.0: {min max} range canonicalisation into a per-form MULTIPLE_RANGES companion dict while the stored -multiple boolean keeps its hot-path list-collect meaning so legacy 0/1 stay byte-unchanged, the -optional/range-min reconciliation resolve error, the {0 1}/{2 4}/{1 -1} scalar-vs-list value shapes, parse-time occurrencecount enforcement with min-under-supply suppressed in the G-152 viability probe and classified incomplete (pure exhaustion) while over-max is invalid, the usage-table Multi column showing 0-1/2-4/1+ with the synopsis ?arg? at-most-once ellipsis distinction, and -multipleunique composition), longopt style-distinction and mash-edge characterisation (`longopts.test`, 2026-08-07 gap-fill: the gnu (`--flag=` inline-only; spaced/solo usage = badoptionformat) vs plain (`--flag` spaced-only; inline `=` = invalidoption) vs mixed (`-f|--file|--file=` both forms incl longopt prefix abbreviation) definition-grammar distinctions, first-`=`-only value splitting and empty inline values, the two `=`-member resolve-time definition errors, single-dash `-flag=value` never split at the `=`, and `@opts -any 1` adhoc passthrough where `--flag=value` splits but `-flag=value` stays a whole adhoc flagname expecting a value; the same gap-fill added to `mashopts.test`: unknown-letter-in-mash invalidoption rejection and the `-any` interplay - defined flags still mash while undefined mash-lookalike tokens are adhoc flags, never mash attempts; error-shape pins use the first three -errorcode elements), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix; version-note labels appear when the class is live OR is a declared forward unavailable class - dict labels on every runtime since G-073, unicode keeps the live-only rule; the G-073 forward-class adoption invariants and the USER-SANCTIONED 'di' prefix-strictness exemption (the model is deliberately stricter than real 8.6, full words parity-true); and the G-166 pin that an unavailable class's per-class virtual id LEADS its help with the unavailability statement on runtimes lacking the class while modern runtimes generate it unchanged. All expectations are derived from the running interpreter, green on 8.6/8.7/9.0 through runtests.tcl on each - native Tcl 8.6 has been a supported runner interpreter since 2026-07-21) - `punk/nav/ns/` — punk::nav::ns tests (`testsuites/nav/navns.test`): the n/ n// n/// navigation state machine (ns/ transitions absolute/relative/glob-no-nav, failed-nav state preservation, quad-colon normalization, v-form content selection, ensemble annotation) and the ::punk::nav::ns::ns_current variable contract the repl/codethread/subshell seeding all consume; display content is covered in punk/ns nslist.test - `punk/repl/` — punk::repl tests (`testsuites/repl/`): opunk console backend integration (`consolebackends.test`) and repl current-namespace retention (`nscurrent.test`: real codethread via repl::init driven by synchronous runscript sends - inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for missing namespaces, the 2026-07-14 stray-namespace seeding fix pinned behaviourally plus a source-text guard on repl::start's inline template; the end-to-end piped subshell session is covered at shell level by shell/testsuites/punkexe/shellnavns.test - which found the first-subshell shared-code-interp asymmetry and the piped-inscope gap recorded there) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity; plus the G-166 availability axis on the flowunavail/flowunavail_nodoc fixtures - the `unavailable` cmdinfo key present on every result, exact and unique-prefix landings on a `-choiceunavailable` name resolving that name's virtual docid ATTRIBUTED rather than resolving nothing, the choice traverse attributing without addressing when no virtual docid exists, and cmdtype staying unchanged throughout since availability is a second axis, never a cmdtype value), n/ display machinery characterization (`nslist.test`: tier A get_ns_dicts classification buckets as the machine contract for display reworks - incl package tail/prefix derivation, alias edge cases, usageinfo scan-dependence; tier B per-element layout-agnostic marking - underline/underdouble/underdotted namespace package styles, command type tag colours, exported/imported markers, the punkargs doc icon; tier C REWORK-flagged pins of the current hardcoded 2-col/4-col layout and nspath subtables, to flip deliberately with the planned punk-tables/width-responsive rework; plus the flipped nslist_types_default pin - bare nslist without -types displays all member types since punk::ns 0.7.1 fixed the braced-literal -types default), corp proc-retrieval and syntax/untabify interplay (`corp.test`: name edge cases, -ranges/-n line handling, basic-highlight ansistrip equivalence, -untabify spaces/unicode tab-free output, the KNOWN-DEFICIENCY pin for default -untabify none on tabbed bodies - grepstr warns per pass and brace overlays mangle tabbed lines, deterministic under mocked console tabstops - and a ::tcl::CopyDirectory -untabify spaces smoke test; precursor coverage for the planned punk::ns hygiene pass), cmdtrace characterization (`cmdtrace.test`: -pause 0 non-interactive runs, linedict line-mark keys for flat and 2-word-form nested switches as correct-mark guards, and GAP pins for the upstream nested-switch mismark - core.tcl-lang.org tktview 5d5b1052280c976ea3d4, arm bodies whose split-list index lands on a literal switch-command word report container-relative lines; mark tests gated on have_tclcoredocs because cmdtrace's arm-offset correction parses against the ::switch argdoc; plus the fixed-canary asserting punk::lib::check::has_tclbug_nestedswitch_tracelines still reports the bug - a live behavioural probe, so a fixed Tcl release fails the canary first and triggers the documented flip workflow), cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); the flipped G-051 pins for pseudo-command cmdtype (`doconly`) + space-form docid prefixes (real `string is` pins behind the have_tclcoredocs constraint); the G-166 unavailability marking on the helpstrua fixture and on the real `string is dict` subject - the `-return dict` key, the table/string marking with the info scheme suppressed so a valid argument tail no longer renders as a cleanly usable command line, and the `-return text` leading `UNAVAILABLE:` line (the two deliberate key-list flips - `cmdhelp_cmdinfo_result_shape`, `cmdhelp_return_dict_valid` - landed with it); remaining GAP pins for TclOO undocumented-method fallback (G-052) and synopsis marking absence (G-050); plus the G-150 flag-led form-narrowing + selection-soundness pins against punk::auto_exec::hash - bare-parse selection contract (incl the dash-led-name noformmatch cost), no-word/unknown-flag/ambiguity whole-render fallbacks, and hash runtime-behaviour-unchanged), and name/path primitive characterization (`nsprimitives.test`: string pins for nsparts/nsprefix/nstail/nsjoin/nsjoinall incl weird colon-run (`:::`) splitting, the trailing-colon parse ambiguity (`::x:` + `y` joins to the same string as `::x` + `:y` and reparses leading-colon-greedy), and prefix/tail/join round-trip and its absolutizing exceptions (the original twin-divergence pins for nsparts1/nsprefix1/nsprefix_orig/nstail1/nstail_orig served as safe-deletion evidence and were removed with the twins in the punk::ns 0.7.0 hygiene pass - divergence record in this file's git history, commit 0c7168a1); plus nseval fq-requirement/create-on-eval/evaluator-proc caching, the native-vs-punk `p:::x` resolution divergence (native namespace eval reaches child `x`, nseval creates/reaches literal `:x`), nseval_ifexists no-create + error propagation on plain and genuinely weird namespaces, nsexists/nschildren/nstree_raw weird-ns and relative-resolution pins, globmatchns `*`/`**`/`?` semantics (incl `*` matching a single inner colon - a formerly stale 'should be fixed' comment above nsglob_as_re was corrected in the 0.7.0 hygiene pass), and nspath_to_absolute/nspath_here_absolute caller-resolution pins; the nsjoinall error-message wart pin ('nsjoin:' prefix) flipped when punk::ns 0.7.1 fixed it) diff --git a/src/tests/modules/punk/args/testsuites/args/parsekey.test b/src/tests/modules/punk/args/testsuites/args/parsekey.test index 05cf75df..179d4f91 100644 --- a/src/tests/modules/punk/args/testsuites/args/parsekey.test +++ b/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. diff --git a/src/tests/modules/punk/args/testsuites/args/parsereport.test b/src/tests/modules/punk/args/testsuites/args/parsereport.test index 9494bef3..6f1c3592 100644 --- a/src/tests/modules/punk/args/testsuites/args/parsereport.test +++ b/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. diff --git a/src/tests/modules/punk/args/testsuites/args/parsestatus.test b/src/tests/modules/punk/args/testsuites/args/parsestatus.test index 82c8b3e6..12a368bb 100644 --- a/src/tests/modules/punk/args/testsuites/args/parsestatus.test +++ b/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. diff --git a/src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test b/src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test index 1a8c3407..8e23de36 100644 --- a/src/tests/modules/punk/args/testsuites/dev/parsekey-knownbugs.test +++ b/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. \ No newline at end of file +tcltest::cleanupTests ;#needed to produce test summary line.