# Getopt::Yath Refactoring Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the internal option parsing/settings engine with Getopt::Yath while keeping App::Yath::Options, App::Yath::Option, Test2::Harness::Settings, and Test2::Harness::Settings::Prefix as backwards-compatible wrappers for downstream plugins.

**Architecture:** The refactoring proceeds in layers: (1) Add Getopt::Yath as a dependency, (2) Convert Settings/Settings::Prefix to thin wrappers around Getopt::Yath::Settings/Settings::Group, (3) Convert all Options definition files to use Getopt::Yath DSL, (4) Convert App::Yath::Options to delegate to Getopt::Yath::Instance for parsing, (5) Update all consumers to use Getopt::Yath::Settings API, (6) Write compatibility tests.

**Tech Stack:** Perl 5, Getopt::Yath (from ../Getopt-Yath), Test2::V0

**Key API Mappings:**
- `Settings::Prefix` → `Getopt::Yath::Settings::Group` (both have AUTOLOAD, lvalue accessors)
- `Settings` → `Getopt::Yath::Settings` (both have AUTOLOAD for group/prefix access)
- `vivify_field($f)` → `option_ref($f, 1)` (both return scalar refs)
- `define_prefix($p)` → `group($p, 1)` (both auto-create)
- `check_field($f)` → `check_option($f)` (both return bool)
- `field($f)` → `option($f)` (both lvalue)
- Option types: `b`→`Bool`, `s`→`Scalar`, `c`→`Count`, `m`→`List`, `h`→`Map`, `H`→`Map` (with split), `d`→`Auto`, `D`→`AutoList`

---

### Task 1: Add Getopt::Yath as a dependency

**Files:**
- Modify: `dist.ini`
- Modify: `Makefile.PL`
- Modify: `cpanfile`

- [ ] **Step 1: Add Getopt::Yath to dist.ini prereqs**

In `dist.ini`, under `[Prereqs]`, add:

```
Getopt::Yath               = 0.001001
```

- [ ] **Step 2: Regenerate Makefile.PL and cpanfile**

Run: `dzil build` then copy the generated files, or manually add the dependency to Makefile.PL and cpanfile in the same pattern as existing deps.

- [ ] **Step 3: Verify Getopt::Yath is loadable**

Run: `perl -MGetopt::Yath -e 'print "OK\n"'`
Expected: `OK`

- [ ] **Step 4: Commit**

```bash
git add dist.ini Makefile.PL cpanfile
git commit -m "Add Getopt::Yath as a dependency"
```

---

### Task 2: Convert Settings::Prefix to wrapper around Settings::Group

The key insight: both `Settings::Prefix` and `Getopt::Yath::Settings::Group` have AUTOLOAD-based lvalue accessors, so they're nearly API-compatible. We make Settings::Prefix inherit from Settings::Group and add compatibility methods.

**Files:**
- Modify: `lib/Test2/Harness/Settings/Prefix.pm`
- Create: `t/unit/Test2/Harness/Settings/Prefix_compat.t`

- [ ] **Step 1: Write compatibility test**

```perl
# t/unit/Test2/Harness/Settings/Prefix_compat.t
use Test2::V0;
use Test2::Harness::Settings::Prefix;

# Test construction
my $p = Test2::Harness::Settings::Prefix->new(foo => 1, bar => 'hello');

# Test field access via AUTOLOAD
is($p->foo, 1, "AUTOLOAD accessor works");
is($p->bar, 'hello', "AUTOLOAD accessor works for strings");

# Test lvalue
$p->foo = 42;
is($p->foo, 42, "lvalue assignment works");

# Test vivify_field returns scalar ref
my $ref = $p->vivify_field('new_field');
is(ref($ref), 'SCALAR', "vivify_field returns scalar ref");
$$ref = 'created';
is($p->new_field, 'created', "vivify_field creates accessible field");

# Test vivify_field on existing field
my $ref2 = $p->vivify_field('foo');
is($$ref2, 42, "vivify_field returns ref to existing field");

# Test check_field
ok($p->check_field('foo'), "check_field returns true for existing field");
ok(!$p->check_field('nonexistent'), "check_field returns false for missing field");

# Test field() method
is($p->field('foo'), 42, "field() getter works");
$p->field('foo', 99);
is($p->field('foo'), 99, "field() setter works");

# Test remove_field
$p->remove_field('bar');
ok(!$p->check_field('bar'), "remove_field removes the field");

# Test TO_JSON
my $json = $p->TO_JSON;
is(ref($json), 'HASH', "TO_JSON returns hashref");
is($json->{foo}, 99, "TO_JSON has correct values");

# Test build()
{
    package My::Test::Buildable;
    sub new { my $c = shift; bless {@_}, $c }
}
my $bp = Test2::Harness::Settings::Prefix->new(x => 1, y => 2);
my $obj = $bp->build('My::Test::Buildable', z => 3);
isa_ok($obj, 'My::Test::Buildable');
is($obj->{x}, 1, "build passes prefix fields");
is($obj->{z}, 3, "build passes extra args");

done_testing;
```

- [ ] **Step 2: Run test to verify it fails**

Run: `perl -Ilib t/unit/Test2/Harness/Settings/Prefix_compat.t`

- [ ] **Step 3: Rewrite Settings::Prefix as a wrapper**

Replace `lib/Test2/Harness/Settings/Prefix.pm` with a wrapper that inherits from `Getopt::Yath::Settings::Group` and adds the legacy API methods (`vivify_field`, `check_field`, `field`, `remove_field`, `build`).

Key implementation:

```perl
package Test2::Harness::Settings::Prefix;
use strict;
use warnings;

our $VERSION = '1.000168';

use parent 'Getopt::Yath::Settings::Group';

# Compat: vivify_field returns a scalar ref (option_ref with create=1)
sub vivify_field {
    my $self = shift;
    my ($field) = @_;
    return $self->option_ref($field, 1);
}

# Compat: check_field delegates to check_option
sub check_field {
    my $self = shift;
    my ($field) = @_;
    return $self->check_option($field);
}

# Compat: field delegates to option (lvalue)
sub field :lvalue {
    my $self = shift;
    if (@_ > 1) {
        my ($field, $val) = @_;
        $self->{$field} = $val;
        return $self->{$field};
    }
    my ($field) = @_;
    $self->{$field};
}

# Compat: remove_field delegates to delete_option
sub remove_field {
    my $self = shift;
    my ($field) = @_;
    $self->delete_option($field);
}

# Compat: build() creates an instance of a class using prefix fields as constructor args
sub build {
    my $self = shift;
    my ($class, @args) = @_;
    return $class->new(%{$self->TO_JSON}, @args);
}

1;
```

Note: `Getopt::Yath::Settings::Group` already provides `AUTOLOAD :lvalue`, `new()`, `option()`, `create_option()`, `option_ref()`, `check_option()`, `delete_option()`, `all()`, `TO_JSON()`. We only need to add the legacy method names as aliases.

- [ ] **Step 4: Run test to verify it passes**

Run: `perl -Ilib t/unit/Test2/Harness/Settings/Prefix_compat.t`
Expected: All tests pass

- [ ] **Step 5: Run existing Settings::Prefix tests**

Run: `perl -Ilib t/unit/Test2/Harness/Settings/Prefix.t` (if it exists)
Expected: All tests pass

- [ ] **Step 6: Commit**

```bash
git add lib/Test2/Harness/Settings/Prefix.pm t/unit/Test2/Harness/Settings/Prefix_compat.t
git commit -m "Convert Settings::Prefix to wrapper around Getopt::Yath::Settings::Group"
```

---

### Task 3: Convert Settings to wrapper around Getopt::Yath::Settings

**Files:**
- Modify: `lib/Test2/Harness/Settings.pm`
- Create: `t/unit/Test2/Harness/Settings_compat.t`

- [ ] **Step 1: Write compatibility test**

```perl
# t/unit/Test2/Harness/Settings_compat.t
use Test2::V0;
use Test2::Harness::Settings;

# Test empty construction
my $s = Test2::Harness::Settings->new();
isa_ok($s, 'Test2::Harness::Settings');

# Test construction with initial values
my $s2 = Test2::Harness::Settings->new(
    harness => { script => 'test.pl', start => 1234 },
    display => { verbose => 1 },
);

# Test AUTOLOAD access
is($s2->harness->script, 'test.pl', "Nested AUTOLOAD access works");
is($s2->display->verbose, 1, "Another prefix works");

# Test define_prefix
my $group = $s->define_prefix('runner');
isa_ok($group, 'Test2::Harness::Settings::Prefix');

# Test define_prefix returns same object on second call
my $group2 = $s->define_prefix('runner');
is("$group", "$group2", "define_prefix returns same object");

# Test prefix()
my $g = $s->prefix('runner');
is("$g", "$group", "prefix() returns the defined prefix");

# Test check_prefix
ok($s->check_prefix('runner'), "check_prefix true for defined prefix");
ok(!$s->check_prefix('nonexistent'), "check_prefix false for missing");

# Test vivify_field via define_prefix
my $ref = $s->define_prefix('harness')->vivify_field('script');
$$ref = 'yath';
is($s->harness->script, 'yath', "vivify_field through define_prefix works");

# Test TO_JSON
my $json = $s2->TO_JSON;
is(ref($json), 'HASH', "TO_JSON returns hashref");
is($json->{harness}->{script}, 'test.pl', "TO_JSON nested values correct");

done_testing;
```

- [ ] **Step 2: Run test to verify it fails**

Run: `perl -Ilib t/unit/Test2/Harness/Settings_compat.t`

- [ ] **Step 3: Rewrite Settings as a wrapper**

Replace `lib/Test2/Harness/Settings.pm`. It inherits from `Getopt::Yath::Settings` and:
- Overrides `new()` to handle the legacy constructor pattern (including JSON file loading)
- Wraps values as `Test2::Harness::Settings::Prefix` instead of bare `Settings::Group`
- Adds `define_prefix()`, `prefix()`, `check_prefix()` as aliases for `group()`, `check_group()`
- Preserves the `build()` method

Key implementation:

```perl
package Test2::Harness::Settings;
use strict;
use warnings;

our $VERSION = '1.000168';

use Carp qw/croak/;
use Test2::Harness::Settings::Prefix;

use parent 'Getopt::Yath::Settings';

sub new {
    my $class = shift;

    # Handle JSON file loading (legacy feature)
    if (@_ == 1 && !ref($_[0])) {
        my $file = shift;
        require Test2::Harness::Util::JSON;
        my $hash = Test2::Harness::Util::JSON::decode_json(
            Test2::Harness::Util::JSON::read_json_file($file)
        );
        return $class->new(%$hash);
    }

    my %params = @_ == 1 ? %{$_[0]} : @_;

    # Wrap all values as Settings::Prefix objects
    for my $key (keys %params) {
        my $val = $params{$key};
        if (ref($val) eq 'HASH') {
            $params{$key} = Test2::Harness::Settings::Prefix->new(%$val);
        }
        elsif (!blessed($val) || !$val->isa('Test2::Harness::Settings::Prefix')) {
            croak "Settings value for '$key' must be a hashref or Settings::Prefix instance";
        }
    }

    my $self = bless \%params, $class;
    return $self;
}

# Override group() to return Settings::Prefix instances
sub group {
    my $self = shift;
    my ($name, $vivify) = @_;

    if ($vivify && !exists $self->{$name}) {
        $self->{$name} = Test2::Harness::Settings::Prefix->new();
    }

    return $self->{$name};
}

# Legacy aliases
sub define_prefix { $_[0]->group($_[1], 1) }
sub prefix        { $_[0]->group($_[1]) }
sub check_prefix  { exists $_[0]->{$_[1]} }

# Legacy build method
sub build {
    my $self = shift;
    my ($prefix, $class, @args) = @_;
    my $p = $self->prefix($prefix) or croak "No prefix '$prefix' defined";
    return $p->build($class, @args);
}

1;
```

- [ ] **Step 4: Run test to verify it passes**

Run: `perl -Ilib t/unit/Test2/Harness/Settings_compat.t`
Expected: All tests pass

- [ ] **Step 5: Run existing Settings tests**

Run: `perl -Ilib t/unit/Test2/Harness/Settings.t`
Expected: All tests pass

- [ ] **Step 6: Run full test suite to verify nothing broke**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`
Expected: All tests pass

- [ ] **Step 7: Commit**

```bash
git add lib/Test2/Harness/Settings.pm t/unit/Test2/Harness/Settings_compat.t
git commit -m "Convert Settings to wrapper around Getopt::Yath::Settings"
```

---

### Task 4: Convert Options definition files to Getopt::Yath DSL

This is the largest task. Each `App::Yath::Options::*` file needs to be converted from the old `option()` DSL to `Getopt::Yath`'s DSL. The conversion pattern is:

**Import change:**
```perl
# Old:
use App::Yath::Options;

# New:
use Getopt::Yath;
use App::Yath::Options; # Keep for backwards compat - provides options() export
```

Wait — actually, since `App::Yath::Options` is becoming a wrapper and we want full Getopt::Yath DSL, the cleanest approach is to have the Options files `use Getopt::Yath` for the DSL and have `App::Yath::Options` provide a compatibility layer that can include Getopt::Yath-based options.

**Files to convert (10 files):**
- `lib/App/Yath/Options/PreCommand.pm`
- `lib/App/Yath/Options/Display.pm`
- `lib/App/Yath/Options/Runner.pm`
- `lib/App/Yath/Options/Debug.pm`
- `lib/App/Yath/Options/Finder.pm`
- `lib/App/Yath/Options/Logging.pm`
- `lib/App/Yath/Options/Run.pm`
- `lib/App/Yath/Options/Workspace.pm`
- `lib/App/Yath/Options/Collector.pm`
- `lib/App/Yath/Options/Persist.pm`

**Plus 4 plugin files:**
- `lib/App/Yath/Plugin/Cover.pm`
- `lib/App/Yath/Plugin/Notify.pm`
- `lib/App/Yath/Plugin/Git.pm`
- `lib/App/Yath/Plugin/YathUI.pm`

**Type conversion table:**
| Old | New | Notes |
|-----|-----|-------|
| `b` or no type (bool) | `Bool` | |
| `s` | `Scalar` | |
| `c` | `Count` | |
| `m` | `List` | |
| `h` | `Map` | |
| `H` | `Map` | With `split_on => ','` |
| `d` | `Auto` | With `autofill => 1` |
| `D` | `AutoList` | With `autofill` |

**Field conversion table:**
| Old field | New field | Notes |
|-----------|-----------|-------|
| `prefix` | `group` | |
| `env_vars` | `from_env_vars` | |
| `clear_env_vars` | `clear_env_vars` | Same |
| `alt` | `alt` | Same |
| `long_examples` | `long_examples` | Same |
| `short_examples` | `short_examples` | Same |
| `normalize` | `normalize` | Same |
| `applicable` | `applicable` | Same — but signature may differ, verify |

**Action callback conversion:**
Most `action` callbacks manipulate `$$slot` directly. In Getopt::Yath, use `trigger` or `normalize` instead where possible. Complex callbacks that need access to the full settings/options objects should use `option_post_process`.

**Post callback conversion:**
```perl
# Old:
post $weight => sub {
    my %params = @_;  # options, args, settings, command
    ...
};

# New:
option_post_process $weight => sub {
    my ($instance, $state) = @_;
    my $settings = $state->{settings};
    ...
};
```

Each file conversion follows this pattern. Convert them one at a time, running the test suite after each to catch regressions. Due to the massive scope, each Options file should be its own subtask/commit.

- [ ] **Step 1: Convert Options::Workspace.pm** (simplest, only 3 options)

- [ ] **Step 2: Convert Options::Collector.pm** (simple, 2 options)

- [ ] **Step 3: Convert Options::Persist.pm** (simple, 1 option)

- [ ] **Step 4: Convert Options::Logging.pm** (moderate, 6 options)

- [ ] **Step 5: Convert Options::Run.pm** (moderate, 18 options)

- [ ] **Step 6: Convert Options::Display.pm** (moderate, 2 groups)

- [ ] **Step 7: Convert Options::Debug.pm** (complex, multiple post callbacks)

- [ ] **Step 8: Convert Options::Runner.pm** (complex, many options and callbacks)

- [ ] **Step 9: Convert Options::PreCommand.pm** (complex, dynamic plugin loading)

- [ ] **Step 10: Convert Options::Finder.pm** (most complex, dynamic options, builds)

- [ ] **Step 11: Convert Plugin::Git.pm**

- [ ] **Step 12: Convert Plugin::Cover.pm**

- [ ] **Step 13: Convert Plugin::Notify.pm**

- [ ] **Step 14: Convert Plugin::YathUI.pm**

- [ ] **Step 15: Run full test suite after all conversions**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`

- [ ] **Step 16: Commit all conversions**

---

### Task 5: Convert App::Yath::Options to delegate to Getopt::Yath

After all definition files use Getopt::Yath DSL, `App::Yath::Options` becomes a wrapper that:
1. Still exports `option()`, `option_group()`, `include_options()`, `post()`, `options()` for backwards compatibility (downstream plugins)
2. Internally delegates to a `Getopt::Yath::Instance` for parsing
3. The `grab_pre_command_opts` / `grab_command_opts` / `process_*` flow now uses Getopt::Yath's `parse_options` with appropriate stop/skip params

**Files:**
- Modify: `lib/App/Yath/Options.pm`
- Modify: `lib/App/Yath/Option.pm`
- Create: `t/unit/App/Yath/Options_compat.t`

This is the most intricate part. The wrapper must translate the multi-phase parsing approach:

```perl
# Phase 1: grab_pre_command_opts → parse_options with stop_at_non_opts => 1
# Phase 2: process_pre_command_opts → already processed by parse_options
# Phase 3: grab_command_opts → parse_options consuming remaining
# Phase 4: process_command_opts → already processed
# Phase 5: process_option_post_actions → run Getopt::Yath post_process callbacks
```

- [ ] **Step 1: Write compatibility test for Options**

Test that `use App::Yath::Options` still works in a package, that `option()`, `option_group()`, `include_options()`, `post()`, `options()` are exported, and that basic parsing works.

- [ ] **Step 2: Rewrite App::Yath::Options as wrapper**

- [ ] **Step 3: Rewrite App::Yath::Option as wrapper**

Convert to thin wrapper around `Getopt::Yath::Option` with legacy method aliases.

- [ ] **Step 4: Run compatibility test**

- [ ] **Step 5: Run full test suite**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`

- [ ] **Step 6: Commit**

---

### Task 6: Update App::Yath.pm to use Getopt::Yath::Settings API

**Files:**
- Modify: `lib/App/Yath.pm`

The main change: replace `define_prefix()->vivify_field()` pattern with `group(name, 1)->option_ref(field, 1)` or just use the Settings::Prefix compat methods (since Settings is now a wrapper).

Actually, since Settings and Settings::Prefix are now wrappers, App::Yath.pm should continue to work unchanged. Verify with tests. If the wrapper is correct, this task may be a no-op.

- [ ] **Step 1: Run full test suite to verify App::Yath works through the wrappers**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`

- [ ] **Step 2: Optionally update App::Yath.pm to use Getopt::Yath API directly**

Replace:
```perl
${$self->{+SETTINGS}->define_prefix('harness')->vivify_field('script')} //= clean_path($caller[1]);
```
With:
```perl
${$self->{+SETTINGS}->group('harness', 1)->option_ref('script', 1)} //= clean_path($caller[1]);
```

- [ ] **Step 3: Commit if changes made**

---

### Task 7: Update all Settings consumer files

Update all 35+ files that access `$settings->prefix->field` to use `Getopt::Yath::Settings` API. Since both old and new use AUTOLOAD, `$settings->runner->job_count` works identically — this task may largely be verifying everything works rather than making changes.

Files to verify/update (check if any use methods not provided by the wrapper):
- `lib/Test2/Harness/Runner.pm`
- `lib/Test2/Harness/Finder.pm`
- `lib/Test2/Harness/Collector.pm`
- `lib/Test2/Harness/Runner/State.pm`
- `lib/Test2/Harness/Runner/Job.pm`
- `lib/Test2/Harness/Runner/Resource/*.pm`
- `lib/Test2/Harness/Renderer/Formatter.pm`
- `lib/App/Yath/Command.pm` and all Command subclasses
- `lib/App/Yath/Plugin/*.pm`

- [ ] **Step 1: Run full test suite**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`

- [ ] **Step 2: Fix any failures from API differences**

- [ ] **Step 3: Commit**

---

### Task 8: Write comprehensive compatibility tests

**Files:**
- Create: `t/compat/options_compat.t`

Test that the old plugin-facing API still works end-to-end:

```perl
# Test that a package using the old-style option definitions
# can still define options, have them parsed, and access results
package TestPlugin;
use App::Yath::Options;

option_group {prefix => 'test', category => 'Test'} => sub {
    option verbose => (type => 'b', description => 'Be verbose');
    option count   => (type => 'c', description => 'Count');
    option name    => (type => 's', description => 'Name', default => 'default');
    option items   => (type => 'm', description => 'Items');
    option pairs   => (type => 'h', description => 'Pairs');
};

package main;
use Test2::V0;

# Verify option definitions were created
my $opts = TestPlugin->options;
ok($opts, "Got options instance");

# Verify settings work
my $settings = $opts->settings;
ok($settings, "Got settings");

# ... etc
done_testing;
```

- [ ] **Step 1: Write and run compatibility tests**
- [ ] **Step 2: Fix any issues found**
- [ ] **Step 3: Commit**

---

### Task 9: Final verification and cleanup

- [ ] **Step 1: Run full test suite**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t -j24`

- [ ] **Step 2: Run integration tests specifically**

Run: `AUTHOR_TESTING=1 prove -Ilib -r t/integration -j24`

- [ ] **Step 3: Verify no leftover references to old internal APIs**

Search for any remaining direct uses of old internal methods that should have been converted.

- [ ] **Step 4: Final commit**

---

## Notes on Complexity

This refactoring is extremely large. The recommended execution order is:

1. **Tasks 1-3** (dependency + Settings wrappers) — Can be done independently and verified immediately. These are the foundation.

2. **Task 4** (Options definition conversion) — This is the bulk of the work. Each file should be converted and tested individually. Start with the simplest files to establish the pattern.

3. **Task 5** (Options.pm wrapper) — This is the most intricate piece and depends on Task 4 being complete.

4. **Tasks 6-9** (consumers, compat tests, cleanup) — These verify everything works together.

The critical risk is in Task 5 where the multi-phase parsing logic must be preserved. The `grab_pre_command_opts` / `grab_command_opts` / `process_*` flow is deeply embedded in `App::Yath.pm` and must continue to work identically.
