punk::ansi 0.2.1 + punk::args 0.25.1: add missing -- to 'regexp -about' sites - dash-led grepstr patterns and -type regexp values work again (project 0.62.1)
grepstr regression report (user, 2026-08-08): 'grepstr -h underline
-group test-group' errored 'bad option "-group": must be -all, -about,
-indices, ...' - regexp's own option list - with or without the --
end-of-options marker, where an old punk901t.exe kit worked.
Diagnosis: the punk::args parse layer was never at fault. A live probe
seats the dash-led word as the pattern value in both invocation forms
(without a marker via the starved-values/valmin reservation rule). The
failure was the body's capture-group probe 'regexp -about $pattern' in
punk::ansi::grepstr - no -- guard, so any dash-led PATTERN VALUE was
read as a regexp option. Present since the proc's introduction
(bc9bacd8, 2025-11-10), which also repointed the repl 'grepstr' alias
from the dash-safe ::punk::grepstr predecessor - so kits baked since
then regressed vs older kits.
Sister defect, same class: the punk::args -type regex|regexp validator's
'catch {regexp -about $e_check}' falsely rejected a legitimate dash-led
regex value as 'requires type regexp' with regexp's bad-option text
embedded as the reason.
Fix: add -- at both sites (ansi grepstr numgroups probe, args
validator). A genuinely invalid dash-led regex still fails as a type
mismatch, now with the real compile reason.
New pins:
- ansi/grepstr.test grepstr_dashled_pattern: the reported invocation
with and without the -- marker, exact highlight expectations.
- args validation.test type_regexp_dashled_value: dash-led valid ARE
accepted; invalid '-(' rejected without 'bad option' in the message.
- args validation.test opt_dashword_seats_when_values_require_it: the
starved-values dash-led seating rule grepstr relies on, pinned at
args level (no -- marker needed when word supply equals
required-values demand).
src/tests/modules/AGENTS.md index updated (punk/ansi + punk/args
bullets). The promoted bootsupport/vfs copies (ansi 0.2.0, args 0.18.0)
still carry the bug until the next coherent-set promotion + bake.
punk::ansi 0.2.0 -> 0.2.1, punk::args 0.25.0 -> 0.25.1, project 0.62.0
-> 0.62.1 + CHANGELOG (user-visible shell command regression fix).
Suites: testbody_lint 1687 clean; grepstr.test 10/10; validation.test
13/13; modules tree 1334 total / 1323 pass / 11 constraint-skipped /
0 fail (zig-built tclsh90s 9.0.5); make.tcl projectversion consistency
+ staleness OK.
Claude-Session: https://claude.ai/code/session_01QgaxV27VZkmEec7oNbEVFc
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
#0.2.1 - grepstr bugfix (dash-led pattern regression, reported 2026-08-08): the capture-group-count probe 'regexp -about' lacked its -- end-of-options guard, so ANY dash-led pattern errored with regexp's own option list ('bad option "-...": must be -all, -about, ...') whether or not the grepstr invocation used the -- marker - present since the proc's introduction 2025-11-10 (commit bc9bacd8, which also repointed the repl 'grepstr' alias from the dash-safe ::punk::grepstr predecessor, so kits baked since then regressed vs older kits such as punk901t). The punk::args parse layer was never at fault: it seats the dash-led word as the pattern value in both invocation forms (without a marker via the starved-values/valmin reservation rule - now pinned args-side in punk/args validation.test). New pin ansi/grepstr.test grepstr_dashled_pattern covers both invocation forms. Sibling fix: punk::args 0.25.1 (-type regexp validation had the same missing --).
#0.2.0 - G-151: new ansistring TRUNCATE ?-ellipsis marker? string maxgraphemes - ANSI/grapheme-safe display truncation primitive (homed beside RANGE/VIEW per the ANSI-surgery domain rule; punk::args::parse_report 0.18.0 is the driving consumer). Returns string unchanged (byte-identical - trailing and non-SGR codes included) when its grapheme count (as for COUNT) is at most maxgraphemes; otherwise the leading graphemes extracted as for RANGE (merged/canonicalized SGR re-application per the contract documented at 0.1.4) with the marker appended such that the result's total grapheme count equals maxgraphemes. The marker (default '...') charges its grapheme count against the budget; a marker alone meeting/exceeding the budget is itself truncated to maxgraphemes; maxgraphemes 0 returns empty string; negative or non-integer maxgraphemes errors. When the kept prefix carries ANSI codes an SGR reset is appended before the marker (RANGE re-applies stacks with no trailing reset) so the marker and any following text render unstyled. Grapheme-count based, not display-column based (wide CJK graphemes count 1 each - a column-aware variant is a possible follow-on). Strict option handling (unknown option errors - unlike VIEW's pinned silent tolerance). Ensemble export + PUNKARGS argdoc + doctools comment added. New pins in ansi/ansistring.test.
#0.2.0 - G-151: new ansistring TRUNCATE ?-ellipsis marker? string maxgraphemes - ANSI/grapheme-safe display truncation primitive (homed beside RANGE/VIEW per the ANSI-surgery domain rule; punk::args::parse_report 0.18.0 is the driving consumer). Returns string unchanged (byte-identical - trailing and non-SGR codes included) when its grapheme count (as for COUNT) is at most maxgraphemes; otherwise the leading graphemes extracted as for RANGE (merged/canonicalized SGR re-application per the contract documented at 0.1.4) with the marker appended such that the result's total grapheme count equals maxgraphemes. The marker (default '...') charges its grapheme count against the budget; a marker alone meeting/exceeding the budget is itself truncated to maxgraphemes; maxgraphemes 0 returns empty string; negative or non-integer maxgraphemes errors. When the kept prefix carries ANSI codes an SGR reset is appended before the marker (RANGE re-applies stacks with no trailing reset) so the marker and any following text render unstyled. Grapheme-count based, not display-column based (wide CJK graphemes count 1 each - a column-aware variant is a possible follow-on). Strict option handling (unknown option errors - unlike VIEW's pinned silent tolerance). Ensemble export + PUNKARGS argdoc + doctools comment added. New pins in ansi/ansistring.test.
#0.1.4 - documentation-only: ansistring RANGE argdoc + doctools comment now state the pinned contract (tests ansi/ansistring.test, 2026-08-05 coverage survey): merged/canonicalized SGR re-emission (style-equivalent, not byte-identical), SGR-only carriage (non-SGR sequence types not re-emitted), trailing-code exclusion with INDEXCODE end+1 as the retrieval mechanism, and clamping/empty-range semantics. Summary reworded (was 'codes preserved' - overclaimed). Open design questions (trailing codes on literal end, non-SGR retention, charset/gx0 handling) stay as code TODO/review comments and G-151 Notes - deliberately not promised in the doc.
#0.1.4 - documentation-only: ansistring RANGE argdoc + doctools comment now state the pinned contract (tests ansi/ansistring.test, 2026-08-05 coverage survey): merged/canonicalized SGR re-emission (style-equivalent, not byte-identical), SGR-only carriage (non-SGR sequence types not re-emitted), trailing-code exclusion with INDEXCODE end+1 as the retrieval mechanism, and clamping/empty-range semantics. Summary reworded (was 'codes preserved' - overclaimed). Open design questions (trailing codes on literal end, non-SGR retention, charset/gx0 handling) stay as code TODO/review comments and G-151 Notes - deliberately not promised in the doc.
#0.1.3 - documentation-only: grepstr -return summarydict choicelabel corrected - the summary dict's 'linemap' key is always present (the old text claimed it appeared only with -n; actual always-present behaviour pinned in tests ansi/grepstr.test). Reconciliation deferred from the grepstr characterization wave to the punk::ns hygiene pass.
#0.1.3 - documentation-only: grepstr -return summarydict choicelabel corrected - the summary dict's 'linemap' key is always present (the old text claimed it appeared only with -n; actual always-present behaviour pinned in tests ansi/grepstr.test). Reconciliation deferred from the grepstr characterization wave to the punk::ns hygiene pass.
#0.25.1 - bugfix: the -type regex|regexp validator's 'regexp -about' lacked its -- end-of-options guard, so a legitimate dash-led regex VALUE (e.g '-group', a valid ARE matching its literal self) was falsely rejected as 'requires type regexp' with regexp's own bad-option text embedded as the reason. Dash-led regexes now validate; a genuinely invalid dash-led regex still fails as a type mismatch with the real compile reason. Found via the 2026-08-08 grepstr dash-led pattern regression report (punk::ansi 0.2.1 carries the sibling 'regexp -about' fix in grepstr itself; the parse/allocation layer was verified NOT at fault). New validation.test pins: type_regexp_dashled_value (accept + real-reason-reject) and opt_dashword_seats_when_values_require_it (the starved-values dash-led seating rule grepstr relies on - no -- marker needed when word supply equals required-values demand).
#0.25.0 - G-053 allocation participation (directed work, post-G-053): bounded -multiple occurrence ranges now PARTICIPATE in positional allocation for leaders and values instead of being post-loop enforcement only. Previously greedy collection overran a bounded max unless a type screen happened to stop it (untyped {3 3} followed by a -multiple tail collected 4 words and occurrencecount then reported the overrun the allocation itself created), and a later REQUIRED ranged argument was starved to a single reserved clause. Three cooperating sites: (a) get_dict_can_assign_value caps a satisfied -multiple argument at its bounded max - it yields the word (no-consume) so the caller's retreat advances to the next argument, serving both the leaders and values loops; the yield carries a G-082 rejection record of new kind 'occurrence', and both loops' overflow selection sites render it as the pointed occurrence-limit report with the G-053 occurrencecount errorcode (count = the attempted occurrence) instead of the generic toomanyarguments - so genuine over-supply keeps its pointed class; (b) reservations are min-occurrence aware - the allocator's tail_needs and the derived valmin floor (leader/value split + option-scan reservation) reserve min-occurrences x min-clause-length for a required ranged argument (e.g a following required -multiple {3 3} reserves 3 clauses; {2 2} pair clauses reserve 4 words); an explicit '@values -min' still overrides the derived floor, and legacy boolean -multiple (no MULTIPLE_RANGES entry) reserves exactly as before; (c) the greedy leader scan caps a bounded -multiple last leader at max occurrences (tracked via leader_posn_names_assigned) so remaining words flow to the opts/values sections. First occurrences are never capped (resolve guarantees range max >= 1); {0 1} at-most-once scalars are unaffected (no collection). define -help -multiple documents the allocation semantics. New multipleranges.test allocation pins: untyped/typed {3 3} cap incl the cap-beats-type-screen 4-ints case, greedy-up-to-max {1 2}, required-range and pair-clause reservations, leaders-side cap + split floor, pointed over-supply report, legacy-greed-unchanged guard. Full punk/args suite 408/0.
#0.25.0 - G-053 allocation participation (directed work, post-G-053): bounded -multiple occurrence ranges now PARTICIPATE in positional allocation for leaders and values instead of being post-loop enforcement only. Previously greedy collection overran a bounded max unless a type screen happened to stop it (untyped {3 3} followed by a -multiple tail collected 4 words and occurrencecount then reported the overrun the allocation itself created), and a later REQUIRED ranged argument was starved to a single reserved clause. Three cooperating sites: (a) get_dict_can_assign_value caps a satisfied -multiple argument at its bounded max - it yields the word (no-consume) so the caller's retreat advances to the next argument, serving both the leaders and values loops; the yield carries a G-082 rejection record of new kind 'occurrence', and both loops' overflow selection sites render it as the pointed occurrence-limit report with the G-053 occurrencecount errorcode (count = the attempted occurrence) instead of the generic toomanyarguments - so genuine over-supply keeps its pointed class; (b) reservations are min-occurrence aware - the allocator's tail_needs and the derived valmin floor (leader/value split + option-scan reservation) reserve min-occurrences x min-clause-length for a required ranged argument (e.g a following required -multiple {3 3} reserves 3 clauses; {2 2} pair clauses reserve 4 words); an explicit '@values -min' still overrides the derived floor, and legacy boolean -multiple (no MULTIPLE_RANGES entry) reserves exactly as before; (c) the greedy leader scan caps a bounded -multiple last leader at max occurrences (tracked via leader_posn_names_assigned) so remaining words flow to the opts/values sections. First occurrences are never capped (resolve guarantees range max >= 1); {0 1} at-most-once scalars are unaffected (no collection). define -help -multiple documents the allocation semantics. New multipleranges.test allocation pins: untyped/typed {3 3} cap incl the cap-beats-type-screen 4-ints case, greedy-up-to-max {1 2}, required-range and pair-clause reservations, leaders-side cap + split floor, pointed over-supply report, legacy-greed-unchanged guard. Full punk/args suite 408/0.
#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.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.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.
#the starved-values seating rule - when the remaining words are exactly what the
#required values need, a dash-led word seats as a value with NO -- marker (the rule
#punk::ansi::grepstr relies on for 'grepstr -h underline -group test-group'; see
#opt_end_of_options_marker above for the -- route and flagvalues.test for the
#declarative @values -flagvalues route)
test opt_dashword_seats_when_values_require_it {a dash-led word seats as a required trailing value without a -- marker when word supply equals values demand}\
#added 2026-08-08 (agent) - directed work: the -type regex|regexp validator's
#'regexp -about' lacked its -- guard, so a legitimate dash-led regex VALUE was
#falsely rejected with regexp's own 'bad option' text embedded in the message
test type_regexp_dashled_value {a dash-led regex value passes -type regexp; a genuinely invalid dash-led regex still fails as a type mismatch with the real compile reason}\
-setup $common -body {
#dash-led but a valid ARE (matches the literal string '-group')
lappend result [dict get [punk::args::parse {-group} withdef @values {re -type regexp}] values]
#dash-led AND invalid: still rejected - and not with regexp's option-parse error