diff --git a/CHANGELOG.md b/CHANGELOG.md index 880e7746..094e2cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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.53.1] - 2026-08-05 + +- G-082 follow-on landed (punk::args 0.15.2, source module): a REQUIRED typed + value or leader whose supplied word fails the basic-type allocation screen + (int/double/bool/number/dict) now reports the pointed typemismatch naming + the word and argument (with -badarg/-badval) instead of the generic + "Not enough remaining values" missingrequiredvalue/missingrequiredleader + shortfall. Genuine shortfalls (end-of-input, literal-typed members, + multi-member clause partials) report unchanged. parse_status for the + affected shape now classifies invalid/typemismatch (the formstatus + viability verdict was already invalid). Kits pick the fix up at the next + bootsupport/vfscommon refresh and bake. + ## [0.53.0] - 2026-08-05 - Single help mechanism (user direction; boot core 0.5.0, moduledoc::punkexe diff --git a/goals/archive/G-082-punkargs-error-selection.md b/goals/archive/G-082-punkargs-error-selection.md index 4f6b8f80..f2cbe761 100644 --- a/goals/archive/G-082-punkargs-error-selection.md +++ b/goals/archive/G-082-punkargs-error-selection.md @@ -84,4 +84,4 @@ multiform rank_form_failures selection. ## Follow-ons -Follow-on: a REQUIRED typed value failing the basic-type screen reports missingrequiredvalue rather than a typemismatch naming the word (the rejection-record seam would cover it) => open +Follow-on: a REQUIRED typed value failing the basic-type screen reports missingrequiredvalue rather than a typemismatch naming the word (the rejection-record seam would cover it) => landed 2026-08-05 (punk::args 0.15.2 - the required-arg raise sites in the values AND leaders loops consult the rejection record and report the pointed typemismatch; end-of-input/literal/clause-partial shortfalls unchanged; pins flipped in parsestatus.test/formviability.test/cmdhelp.test, new errorselection.test required-position coverage) diff --git a/punkproject.toml b/punkproject.toml index f95221ea..3f50c464 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,6 +1,6 @@ [project] name = "punkshell" -version = "0.53.0" +version = "0.53.1" 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 6cb6dc37..1c666454 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -7724,11 +7724,15 @@ tcl::namespace::eval punk::args { status valid | invalid | incomplete invalid - a supplied word failed validation incomplete - required arguments missing (a count or - allocation shortfall). Note a supplied word - failing its -type check can also surface as - an allocation shortfall (missingrequiredvalue) - - badarg and the per-argument statuses carry - the specifics in both situations. For a + allocation shortfall). A supplied word + failing a required argument's basic -type + screen reports typemismatch (invalid) as of + 0.15.2; a word leaving a multi-member + clause short, or failing a literal-typed + member, still surfaces as an allocation + shortfall (missingrequiredvalue) - badarg + and the per-argument statuses carry the + specifics in both situations. For a single-form parse this is the engine failure classification, NOT a viability verdict - read formstatus for that. For a multiform @@ -10971,6 +10975,15 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$leadername ni $leadernames_received} { + #G-082 follow-on (0.15.2): mirrored from the values loop below + #(per the MAINTENANCE contract above) - a present word that + #failed this required leader's basic-type screen reports the + #pointed typemismatch instead of the generic shortfall. + if {$rj ne "" && [dict get $rj kind] eq "type" && [dict get $rj argname] eq $leadername} { + set rj_type [dict get $rj type] + set msg "Leading argument '$leadername' for %caller% requires type '$rj_type'. Received: '$ldr'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $rj_type] -badarg $leadername -badval $ldr -argspecs $argspecs]] $msg + } #puts stderr "private::get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -badarg $leadername -argspecs $argspecs]] $msg @@ -11213,6 +11226,19 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$valname ni $valnames_received} { + #G-082 follow-on (0.15.2): a word IS present but failed this + #required argument's basic-type allocation screen - report the + #pointed typemismatch (same message/errorcode shape as the + #overflow-site selection below) instead of the generic + #missingrequiredvalue shortfall. Only kind 'type' can arrive + #here (choice screens never apply to required args - G-071 + #asymmetry); literal mismatches and multi-member clauses record + #no rejection, so those shapes keep the shortfall report. + if {$rj ne "" && [dict get $rj kind] eq "type" && [dict get $rj argname] eq $valname} { + set rj_type [dict get $rj type] + set msg "Trailing argument '$valname' for %caller% requires type '$rj_type'. Received: '$val'" + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list typemismatch $rj_type] -badarg $valname -badval $val -argspecs $argspecs]] $msg + } #puts stderr "private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -badarg $valname -argspecs $argspecs]] $msg diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index d7896164..e67f03ea 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.15.1 +0.15.2 #First line must be a semantic version number #all other lines are ignored. +#0.15.2 - G-082 follow-on: a REQUIRED typed value or leader whose present word fails the basic-type allocation screen (int/double/bool/number/dict) now reports the pointed typemismatch (message + errorcode with -badarg/-badval, same shape as the G-082 overflow-site selection) instead of the generic missingrequiredvalue/missingrequiredleader shortfall - the in-loop required-arg raise sites consult the rejection record returned by get_dict_can_assign_value for the current word. Unchanged by design: end-of-input shortages (valuemissing/count classes), a word failing a required literal-typed member, and multi-member clause partials (no rejection record - conservatism preserved); required-arg choice words were never screened (G-071 asymmetry) so choiceviolation reporting is untouched. parse_status consequences: this shape's top-level status flips incomplete->invalid with failureclass typemismatch (the formstatus verdict was already 'invalid' - the G-152 probe confirmed each such 'incomplete' away before reporting; one probe re-parse saved); multiform noformmatch classes unchanged (already invalid), per-form failureclass/message now the pointed typemismatch. parse_status argdoc status-key note updated. Pins flipped deliberately: parsestatus.test typed-value allocation failure, formviability.test single-form record (top-status/formstatus divergence demonstration re-anchored on a required-option shape), punk/ns cmdhelp.test badarg message match; new errorselection.test required-position coverage (value + leader) with unchanged-shape guards (end-of-input, literal-typed, clause partial). #0.15.1 - G-164 viability-probe alternative-allocation re-probe: when the valmin reservation seats a word that literally names a declared required option into a required value slot at end-of-input, and the resulting optionmissing stands in the G-152 viability probe (a value word consumed, defeating the position guard), the probe now re-parses with the stolen word(s) consumed as the options they name (private::get_dict_form's viabilitycheck argument carries {1 reseat_words} on the re-probe pass) and grants 'incomplete' (viable) on pure end-of-input exhaustion after that re-seating. Cures the 'i hash -t' false negative: the test form classes viable since {-t foo} parses; sibling forms stay invalid. Options still missing after the re-seating are ordinary exhaustion in the re-probe. Runtime seating is unchanged - the re-probe runs only inside the probe, only via reservation-seated words (the trailing-flag-as-value rule is load-bearing; flagvalues_plain_then_dash and the runtime optionmissing path pinned unchanged). A stolen word naming a value-TAKING required option stays conservative (invalid) - re-seated, it seats as a value again at end-of-input. Verdict contract documentation updated in the parse/parse_status argdoc. formviability.test gains 7 pins (re-probe, delete-form, other-missing-options exhaustion, unnamed stolen word, value-taking conservatism, runtime-unchanged, single-form formstatus); the cmdhelp.test GAP pin cmdhelp_GAP_multiform_flagled_testform_false_notviable flips to cmdhelp_multiform_flagled_testform_viable. #0.15.0 - @values -flagvalues (G-150 follow-on, user-directed): a @values section may declare -flagvalues 1 - an unmatched dash-led word in option position is then consumed as a trailing value instead of raising invalidoption (declared options still match first; the '--' end-of-options marker is unaffected; off by default). Motivation: '--help'-style forms robustly swallowing ignored trailing words of any shape - previously such a word seated as a value only positionally (at end-of-input via the valmin reservation, or within a bounded valmax) and errored 'Unexpected option' otherwise ('{--help -t foo}' on the unconstrained shape). Mechanism: VAL_FLAGVALUES on the form dict (template default false; @values compiler key, boolean-validated at resolve time) short-circuits the unmatched-dash-word fallback in the parse engine. In-tree consumer: punk::auto_exec::hash's help form. New testsuite flagvalues.test (9 tests: seating matrix, declared-option precedence, off-by-default contrast, optionmissing after value seating, key validation). Documented in the module header notes. #0.14.0 - G-152 suffix-viability verdict: multiform per-form failure statuses (the 'formstatus' records, the noformmatch errorcode's 'classes' list and its -formerrors records) are now SOUND suffix-viability verdicts - a form reports 'incomplete' (viable) if and only if every supplied word was validly consumed as a prefix of that form and the failure is pure exhaustion at end-of-input; anything already supplied contradicting the form reports 'invalid'. Previously the count/missing-required failure classes granted 'incomplete' before final validation ever saw the consumed words, so a wrong action word landed 'incomplete' on a value-carrying sibling form identically to a genuinely-viable prefix ({frobnicate} vs {info} were indistinguishable), and a word rejected by an allocation type screen (missingrequiredvalue) also classed 'incomplete'. Mechanism: private::get_dict_form gains a viability-probe mode (optional 4th arg) suppressing exactly the pure-exhaustion raises - below-minimum leading/trailing count shortfalls and missing-required leader/option/value checks, each position-guarded so appended words could actually reach the missing element (leaders only with no option/value words consumed; options only before value words and any eopts --; values always), plus an adhoc option's value lying beyond the supplied words - while every contradiction raise stands, including final validation of the consumed words (the soundness backstop). Multiform candidacy and parse_status's single-form formstatus record confirm each classify-'incomplete' failure with one probe re-parse before reporting it. Documented conservatism: input ending INSIDE a multi-member type clause reports invalid (clause allocation cannot affirm the partial words). Rendered noformmatch messages mark each form line '(viable - needs more arguments)' or '(not viable)' after the stable "form '':" anchor; the existing incomplete-first ranking thereby puts still-satisfiable forms first. Contract documented in the parse ('Multiform failure contract' help section) and parse_status (formstatus/status key) argdocs; parse_status's top-level 'status' for single-form parses keeps the engine classification (display-scheme driver) - only formstatus carries the verdict. New testsuite formviability.test (acceptance scenarios, option-position guards, single-form record soundness, clause conservatism, message marking); forms.test verdict pins deliberately updated (ms/idle 'incomplete'->'invalid' for words failing their type screens). diff --git a/src/tests/modules/punk/args/testsuites/args/errorselection.test b/src/tests/modules/punk/args/testsuites/args/errorselection.test index 965bc9ed..00b560dc 100644 --- a/src/tests/modules/punk/args/testsuites/args/errorselection.test +++ b/src/tests/modules/punk/args/testsuites/args/errorselection.test @@ -127,6 +127,69 @@ namespace eval ::testspace { ] + #added 2026-08-05 (agent, G-082 follow-on) - required-position basic-type screen rejections. + #The goal's recorded residue: a REQUIRED typed value/leader whose PRESENT word failed the + #basic-type screen reported the generic missingrequiredvalue/missingrequiredleader + #shortfall ("Not enough remaining values...") - punk::args 0.15.2 reports the pointed + #typemismatch from the same rejection-record seam the optional-overflow selection uses. + test errsel_required_type_reject {Required typed (int) value and leader rejecting a present word report pointed typemismatch (punk::args 0.15.2 - was missingrequiredvalue/missingrequiredleader)}\ + -setup $common -body { + set docid1 ::testspace::errsel_required_type_value + set docid2 ::testspace::errsel_required_type_leader + set docids [list $docid1 $docid2] + punk::args::define [list @id -id $docid1] {@values -min 1 -max 1} {num -type int -optional 0} + punk::args::define [list @id -id $docid2] {@leaders -min 1 -max 1} {count -type int -optional 0} {@values -min 0 -max -1} {extra -type string -optional 1 -multiple 1} + + set err [catch {punk::args::parse {notanumber} withid $docid1} msg opts] + lappend result $err {*}[ecinfo $opts] + lappend result [string match "*Trailing argument 'num' for*requires type 'int'. Received: 'notanumber'*" $msg] + + set err [catch {punk::args::parse {notanumber} withid $docid2} msg opts] + lappend result $err {*}[ecinfo $opts] + lappend result [string match "*Leading argument 'count' for*requires type 'int'. Received: 'notanumber'*" $msg] + }\ + -cleanup { + foreach id $docids { + punk::args::undefine $id 1 + } + }\ + -result [list\ + 1 {PUNKARGS VALIDATION {typemismatch int}} {-badarg num -badval notanumber} 1\ + 1 {PUNKARGS VALIDATION {typemismatch int}} {-badarg count -badval notanumber} 1 + ] + + + #added 2026-08-05 (agent, G-082 follow-on) + test errsel_required_shortfall_shapes_unchanged {Genuine shortfalls keep their missing-class reports: end-of-input, literal-typed required member, multi-member clause partial}\ + -setup $common -body { + set docid1 ::testspace::errsel_shortfall_endofinput + set docid2 ::testspace::errsel_shortfall_literal + set docid3 ::testspace::errsel_shortfall_clause + set docids [list $docid1 $docid2 $docid3] + punk::args::define [list @id -id $docid1] {@values -min 0 -max -1} {num -type int -optional 0} {name -type string -optional 0} + punk::args::define [list @id -id $docid2] {@values -min 1 -max 1} {reset -type literal(reset) -optional 0} + punk::args::define [list @id -id $docid3] {@values -min 0 -max -1} {coord -type {int int} -optional 0 -multiple 1} + + #no word present for name - the post-loop required check reports it + set err [catch {punk::args::parse {5} withid $docid1} msg opts] + lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0] + #literal-category rejections are deliberately unrecorded (syntax-word shapes) - shortfall stays + set err [catch {punk::args::parse {other} withid $docid2} msg opts] + lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0] + #end-of-input inside a multi-member clause - allocation cannot affirm the partial words + set err [catch {punk::args::parse {5} withid $docid3} msg opts] + lappend result $err [lindex [lindex [dict get $opts -errorcode] 2] 0] + }\ + -cleanup { + foreach id $docids { + punk::args::undefine $id 1 + } + }\ + -result [list\ + 1 valuemissing 1 missingrequiredvalue 1 missingrequiredvalue + ] + + test errsel_genuine_surplus {Genuinely-surplus words still report toomanyarguments (all optionals satisfied or none defined)}\ -setup $common -body { set docid1 ::testspace::errsel_surplus_after_choice diff --git a/src/tests/modules/punk/args/testsuites/args/formviability.test b/src/tests/modules/punk/args/testsuites/args/formviability.test index d3dad490..b44e1878 100644 --- a/src/tests/modules/punk/args/testsuites/args/formviability.test +++ b/src/tests/modules/punk/args/testsuites/args/formviability.test @@ -215,6 +215,12 @@ namespace eval ::testspace { -result [list incomplete invalid valid] #added 2026-08-02 (agent, G-152) + #2026-08-05 (agent, G-082 follow-on): the type-screen probe formerly demonstrated the + #documented top-status/formstatus split (engine 'incomplete' via missingrequiredvalue + #vs verdict 'invalid') - punk::args 0.15.2 reports that shape as the pointed + #typemismatch (engine 'invalid', agreeing with the verdict), so the split + #demonstration moved to the required-option probe (optionmissing classifies + #'incomplete' while the consumed value word defeats the probe's position guard). test formviability_singleform_formstatus_sound {single-form parse_status: the formstatus record carries the viability verdict while the top-level status keeps the engine classification}\ -setup $common -body { punk::args::define { @@ -224,18 +230,32 @@ namespace eval ::testspace { num -type int -optional 0 name -type string -optional 0 } + punk::args::define { + @id -id ::testspace::vsingleopt + @cmd -name testspace::vsingleopt -summary "required-option fixture" -help "required-option fixture" + @opts + -dest -type string -optional 0 + @values -min 0 -max -1 + extra -type string -optional 1 -multiple 1 + } #valid prefix (5 satisfies num) ending in exhaustion: viable set pstat [punk::args::parse_status {5} withid ::testspace::vsingle] lappend result [dict get $pstat status] [dict get $pstat formstatus _default status] - #word contradicts num's int type: engine classifies the allocation - #shortfall incomplete (documented) but the form is NOT viable + #word contradicts num's int type: pointed typemismatch (0.15.2) - engine + #classification and viability verdict agree on invalid set pstat [punk::args::parse_status {notanint} withid ::testspace::vsingle] lappend result [dict get $pstat status] [dict get $pstat failureclass] [dict get $pstat formstatus _default status] + #required option missing after a value word has begun: the engine classifies + #optionmissing 'incomplete' but the form is NOT viable (appended flag words + #would be consumed as values) - the documented split, single-form + set pstat [punk::args::parse_status {sometarget} withid ::testspace::vsingleopt] + lappend result [dict get $pstat status] [dict get $pstat failureclass] [dict get $pstat formstatus _default status] }\ -cleanup { punk::args::undefine ::testspace::vsingle 1 + punk::args::undefine ::testspace::vsingleopt 1 }\ - -result [list incomplete incomplete incomplete missingrequiredvalue invalid] + -result [list incomplete incomplete invalid typemismatch invalid incomplete optionmissing invalid] #added 2026-08-02 (agent, G-152) test formviability_clause_partial_conservative {input ending inside a multi-member type clause reports invalid - documented conservatism of the verdict}\ diff --git a/src/tests/modules/punk/args/testsuites/args/parsestatus.test b/src/tests/modules/punk/args/testsuites/args/parsestatus.test index f9cf52a9..82c8b3e6 100644 --- a/src/tests/modules/punk/args/testsuites/args/parsestatus.test +++ b/src/tests/modules/punk/args/testsuites/args/parsestatus.test @@ -103,7 +103,10 @@ namespace eval ::testspace { }\ -result [list 0 invalid error animal choiceviolation bad choiceviolation unparsed 1] - test parsestatus_typed_value_allocation_failure {a supplied word failing its -type check surfaces as an allocation shortfall WITH the unfillable argument as badarg}\ + #flipped 2026-08-05 (agent, G-082 follow-on) - was parsestatus_typed_value_allocation_failure + #pinning failureclass missingrequiredvalue: punk::args 0.15.2 reports a required typed + #value's basic-type screen rejection as the pointed typemismatch (badarg unchanged) + test parsestatus_typed_value_screen_typemismatch {a supplied word failing a required value's -type screen reports typemismatch WITH the unfillable argument as badarg}\ -setup $common -body { set ps [punk::args::parse_status {v1 x x} withid ::testspace::psfix] lappend result [dict get $ps ok] [dict get $ps failureclass] [dict get $ps badarg] @@ -112,7 +115,7 @@ namespace eval ::testspace { }\ -cleanup { }\ - -result [list 0 missingrequiredvalue firstval bad unparsed] + -result [list 0 typemismatch firstval bad unparsed] test parsestatus_count_shortfall_incomplete {too few values reports incomplete with no offending argument}\ -setup $common -body { diff --git a/src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test b/src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test index 19fe4849..0b17780e 100644 --- a/src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test +++ b/src/tests/modules/punk/ns/testsuites/ns/cmdhelp.test @@ -349,13 +349,17 @@ namespace eval ::testspace { #the failure message names the queried command via parse -caller instead of whatever #the call-frame walk found. - test cmdhelp_badarg_marking_failed_typed_value {a value failing its -type check (surfacing as an allocation shortfall) badarg-marks the unfillable argument's row}\ + #2026-08-05 (agent, G-082 follow-on): message pin updated - punk::args 0.15.2 reports a + #required typed value's screen rejection as the pointed typemismatch ("Trailing argument + #'firstval' ... requires type 'int'") instead of the missingrequiredvalue shortfall + #("fail on firstval"); the badarg marking derivation is unchanged + test cmdhelp_badarg_marking_failed_typed_value {a value failing its -type check (typemismatch since punk::args 0.15.2) badarg-marks the failing argument's row}\ -setup $common -body { variable BAD set out [punk::ns::cmdhelp ::testspace::helpfix v1 x x] #the message names the failing argument and its row is badarg-marked #punk::args 0.12.6: the message renders once BELOW the usage display - sample the last line - lappend result [string match "*fail on firstval*" [punk::ansi::ansistrip [lindex [split $out \n] end]]] + lappend result [string match "*Trailing argument 'firstval'*requires type 'int'. Received: 'x'*" [punk::ansi::ansistrip [lindex [split $out \n] end]]] lappend result [any_line_has_sgr $out firstval $BAD] lappend result [any_line_has_sgr $out lastval $BAD] }\