diff --git a/CHANGELOG.md b/CHANGELOG.md index 5148ec58..a71b87a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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.12.17] - 2026-07-13 + +- punk::args 0.11.1 (documentation-only): the define help now documents the two definition-registration styles (direct define vs lazy PUNKARGS/register::NAMESPACES registration used by module templates and moduledocs) and the tstr interpolation rules - display-field deferral, parse-field expansion at first resolve, the defspace rule (argdoc child namespace wins when present), the silent-literal fallback for unresolvable placeholders, and the safe patterns for load-time-computed values. + ## [0.12.16] - 2026-07-13 - tclcore moduledoc 0.3.0: 'after cancel ' and 'after info ' now discriminate by the after-id shape, harvested from the running interpreter at load time (user-directed). 'i after cancel someid' resolves cleanly to the cancel-script form exactly as real Tcl treats it (silent script-match no-op), and only genuinely id-shaped words ('after#N') report the cancelid/cancelscript ambiguity - the junction real Tcl itself resolves by id liveness at runtime. Parity pins added (tclcoreparity.test) including the accepted dead-id over-acceptance boundary. diff --git a/punkproject.toml b/punkproject.toml index d07dabf6..d583ccab 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.12.16" +version = "0.12.17" license = "BSD-2-Clause" diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 9729e808..779f5f41 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -683,6 +683,61 @@ tcl::namespace::eval punk::args { can be left entirely as documentation for interactive use with: i ... and for synopsis generation with: s ... + %B%Registration styles%N% + There are two ways for a package to provide definitions: + + 1) Direct calls to punk::args::define. The raw text is stored + immediately (parsing to the specifications dictionary still + happens lazily on first use). + + 2) Deferred registration: append definition text-block lists to + a namespace variable named PUNKARGS and add the namespace to the + public list variable punk::args::register::NAMESPACES. + Nothing is processed at load time. On the first id lookup that + misses (e.g punk::args::parse ... withid, or the i/s help + commands - via punk::args::update_definitions) registered + namespaces are scanned cheaply for @id values, and the + namespace's definitions are defined on demand. + This is the style used by the module templates (dev module.new) + and by the punk::args::moduledoc::* packages - which document + OTHER packages' commands (the scan records which namespaces each + registered namespace documents). Prefer it for anything large: + a package with many or lengthy definitions then adds essentially + nothing to load time. Command-id aliases can similarly be + registered in a PUNKARGS_aliases variable (list of + {aliasid id} pairs). + Per-namespace scan/load timings are visible via + punk::args::status. + + %B%Interpolation (tstr placeholders) and the defspace%N% + \$\{...\} placeholders in definition text use + punk::args::lib::tstr semantics, but WHEN they are expanded + depends on the field: + - display-only fields (-help of @cmd/@examples/arguments, + @formdisplay -header/-body) are deferred and expanded at + display time. + - all other (parse-relevant) fields are expanded when the + definition is first resolved. + - @dynamic definitions re-expand on every resolve. + Expansion happens in the definition's 'defspace' namespace: + for a direct define call, the calling namespace; for registered + PUNKARGS, the namespace's argdoc child IF IT EXISTS, otherwise + the namespace itself. This applies even when the PUNKARGS + variable itself is in the parent: a variable set in the parent + namespace is not visible to placeholder expansion when an + argdoc child exists. + A placeholder that cannot be resolved is currently left literal + in the field without warning - if a literal \$\{...\} shows up + in a parsed spec or rendered help, check the defspace. + For values computed once at package load (e.g a capability or + enumeration harvested from the runtime), the robust patterns + are: pre-expand the block explicitly + (punk::args::define \[punk::args::lib::tstr -return string {...}\]), + substitute a plain token at build time + (\[string map \[list %TOKEN% $val\] {...}\]), + or set the variable in the argdoc namespace. + Use @dynamic only when the value can change between resolves. + The definition should usually contain an initial line of the form: @id -id ::somecmd Blank lines are ignored at the top level, ie if they are not part of another structure. diff --git a/src/modules/punk/args-buildversion.txt b/src/modules/punk/args-buildversion.txt index ac57afe3..e057ae16 100644 --- a/src/modules/punk/args-buildversion.txt +++ b/src/modules/punk/args-buildversion.txt @@ -1,6 +1,7 @@ -0.11.0 +0.11.1 #First line must be a semantic version number #all other lines are ignored. +#0.11.1 - documentation-only: define -help gains a 'Registration styles' section (direct define vs deferred lappend-PUNKARGS + register::NAMESPACES registration - lazy @id scan and on-demand definition via update_definitions, the module-template/moduledoc style, PUNKARGS_aliases, punk::args::status timings; prefer deferred for anything large) and an 'Interpolation (tstr placeholders) and the defspace' section (display-field deferral vs parse-field expansion at first resolve vs @dynamic re-expansion; defspace rule: direct define = calling namespace, registered PUNKARGS = the argdoc child namespace when it exists even if the PUNKARGS variable is in the parent; unresolvable placeholders left silently literal - check the defspace when a literal ${...} appears; robust patterns for load-time-computed values: explicit tstr pre-expansion, build-time string-map token, or setting the variable in argdoc). Motivated by the ::after id-shape harvest misattribution (2026-07-13, corrected record in goals/G-055). #0.11.0 - G-041 increment 2 (doc-surface support): synopsis renderer honours the documented @form -synopsis override - the stored override now replaces the auto-calculated synopsis line in punk::args::synopsis full and summary renders (arg_error's synopsis section honoured it already; the per-form dict gains a 'synopsis' key when overridden, FORMARGS unchanged for dict consumers) - forms.test GAP forms_form_synopsis_override_stored_not_rendered_GAP flipped to forms_form_synopsis_override_rendered. Candidate-form ranking extended to choice discriminators: form_literal_affinity now treats a required argument with a RESTRICTED choice set as a discriminator (matched via choiceword_match, the shared G-040 resolver) alongside literal()/literalprefix() types - the tclcore models express subcommand-ish literals as -choices (e.g after's cancel/idle/info), so 'after cancel' now ranks the cancel forms first in noformmatch errors and parse_status best-candidate selection instead of falling back to declaration order. #0.10.0 - G-041 multi-form candidacy: punk::args::parse/get_dict with the default -form * (or any multi-form selection) now attempts every permitted form instead of effectively parsing form 0 only - a clean match against exactly one form is auto-selected, no match raises a 'noformmatch' PUNKARGS VALIDATION error naming each candidate form's first-line failure (candidates ranked best-first: leading-literal affinity with the supplied words, then incomplete before invalid, then declaration order - private::form_selection/form_literal_affinity/rank_form_failures), and several clean matches raise 'multipleformmatches' naming the forms (no silent preference - deliberate, callers pass -form to disambiguate). -form now accepts a list of form names/indices as documented (get_dict, parse, parse_status, arg_error; supplied order preserved - arg_error renders the argument table for the first listed form and marks all listed forms' synopsis entries; parse's error render passes the ranked candidates). Parse results gain a 'form' key (the parsed form; keys appended after 'id' - positional consumers unaffected) and, when candidacy ran, a 'formstatus' key reporting every attempted form's outcome; parse_status 'form' is now the matched/best-candidate form (per-argument statuses and badarg marking built for it) and gains the documented 'formstatus' key (per-form status/failureclass/badarg/message with caller attribution) - the per-form compatibility surface the G-044 completion/hinting consumer needs. Engine body extracted verbatim to private::get_dict_form (single-form parse; argspecs resolved before selection - no caller-frame use). forms.test GAP pins flipped (auto-selection, -form list restriction) + new coverage (shared-prologue arity discrimination, noformmatch ranking/errorcode, multipleformmatches, formstatus in results and parse_status). #0.9.0 - G-071: allocation choice screen - get_dict_can_assign_value no longer blanket-accepts any word for an argument with a RESTRICTED choice set; where allocation has an alternative (the argument is -optional, or a further occurrence of a -multiple argument) the candidate word is screened with choiceword_match (the shared G-040 implementation: exact/alias/prefix/nocase semantics; -choicemultiple words screened per list member within min/max), and a non-matching word yields onward to later elements/clauses instead of being consumed and failing validation. Fixes the lseq-class in-form failures: an optional choice noise word between required values plus a trailing optional-member clause ('lseq 0 10 2', '0 10 by 2', '1 5 by 0' now parse; noise-word variants and prefix normalization unchanged; genuinely invalid arglists now get the plain excess-values rejection instead of blaming the unrelated optional). REQUIRED arguments are deliberately not screened - the word must fill them and validation's choiceviolation reporting stays informative. -choicerestricted 0 behaviour unchanged. allocation.test GAPs flipped; parse_status -form documented-order behaviour pinned (a prior mis-premise: -form was already supported before the withid/withdef tail per its synopsis).