From 055f4bbf2ff10c1a40a44ecbec5f6036b0b286d9 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Fri, 7 Aug 2026 14:35:01 +1000 Subject: [PATCH] G-083 increment 1: punk::args argument-relations define-time vocabulary (0.19.0) Spec compiler (resolve) accepts and stores the new relation keys: - per-argument -conflicts : parsekeys or flag names that must not be RECEIVED together with this argument; checked against received args only at parse time (defaults never conflict) - parse-time optionconflict enforcement lands in a later G-083 increment. - @opts-level -parsekeymode override|error: per named OPT_GROUPS group; error = distinct-member co-occurrence within a shared-parsekey group raises optionconflict at parse; override = legacy last-wins, the default. Both keys are cross-validated at resolve: -conflicts targets must name a defined argname or declared -parsekey; -parsekeymode requires -group and a value of override|error. Define-time integrity hole closed: a -parsekey colliding with a distinct defined argument's name (one not sharing that -parsekey) is now a resolve error instead of silently forming an implicit shared-key group (parsekey_collides_with_defined_optname_GAP flipped in parsekey.test to a define-time error pin, plus redundant-self-name and deliberate-shared-group guards). Design settled (recorded in the goal detail): -parsekeymode is per named group (OPT_GROUPS entry, keyed by the @opts line's -group), not form-wide. Rationale: lsearch uses per-arg -conflicts only (partial conflicts, groups stay default override); clock clicks uses -parsekeymode error on its single named group (full pairwise exclusivity). Unnamed shared-parsekey groups default override. Legacy untouched by default: -parsekeymode override is the default and absent -conflicts means no check runs; the pinned parsekey_repeat_ordering last-wins / prepend-defaults idiom and the full existing suite pass unchanged. New testsuite relations.test pins the define-time vocabulary (7 tests, matching the longopts.test/mashopts.test error-pin style). buildversion 0.18.0 -> 0.19.0. Verification: full punk/args suite via canonical tclsh90s - 356 total / 353 passed / 3 skipped (punkargsKnownBug) / 0 failed. goals_lint clean. No punkproject.toml bump (define-time only; no shell-level user-visible behaviour yet - the module change, not the shell product). Assisted-by: harness=pi; primary-model=huggingface/zai-org/GLM-5.2; api-location=huggingface.co --- goals/G-083-punkargs-argument-relations.md | 28 +++++ src/modules/punk/args-999999.0a1.0.tm | 84 ++++++++++++- src/modules/punk/args-buildversion.txt | 3 +- src/tests/modules/AGENTS.md | 2 +- .../punk/args/testsuites/args/parsekey.test | 30 +++-- .../punk/args/testsuites/args/relations.test | 116 ++++++++++++++++++ 6 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 src/tests/modules/punk/args/testsuites/args/relations.test diff --git a/goals/G-083-punkargs-argument-relations.md b/goals/G-083-punkargs-argument-relations.md index 836fce22..2ab771e7 100644 --- a/goals/G-083-punkargs-argument-relations.md +++ b/goals/G-083-punkargs-argument-relations.md @@ -84,3 +84,31 @@ Design leans (settle and record here during the work): - Related: G-053 (occurrence arity), G-072 (compound clause types - orthogonal), G-084 (parsekey completeness fixes; its cross-member -multiple design decision should be made with this goal's vocabulary in mind). + +## Progress + +- 2026-08-07 increment 1 (define-time vocabulary, punk::args 0.19.0): spec compiler + accepts and stores the new keys - per-arg `-conflicts ` (ARG_INFO) and + @opts-level `-parsekeymode override|error` (per named OPT_GROUPS entry, default + override). Resolve cross-validates: `-conflicts` targets must name a defined + argname or declared `-parsekey`; `-parsekeymode` requires `-group` and a value of + override|error. Define-time integrity hole closed: a `-parsekey` colliding with a + distinct defined argument's name (one not sharing that `-parsekey`) is now a + resolve error instead of silently forming an implicit shared-key group + (parsekey_collides_with_defined_optname_GAP flipped in parsekey.test to a + define-time error pin). No parse-time enforcement yet - the optionconflict raise + and parse_status_classify entry are increment 2. New testsuite relations.test pins + the define-time vocabulary (7 tests). Legacy untouched by default confirmed: full + punk/args suite 356 total / 353 passed / 3 skipped (punkargsKnownBug) / 0 failed + via tclsh90s. + - Design decision settled: `-parsekeymode` is per named group (OPT_GROUPS entry, + keyed by the @opts line's `-group`), not form-wide. Rationale: lsearch has + three shared-parsekey groups and uses per-arg `-conflicts` only (partial + conflicts within/across groups, all groups stay default override); clock + clicks uses `-parsekeymode error` on its single named group (full pairwise + exclusivity). Both adoption cases use named groups. Unnamed shared-parsekey + groups default to override (a user wanting strict adds `-group`). + - Remains for acceptance: increment 2 (parse-time optionconflict raise in the + shared post-resolution block, mash+ordinary parity, parse_status_classify); + increment 3 (synopsis/usage hints for conflicts/parsekeymode; lsearch + clock + clicks moduledoc adoption and dropping the caveat). diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 40567c08..a8ec83bc 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -2659,7 +2659,7 @@ tcl::namespace::eval punk::args { -group { tcl::dict::set tmp_optspec_defaults -group $v if {$v ne "" && ![tcl::dict::exists $FDICT OPT_GROUPS $v]} { - tcl::dict::set FDICT OPT_GROUPS $v {-parsekey {} -help {}} + tcl::dict::set FDICT OPT_GROUPS $v {-parsekey {} -help {} -parsekeymode override} } if {$v ne ""} { if {[tcl::dict::exists $at_specs -parsekey]} { @@ -2678,15 +2678,37 @@ tcl::namespace::eval punk::args { set groupdict [tcl::dict::get $FDICT OPT_GROUPS] #set helprecords [tcl::dict::get $F $fid OPT_GROUPS_HELP] if {![tcl::dict::exists $groupdict $g]} { - tcl::dict::set FDICT OPT_GROUPS $g [dict create -parsekey {} -help $v] + tcl::dict::set FDICT OPT_GROUPS $g [dict create -parsekey {} -help $v -parsekeymode override] } else { tcl::dict::set FDICT OPT_GROUPS $g -help $v } } + -parsekeymode { + #G-083: @opts-level strict mode for a shared-parsekey group. + #override (default) = legacy last-wins among distinct members sharing the parsekey. + #error = distinct-member co-occurrence within the group raises optionconflict at parse. + #Applies to the group named by this @opts line's -group (and its -parsekey); + #-group is required when -parsekeymode is given so the target group is named. + if {$v ne "override" && $v ne "error"} { + error "punk::args::resolve Bad @opt line. -parsekeymode must be 'override' or 'error'. got '$v' @id:$DEF_definition_id" + } + if {![tcl::dict::exists $at_specs -group]} { + error "punk::args::resolve Bad @opt line. -parsekeymode requires -group to name the shared-parsekey group it applies to. @id:$DEF_definition_id" + } + set g [tcl::dict::get $at_specs -group] + if {$g eq ""} { + error "punk::args::resolve Bad @opt line. -parsekeymode requires a non-empty -group to name the shared-parsekey group it applies to. @id:$DEF_definition_id" + } + if {![tcl::dict::exists [tcl::dict::get $FDICT OPT_GROUPS] $g]} { + tcl::dict::set FDICT OPT_GROUPS $g [dict create -parsekey {} -help {} -parsekeymode $v] + } else { + tcl::dict::set FDICT OPT_GROUPS $g -parsekeymode $v + } + } -range { if {[dict exists $at_specs -type]} { set tp [dict get $at_specs -type] - } else { + } else { set tp [dict get $tmp_optspec_defaults -type] } if {[llength $tp] == 1} { @@ -2728,7 +2750,7 @@ tcl::namespace::eval punk::args { tcl::dict::set tmp_optspec_defaults $k $v } default { - set known { -parsekey -group -grouphelp + set known { -parsekey -group -grouphelp -parsekeymode -any -anyopts -mash -arbitrary -form -minsize -maxsize -choices -choicegroups -choicemultiple -choicecolumns -choicelabels -choiceinfo -type -range -typeranges -default -defaultdisplaytype -typedefaults -choiceprefix -choiceprefixdenylist -choiceprefixreservelist -choicerestricted -nocase @@ -3376,6 +3398,17 @@ tcl::namespace::eval punk::args { -parsekey - -group { tcl::dict::set spec_merged $spec $specval } + -conflicts { + #G-083: per-argument relation. A list of parsekeys or flag names that + #must not be RECEIVED together with this argument. Checked against + #received arguments only (defaults never conflict); resolved through + #the alias/prefix machinery at check time. Cross-validated against + #the defined argument set after all specs merge. + if {[catch {llength $specval}]} { + error "punk::args::resolve - invalid value for key '$spec' in specifications for argument '$argname' - value must be a list of parsekeys or flag names @id:$DEF_definition_id" + } + tcl::dict::set spec_merged -conflicts $specval + } -mash { #allow when any alt in argname is a single letter flag such s -a or -Z #single letter flags do not have to be -type none to allow -mash to be set true. @@ -3533,7 +3566,7 @@ tcl::namespace::eval punk::args { } else { set known_argopts [list {*}{ -form -type - -parsekey -group + -parsekey -group -conflicts -range -typeranges -default -defaultdisplaytype -typedefaults -minsize -maxsize -choices -choicegroups @@ -3740,6 +3773,47 @@ tcl::namespace::eval punk::args { } } + #G-083: define-time integrity for -parsekey and the new relations vocabulary. + #Cross-arg checks run here because all ARG_INFO entries for the form exist by now. + set _g83_argnames [dict get $FDICT OPT_NAMES] + lappend _g83_argnames {*}[dict get $FDICT VAL_NAMES] + lappend _g83_argnames {*}[dict get $FDICT LEADER_NAMES] + + #(a) -parsekey colliding with a distinct defined arg's name silently forms an + #implicit shared-key group (parsekey_collides_with_defined_optname_GAP). Reject + #unless that other arg also declares the same -parsekey (a deliberate group). + foreach _g83_an $_g83_argnames { + set _g83_pk [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g83_an] -parsekey ""] + if {$_g83_pk eq ""} continue + foreach _g83_other $_g83_argnames { + if {$_g83_other eq $_g83_an} continue + if {$_g83_other ne $_g83_pk} continue + set _g83_other_pk [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g83_other] -parsekey ""] + if {$_g83_other_pk ne $_g83_pk} { + error "punk::args::resolve - -parsekey '$_g83_pk' for argument '$_g83_an' collides with the name of defined argument '$_g83_other' (which does not share that -parsekey). Use a distinct -parsekey, or give '$_g83_other' the same -parsekey to form an explicit shared-key group. @id:$DEF_definition_id" + } + } + } + + #(b) -conflicts targets must name a defined argname or a declared -parsekey. + #(the alias/prefix resolution of received args happens at parse time.) + set _g83_resolve_targets [dict create] + foreach _g83_t $_g83_argnames { + dict set _g83_resolve_targets $_g83_t 1 + set _g83_tpk [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g83_t] -parsekey ""] + if {$_g83_tpk ne ""} {dict set _g83_resolve_targets $_g83_tpk 1} + } + foreach _g83_an $_g83_argnames { + set _g83_conflicts [punk::args::system::Dict_getdef [dict get $FDICT ARG_INFO $_g83_an] -conflicts {}] + if {![llength $_g83_conflicts]} continue + foreach _g83_target $_g83_conflicts { + if {![dict exists $_g83_resolve_targets $_g83_target]} { + error "punk::args::resolve - -conflicts for argument '$_g83_an' names '$_g83_target' which is neither a defined argument name nor a declared -parsekey in this form. @id:$DEF_definition_id" + } + } + } + unset -nocomplain _g83_argnames _g83_resolve_targets _g83_an _g83_pk _g83_other _g83_other_pk _g83_t _g83_tpk _g83_conflicts _g83_target + #set mashargs [dict get $F $fid OPT_MASHES] set mashargs [dict get $FDICT OPT_MASHES] if {[llength $mashargs]} { diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index 10096581..4a9b79aa 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.18.0 +0.19.0 #First line must be a semantic version number #all other lines are ignored. +#0.19.0 - G-083 increment 1 (argument-relations define-time vocabulary): new per-argument key -conflicts (parsekeys or flag names that must not be RECEIVED together with this argument; checked against received args only at parse time, defaults never conflict - parse-time optionconflict enforcement lands in a later G-083 increment) and new @opts-level -parsekeymode override|error (per named OPT_GROUPS group; error = distinct-member co-occurrence within a shared-parsekey group raises optionconflict at parse; override = legacy last-wins, the default). Both keys are cross-validated at resolve: -conflicts targets must name a defined argname or declared -parsekey; -parsekeymode requires -group and a value of override|error. Define-time integrity hole closed: a -parsekey value colliding with a distinct defined argument's name (one that does not share that -parsekey) is now a resolve error instead of silently forming an implicit shared-key group (parsekey_collides_with_defined_optname_GAP flipped to a define-time error pin in parsekey.test). Legacy behaviour untouched by default (no -conflicts, default -parsekeymode override, no colliding parsekey). New testsuite relations.test pins the define-time vocabulary; full punk/args suite green. #0.18.0 - G-151 annotated success render: new punk::args::parse_report - landing report for a SUCCESSFUL parse, one row per value-holding argument (received, or filled from -default) in declaration-section order (leaders, opts, values) with columns Argument | Source | Value - the success-side sibling of the arg_error usage table (a flag-like word consumed as a VALUE by position shows attributed to its consuming argument - the make.tcl dry-run 'kitname = punk91 -confirm 0' class). Input: a punk::args::parse result dict (its id key must resolve to a registered definition), or words + 'withid ' (the parse is performed first; a rejected line raises the same validation error parse would - only successful parses have a landing report). Source column: received / received xN (-multiple aggregation count) / default - the plain-mode-safe twin of the colour marking (received CLR(check), default CLR(parsekey_hint)); absent optionals with no default-in-effect get no row. Returns: table (arg_error-styled bordered block via textblock; the default), tableobject, string (plain lines 'name (source) = value'; also the degraded no-textblock path), dict (machine form: id/form/rows with class/source/received/positions/multiple/hasvalue/value - never elided). Display elision is type-aware, deterministic and always marked (display-hint honesty): strings single-line-ized (ansistring VIEW -lf 1 -vt 1 -sp 0 - embedded controls/ANSI render visibly so a value cannot disturb the table; real spaces stay spaces) then grapheme-capped via the new ansistring TRUNCATE (punk::ansi 0.2.0) with trailing '...' plus ' (len N)' citing the raw value's string length; -multiple aggregations and -type list values element-capped with '(+N more)' (whole list-quoted elements kept while they fit, first element char-capped if it alone overflows); -type dict values pair-capped with '(+N pairs)'; markers stay intact so cells can exceed very small caps by the marker width; plain-string fallbacks when punk::ansi is unavailable. Width cascade per the G-149 plumbing precedent: caller -valuewidth > new @cmd directive key -reportvaluewidth > built-in 48; 0 means no cap; deliberately never console-derived (deterministic and pipe-safe - the G-007 emit-then-query lesson). Value-in-effect lookup bridges storage-key folds at display level (-parsekey renames, aliased optionsets' last-|-member finalopts fold) so received aliased/parsekey'd arguments keep their row - G-084 owns the storage-key model itself. Failure-side rendering (arg_error/usage) unchanged. make.tcl retiring its interim one-line 'dry-run: line accepted ...' report in favour of this render is consumer follow-through after a bootsupport promotion (not part of this change). New testsuite parsereport.test; define -help documents @cmd -reportvaluewidth beside the other directive-options. #0.17.0 - G-149 configurable multi-form synopsis hint labels: the per-form hint labels rendered beside a multi-form definition's synopsis lines in the usage/arg_error table (hard-coded ' i -form N ...' previously) are now a template substituted per form - %formindex% replaced with the form's 0-based ordinal, %formname% with its name; an empty template suppresses the labels entirely (the cell keeps just the Synopsis: label, as single-form renders do). Precedence: caller option > definition > built-in default. New arg_error/usage option -formhint