# G-083 punk::args argument relations: strict mutual exclusivity and parsekey-group integrity Status: achieved 2026-08-08 Scope: src/modules/punk/args-999999.0a1.0.tm (resolve/spec compiler, parse paths incl mash, arg_error/synopsis/usage renderers, errorcode vocabulary); src/modules/punk/args/moduledoc/tclcore-999999.0a1.0.tm (lsearch, clock clicks dogfood); src/tests/modules/punk/args/testsuites/args/ + testsuites/dev/ (GAP flips + new characterization) Goal: punk::args definitions can express hard mutual exclusivity - a per-argument -conflicts relation (keyed on parsekeys, checked only against received arguments so defaults never conflict) and an @opts-level strict mode making distinct-member co-occurrence within a shared-parsekey group an error - while legacy last-wins remains the default, so lsearch-class commands (grouped styles with pairwise incompatibilities like -sorted vs -glob/-regexp and -bisect vs -all/-not) parse and document faithfully without combinatorial @form explosion. Acceptance: parse raises a usage-style error with a structured errorcode (PUNKARGS VALIDATION optionconflict shape) naming both offending received arguments for -conflicts violations and strict-group co-occurrence, through both the ordinary and mash option paths with prefix abbreviations resolved before the check; define time rejects a -parsekey colliding with a distinct defined argument's name (parsekey_collides_with_defined_optname_GAP flips); legacy behaviour is untouched by default (full existing punk::args suite passes, including the pinned last-wins/prepend-defaults override idiom); the lsearch moduledoc models its documented incompatibilities with the new vocabulary and drops its "punk::args fixes required for grouped mutually exclusive options" caveat; usage/synopsis output for conflict rules is pinned by characterization tests. ## Context Review finding (2026-07-14): punk::args has no way to express "error if these arguments are combined". What exists today: - `-parsekey` shared keys give SOFT exclusivity by overwrite: last received member wins under the common key. That semantics is deliberate and pinned (parsekey_repeat_ordering: overriding earlier non -multiple options must always be possible - it protects the prepend-defaults idiom `punk::args::parse [list -flag default {*}$userargs]`). - `-group`/`-grouphelp` associate options for usage display only; no parse semantics. - Multiple `@form`s are the only strict mechanism - structural, combinatorially explosive, and they fork the documentation. - The `namespace which` moduledoc entry works around the gap by modelling `-command`/`-variable` as one positional with `literalprefix(...)` choices (exactly-one-of, but only for a fixed position, not free-order flags) and carries `##todo - make mutually exclusive - (separate forms)`. Evidence of need in the tclcore moduledoc alone: - The lsearch definition states: "(documentation incomplete - punk::args fixes required for grouped mutually exclusive options and prefix calculation)". - lsearch `-sorted` is "mutually exclusive with -glob and -regexp" per the man page - yet it shares the -MATCHSTYLE parsekey group with them, whose members are otherwise last-wins. Both semantics coexist within ONE group, so a per-group boolean alone cannot model lsearch. - lsearch `-bisect` "cannot be used with either -all or -not" - a cross-group conflict with no shared value key. - `clock clicks` `-milliseconds`/`-microseconds` carry a definition comment noting they are mutually exclusive and unmodelled. A define-time integrity hole was found in the same review: nothing prevents a `-parsekey` value colliding with a DIFFERENT defined option's name (e.g. `{-a -parsekey -b} {-b}`); the two silently conflate into an implicit shared-key group (pinned by parsekey_collides_with_defined_optname_GAP). ## Approach Design leans (settle and record here during the work): - Per-argument `-conflicts {parsekey-or-flag ...}`: violation checked against RECEIVED arguments only, after prefix/abbreviation resolution, never against defaults. Applies to any pair regardless of grouping. - `@opts`-level strict mode for a shared-parsekey group (name TBD, e.g. `-parsekeymode override|error` or `-groupexclusive`): distinct-member co-occurrence errors; same-member repetition stays G-053 territory (`-multiple {0 1}`), and legacy last-wins remains the default everywhere. - The mash (short-flag bundling) parse path implements its own parsekey logic and must share the conflict check - covered explicitly by acceptance. - Errorcode vocabulary: extend PUNKARGS VALIDATION with an `optionconflict` member mirroring the existing `optionmissing` shape, designed so a later requires-relation slots in alongside. - Usage/synopsis rendering: the group rendering machinery (OPT_GROUPS headers, parsekey hints) is most of the display substrate; conflict rules need a representation there. ## Alternatives considered - Per-group strict boolean only - insufficient: lsearch mixes last-wins and error semantics within one group (see Context). - Modelling exclusivity via `@form` explosion - rejected: combinatorial and forks documentation; the goal exists to avoid this. - Folding into G-053 range-valued -multiple - rejected: G-053 is occurrence arity of ONE argument; this goal is relations BETWEEN arguments. They compose (shared error vocabulary, different axes). ## Notes - `-requires`/`-implies` (dependency relations, e.g. lsearch `-subindices` with `-index`, `-bisect` implying `-sorted`) deliberately deferred - the man page semantics there are mostly soft ("has no effect unless"); design the errorcode vocabulary so they can be added without reshaping it. - Test assets already in place (2026-07-14): characterization pins in src/tests/modules/punk/args/testsuites/args/parsekey.test (mash ordering, prefix abbreviation, collision conflation GAP) ready to flip/extend. - 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 3 (synopsis/usage hints for conflicts/parsekeymode; lsearch + clock clicks moduledoc adoption and dropping the caveat). - 2026-08-07 increment 2 (parse-time enforcement, punk::args 0.20.0): the optionconflict failure class joins PUNKARGS VALIDATION, mirroring optionmissing's shape {optionconflict received }. Enforced in a single post-resolution site in get_dict_form (after the optionmissing/valuemissing block) so the ordinary option path and the mash (short-flag bundling) path share it - both raise identically (verified: -ab mash with -conflicts and with -parsekeymode error both raise). Checked against RECEIVED args only (defaults never conflict, pinned), after prefix/abbreviation resolution, on optset identity (a new optsets_received tracker for options, since flagsreceived collapses shared-parsekey members onto one api_opt). Runs unconditionally - a received conflict is a hard contradiction, not end-of-input exhaustion, so parse_status_classify maps optionconflict to invalid (not incomplete) and the candidacy/viability probe reports it. Define-time check added: a -parsekeymode error group must also declare a non-empty -parsekey. relations.test extended with 7 enforcement pins (raise cases for -conflicts and -parsekeymode error, defaults-never-conflict, cross-group conflicts, one-received-ok, override-legacy last-wins, parse_status invalid). Legacy untouched by default confirmed: full punk/args suite 363 total / 360 passed / 3 skipped / 0 failed via tclsh90s; punk/ns suite 125/125 clean. - Bug found and fixed during increment 2: the group-co-occurrence dedup check initially used `ni` (not-in) where `in` was meant - `ni` returns true on an empty list, so it skipped every member and the check never fired. Corrected to `in`. - 2026-08-08 increment 3 (usage rendering + lsearch moduledoc adoption, punk::args 0.21.0, project 0.59.0): the conflict rules now surface in usage/arg_error and synopsis output, and the lsearch tclcore moduledoc models its documented incompatibilities. Rendering: (a) a per-arg `-conflicts` list appends a 'conflicts with: ' hint to the argument's help text in the usage table (targets resolved to display names via lookup_optset, so a parsekey target shows its member flag, not the raw parsekey); (b) a named @opts group marked -parsekeymode error is annotated 'mutually exclusive (distinct members may not be combined)' in its group header, while override (default) groups carry no such annotation; (c) the synopsis one-line form carries no conflict detail (conflicts are a usage-table concern, not a synopsis-line one). lsearch moduledoc: -sorted gains -conflicts {-glob -regexp} and -bisect gains -conflicts {-all -not} (per-arg conflicts, the whole group stays -parsekeymode override so -glob/-regexp remain last-wins); the '(documentation incomplete - punk::args fixes required for grouped mutually exclusive options and prefix calculation)' caveat is dropped from @cmd -help. clock clicks left unchanged - its active positional-choice definition already models exclusivity (exactly-one-of) and the acceptance clause names lsearch only. relations.test extended with 8 increment-3 pins (4 rendering: conflicts_usage_hint, parsekeymode_error_usage_group_header, parsekeymode_override_no_header_annotation, conflicts_synopsis_no_hint; 4 lsearch adoption: lsearch_sorted_conflicts_glob + lsearch_bisect_conflicts_all raise optionconflict via errorcode substring match, lsearch_glob_regexp_last_wins stays last-wins, lsearch_caveat_dropped). Legacy untouched by default confirmed: full punk/args suite 371 total / 368 passed / 3 skipped / 0 failed via tclsh90s; tclcoreparity 10/10; punk/ns 125/125. All G-083 acceptance criteria met. ## Follow-ons Follow-on: -requires/-implies dependency relations (e.g lsearch -subindices with -index, -bisect implying -sorted) - deliberately deferred during G-083 (the man-page semantics are mostly soft "has no effect unless"); the optionconflict errorcode vocabulary was shaped so a later requires-relation slots in alongside without reshaping it => open