diff --git a/src/bootsupport/modules/punk/args-0.13.0.tm b/src/bootsupport/modules/punk/args-0.14.0.tm similarity index 98% rename from src/bootsupport/modules/punk/args-0.13.0.tm rename to src/bootsupport/modules/punk/args-0.14.0.tm index 74e8eb73..4b257760 100644 --- a/src/bootsupport/modules/punk/args-0.13.0.tm +++ b/src/bootsupport/modules/punk/args-0.14.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.13.0 +# Application punk::args 0.14.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.13.0] +#[manpage_begin punkshell_module_punk::args 0 0.14.0] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -6528,6 +6528,31 @@ tcl::namespace::eval punk::args { punk::args::define). When -form permits several forms, the form that auto-selection settled on is reported here, and a formstatus key is added recording each attempted form's outcome (see punk::args::parse_status). + + Multiform failure contract (stable, machine-readable): + when a multi-form candidacy attempt matches no form, the raised error + carries the errorcode + PUNKARGS VALIDATION {noformmatch forms classes } + -formerrors -argspecs + where 'forms' lists the candidate form names ranked best-candidate + first and 'classes' holds one suffix-viability verdict per listed + form (same order) from the vocabulary: + incomplete - VIABLE: every supplied word was validly consumed as + a prefix of that form and parsing stopped only from + exhaustion at end-of-input - appending further + words can still satisfy the form + invalid - NOT viable: something already supplied contradicts + the form - no appended words can satisfy it + -formerrors holds one record per form (same ranked order) with keys + status/failureclass/badarg/message - 'status' is the same verdict. + The verdict is conservative for arguments whose -type is a + multi-member clause: input ending inside such a clause reports + invalid even when the partial clause words match. + Arguments matching several forms cleanly raise errorcode + PUNKARGS VALIDATION {multipleformmatches forms } + Consumers (form narrowing, interactive completion) should select + candidate forms from these channels rather than parsing message + text. } @form -form {withid withdef} @leaders -min 1 -max 1 @@ -6934,7 +6959,10 @@ tcl::namespace::eval punk::args { } noformmatch { #G-041 multi-form candidacy: payload is: forms classes - #incomplete when some candidate form needs only more words + #G-152: the classes are sound per-form suffix-viability verdicts (candidacy + #confirms each 'incomplete' by a viability-mode re-parse before raising) - + #overall incomplete therefore means at least one form is still satisfiable + #by appending words if {"incomplete" in [Dict_getdef $payload classes {}]} { return incomplete } @@ -7176,6 +7204,10 @@ tcl::namespace::eval punk::args { #G-041: order a formfailures dict (fid -> {status .. failureclass .. badarg .. message ..}) #best-candidate first: literal affinity desc, then incomplete before invalid, then the #incoming (declaration) order. Returns the re-ordered dict. + #order a noformmatch failure's per-form records best-candidate first: leading-literal + #affinity with the supplied words, then status incomplete before invalid, then + #declaration order (stable sort). G-152: status is the confirmed suffix-viability + #verdict, so the class tiebreak ranks still-satisfiable forms above impossible ones. proc private::rank_form_failures {argspecs rawargs formfailures} { set scored [list] dict for {fid finfo} $formfailures { @@ -7676,7 +7708,14 @@ tcl::namespace::eval punk::args { failing its -type check can also surface as an allocation shortfall (missingrequiredvalue) - badarg and the per-argument statuses carry - the specifics in both situations. + 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 + noformmatch failure incomplete does carry + verdict weight: it means at least one + candidate form is still viable (its + formstatus status is incomplete). scheme suggested display scheme: info (ok) | error (failure) message empty | the validation failure message errorcode empty | the -errorcode of the validation failure with @@ -7695,14 +7734,33 @@ tcl::namespace::eval punk::args { then incomplete before invalid, then declaration order). formstatus dict keyed by candidate form name - each value a - dict with at least the key 'status' - (valid | incomplete | invalid) and for failed + dict with at least the key 'status' and for failed candidates failureclass, badarg and message. + 'status' is a per-form suffix-viability verdict + (stable consumer contract): + valid - the supplied words satisfy the form + incomplete - VIABLE: every supplied word was + validly consumed as a prefix of the + form and parsing stopped only from + exhaustion at end-of-input - + appending further words can still + satisfy the form + invalid - NOT viable: something already + supplied contradicts the form - no + appended words can satisfy it + The verdict is conservative for arguments whose + -type is a multi-member clause (e.g + {literal(elseif) expr script}): input ending + inside such a clause reports invalid even when + the partial clause words match. Single-form parses report their one form; when multi-form candidacy ran, every attempted form is reported - partial-arglist consumers (e.g command - completion/hinting) can read per-form - compatibility from this key. + completion/hinting, cmdhelp form narrowing) can + select the still-satisfiable form(s) from this + key. The raising parse exposes the same verdicts + in its noformmatch errorcode - see + punk::args::parse. receivednames received argument names normalized to definition argument names (opt aliases folded to their -alias|-name optionset) @@ -7848,7 +7906,17 @@ tcl::namespace::eval punk::args { } } default { - set formstatus [dict create $fid [dict create status $status failureclass $failureclass badarg $badarg message $r]] + #G-152: formstatus statuses are suffix-viability verdicts. The overall + #status above keeps the engine failure classification (it drives the + #display scheme), but the per-form record must not report 'incomplete' + #unless the supplied words are a valid prefix of the form - confirm by + #a viability-mode re-parse exactly as multiform candidacy does. + set formverdict $status + if {$formverdict eq "incomplete" + && [catch {private::get_dict_form $spec_dict $fid $arglist 1}]} { + set formverdict invalid + } + set formstatus [dict create $fid [dict create status $formverdict failureclass $failureclass badarg $badarg message $r]] } } #the -argspecs payload (the whole resolved spec) is display machinery - too bulky @@ -9512,8 +9580,21 @@ tcl::namespace::eval punk::args { } set classinfo [lindex $ecode 2] set failureclass [lindex $classinfo 0] + set formverdict [private::parse_status_classify $failureclass [lrange $classinfo 1 end]] + #G-152: a per-form 'incomplete' is a suffix-viability verdict - it must hold + #only when every supplied word was validly consumed as a prefix of the form + #and the failure is pure exhaustion at end-of-input. The failure class alone + #cannot witness that (count and missing-required classes fire before final + #validation sees the consumed words), so confirm by re-parsing in viability + #mode: exhaustion raises are skipped there while validation of the consumed + #words still applies - a raise means something already supplied contradicts + #the form. + if {$formverdict eq "incomplete" + && [catch {private::get_dict_form $argspecs $fid $rawargs 1}]} { + set formverdict invalid + } dict set formfailures $fid [dict create\ - status [private::parse_status_classify $failureclass [lrange $classinfo 1 end]]\ + status $formverdict\ failureclass $failureclass\ badarg [Dict_getdef [lrange $ecode 3 end] -badarg ""]\ message [lindex [split $fresult \n] 0]\ @@ -9547,7 +9628,16 @@ tcl::namespace::eval punk::args { set classes [lmap finfo [dict values $formfailures] {dict get $finfo status}] set msg "Bad arguments for %caller%. No form of the command matches the supplied arguments. Candidate forms: $candidate_fids" dict for {fid finfo} $formfailures { - append msg \n " form '$fid': [dict get $finfo message]" + #G-152: visibly distinguish forms still satisfiable by appending words + #(status incomplete - the confirmed suffix-viability verdict) from + #forms nothing appended can satisfy. Keep the "form '':" prefix + #intact - consumers/tests anchor on it. + if {[dict get $finfo status] eq "incomplete"} { + set verdicttag "(viable - needs more arguments)" + } else { + set verdicttag "(not viable)" + } + append msg \n " form '$fid': $verdicttag [dict get $finfo message]" } return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list noformmatch forms $candidate_fids classes $classes] -argspecs $argspecs -formerrors $formfailures]] $msg } @@ -9562,7 +9652,23 @@ tcl::namespace::eval punk::args { #body, extracted verbatim from get_dict when multi-form candidacy landed (G-041). #argspecs is a RESOLVED spec dict (get_dict resolves the deflist in its caller's #context before selecting forms - nothing here touches the caller's frame). - proc private::get_dict_form {argspecs fid rawargs} { + # + #viabilitycheck (G-152): when 1, run as a suffix-viability probe - the question + #answered is not "do these arguments satisfy the form" but "are these arguments a + #valid PREFIX of the form" (could appending further words still satisfy it). The + #probe suppresses exactly the raises that represent pure exhaustion at + #end-of-input (below-minimum count checks and missing-required checks, each + #guarded by position conditions ensuring appended words could actually reach the + #missing element, and an option's value lying beyond the supplied words) and lets + #every other raise stand - including the final validation of the words actually + #consumed, which is the soundness backstop: a probe raise means something already + #supplied contradicts the form. Callers only test raised-vs-returned; the + #returned dict of a probe run is partial and must be discarded. + #Known conservatism (documented in the parse/parse_status contracts): input + #ending INSIDE a multi-member type clause raises (clause allocation cannot + #affirm the partial words) - such prefixes report non-viable even when the + #partial clause words match. + proc private::get_dict_form {argspecs fid rawargs {viabilitycheck 0}} { #puts "-arg_info->$arg_info" set flagsreceived [list] ;#for checking if required flags satisfied set solosreceived [list] @@ -10564,6 +10670,15 @@ tcl::namespace::eval punk::args { tcl::dict::set arg_checks $a $OPT_CHECKS_DEFAULTS #assert -type value has llength 1 (multitype clauses not allowed for opts) if {[tcl::dict::get $argstate $a -type] ne "none"} { + if {$viabilitycheck && $i == $maxidx} { + #G-152 viability probe: the adhoc option's value lies beyond + #the supplied words - pure end-of-input exhaustion. End the + #opts scan; the unfinished flag stays unassigned (required + #checks are position-guarded in probe mode). + set arglist [lrange $remaining_rawargs 0 $i-1] + set post_values {} + break + } if {[tcl::dict::get $argstate $a -multiple]} { tcl::dict::lappend opts $a $flagval if {$a ni $multisreceived} { @@ -11172,14 +11287,29 @@ tcl::namespace::eval punk::args { } #----------------------------------------------------- + #G-152 viability probe: a below-minimum count is pure end-of-input exhaustion - + #satisfiable by appending words - only while the parse position can still reach + #that section: the leaders section only when nothing was consumed beyond it (no + #option words, no value words - appended words land after those and cannot fill + #a leader); the trailing values always (values end the argument list and the + #loops above consumed every supplied word). The probe skips those raises so + #final validation can pass verdict on the words actually consumed. + #Above-maximum counts are contradictions in any mode. + if {$viabilitycheck} { + set skip_leadershortfall [expr {![llength $flagsreceived] && ![llength $values]}] + set skip_valueshortfall 1 + } else { + set skip_leadershortfall 0 + set skip_valueshortfall 0 + } if {$leadermax == -1} { #only check min - if {$num_leaders < $leadermin} { + if {$num_leaders < $leadermin && !$skip_leadershortfall} { set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected at least $leadermin" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg } } else { - if {$num_leaders < $leadermin || $num_leaders > $leadermax} { + if {($num_leaders < $leadermin && !$skip_leadershortfall) || $num_leaders > $leadermax} { if {$leadermin == $leadermax} { set msg "Bad number of leading values for %caller%. Got $num_leaders leaders. Expected exactly $leadermin" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadingvaluecount $num_leaders min $leadermin max $leadermax] -argspecs $argspecs]] $msg @@ -11192,12 +11322,12 @@ tcl::namespace::eval punk::args { if {$valmax == -1} { #only check min - if {$num_values < $valmin} { + if {$num_values < $valmin && !$skip_valueshortfall} { set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected at least $valmin" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg } } else { - if {$num_values < $valmin || $num_values > $valmax} { + if {($num_values < $valmin && !$skip_valueshortfall) || $num_values > $valmax} { if {$valmin == $valmax} { set msg "Bad number of trailing values for %caller%. Got $num_values values. Expected exactly $valmin" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list trailingvaluecount $num_values min $valmin max $valmax] -argspecs $argspecs]] $msg @@ -11230,14 +11360,30 @@ tcl::namespace::eval punk::args { # error "Required value missing for [Get_caller]. missing values $missing marked with -optional false - so must be present" #} #for now (2024-06) punk::lib::ldiff is a better compromise across normal/safe interps e.g 0.7/0.8us - if {[llength $LEADER_REQUIRED]} { + #G-152 viability probe: a missing required argument is pure end-of-input + #exhaustion - satisfiable by appending words - only while the parse position + #can still reach its argument class: a required leader only when no option or + #value words followed the leaders; a required option only while no value words + #have begun and no eopts marker (--) has closed the options; a required value + #always (values end the argument list). The probe skips those raises in that + #situation; otherwise the absence stands as a contradiction even in probe mode. + if {$viabilitycheck} { + set skip_leadermissing [expr {![llength $flagsreceived] && ![llength $values]}] + set skip_optionmissing [expr {![llength $values] && "--" ni $flagsreceived}] + set skip_valuemissing 1 + } else { + set skip_leadermissing 0 + set skip_optionmissing 0 + set skip_valuemissing 0 + } + if {[llength $LEADER_REQUIRED] && !$skip_leadermissing} { if {[llength [set missing [punk::args::system::punklib_ldiff $LEADER_REQUIRED $leadernames_received]]]} { set msg "Required leader missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list leadermissing $missing received $leadernames_received] -argspecs $argspecs]] $msg #arg_error "Required leader missing for [Get_caller]. missing values: '$missing' marked with -optional false - so must be present" $argspecs } } - if {[llength $OPT_REQUIRED]} { + if {[llength $OPT_REQUIRED] && !$skip_optionmissing} { #broken e.g #punk::args::define {@id -id ::spud} @opts {-x -parsekey -coord -optional 0} {-y -parsekey -coord -optional 0} #error is: @@ -11259,7 +11405,7 @@ tcl::namespace::eval punk::args { #arg_error "Required option missing for [Get_caller]. missing flags: '$missing' are marked with -optional false - so must be present " $argspecs } } - if {[llength $VAL_REQUIRED]} { + if {[llength $VAL_REQUIRED] && !$skip_valuemissing} { if {[llength [set missing [punk::args::system::punklib_ldiff $VAL_REQUIRED $valnames_received]]]} { set msg "Required value missing for %caller%. missing values: '$missing' marked with -optional false - so must be present" return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list valuemissing $missing received $valnames_received] -argspecs $argspecs]] $msg @@ -14113,7 +14259,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.13.0 + set version 0.14.0 }] return diff --git a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm index 132f49d4..0a9395e2 100644 Binary files a/src/bootsupport/modules/punk/mix/templates-0.2.0.tm and b/src/bootsupport/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/binary-glob b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/binary-glob new file mode 100644 index 00000000..a292bf61 --- /dev/null +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/binary-glob @@ -0,0 +1,5 @@ +#Allow binary checkins by default - no fossil commit warnings/prompts for binary content. +#Generated projects carry binary payloads from generation time: zip-based .tm modules +#injected into src/bootsupport by the generating shell, and layout-carried binaries +#(e.g bin/sdx.kit) where the layout ships them. +* diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/crlf-glob b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/crlf-glob index eaa1c665..3bd5fc22 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/crlf-glob +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/.fossil-settings/crlf-glob @@ -1 +1,8 @@ +#Suppress fossil CRLF commit warnings/prompts for trees whose content arrives with +#endings the generated project's owner does not author: bootsupport module/lib +#snapshots injected at generation time inherit the generating shell's file endings, +#src/embedded holds generated docs, and bin/ carries layout-shipped utility scripts. +#Authored project files outside these trees still get fossil's CRLF warning. src/embedded/* +src/bootsupport/* +bin/*