package Developer::Dashboard::PathRegistry;

use strict;
use warnings;

our $VERSION = '2.21';

use Cwd qw(abs_path cwd);
use File::Basename qw(dirname);
use File::Find ();
use File::Path qw(make_path);
use File::Spec;

# new(%args)
# Constructs the logical path registry for the runtime.
# Input: home, app_name, workspace_roots, project_roots, and named_paths.
# Output: Developer::Dashboard::PathRegistry object.
sub new {
    my ( $class, %args ) = @_;

    my $home = $args{home} || $ENV{HOME} || die 'Missing home directory';

    my $self = bless {
        home            => $home,
        app_name        => $args{app_name} || 'developer-dashboard',
        workspace_roots => $args{workspace_roots} || [],
        project_roots   => $args{project_roots} || [],
        named_paths     => $args{named_paths} || {},
    }, $class;

    return $self;
}

# home()
# Returns the configured home directory.
# Input: none.
# Output: home directory path string.
sub home { $_[0]->{home} }

# app_name()
# Returns the runtime application name.
# Input: none.
# Output: application name string.
sub app_name { $_[0]->{app_name} }

# register_named_paths($paths)
# Registers additional logical path aliases.
# Input: hash reference of alias-to-path mappings.
# Output: registry object.
sub register_named_paths {
    my ( $self, $paths ) = @_;
    return $self if ref($paths) ne 'HASH';
    for my $name ( keys %$paths ) {
        next if !defined $name || $name eq '';
        $self->{named_paths}{$name} = $paths->{$name};
    }
    return $self;
}

# unregister_named_path($name)
# Removes a logical path alias from the in-memory registry when present.
# Input: alias name string.
# Output: registry object.
sub unregister_named_path {
    my ( $self, $name ) = @_;
    return $self if !defined $name || $name eq '';
    delete $self->{named_paths}{$name};
    return $self;
}

# named_paths()
# Returns the currently registered logical path aliases.
# Input: none.
# Output: hash reference of alias-to-path mappings.
sub named_paths {
    my ($self) = @_;
    return { %{ $self->{named_paths} || {} } };
}

# runtime_root()
# Returns the effective runtime root directory.
# Input: none.
# Output: deepest discovered runtime layer directory, falling back to the home runtime directory.
sub runtime_root {
    my ($self) = @_;
    my @layers = $self->runtime_layers;
    return $layers[-1] || $self->home_runtime_root;
}

# home_runtime_root()
# Returns the home-backed runtime root directory.
# Input: none.
# Output: home runtime directory path string.
sub home_runtime_root {
    my ($self) = @_;
    return $self->_ensure_dir( $self->home_runtime_path );
}

# home_runtime_path()
# Returns the canonical home-backed runtime root path without creating it.
# Input: none.
# Output: home runtime directory path string.
sub home_runtime_path {
    my ($self) = @_;
    return File::Spec->catdir( $self->home, '.developer-dashboard' );
}

# project_runtime_root()
# Returns the active project-local runtime root when the project already contains one.
# Input: none.
# Output: project-local runtime directory path string or undef when absent.
sub project_runtime_root {
    my ($self) = @_;
    my $repo = $self->current_project_root or return;
    my $home_runtime = File::Spec->catdir( $self->home, '.developer-dashboard' );
    return if $repo eq $home_runtime;
    my $root = File::Spec->catdir( $repo, '.developer-dashboard' );
    return -d $root ? $root : undef;
}

# runtime_roots()
# Returns the effective runtime roots in lookup order.
# Input: none.
# Output: ordered list of runtime root directory path strings from deepest layer to home fallback.
sub runtime_roots {
    my ($self) = @_;
    return reverse $self->runtime_layers;
}

# runtime_layers()
# Returns the effective runtime roots in inheritance order from home to the
# current working directory layer.
# Input: none.
# Output: ordered list of runtime root directory path strings from home to deepest layer.
sub runtime_layers {
    my ($self) = @_;
    my @roots;
    my %seen;
    for my $root ( $self->_runtime_layers_from_env, $self->home_runtime_root, $self->_ancestor_runtime_layers ) {
        next if !defined $root || $root eq '';
        my $identity = $self->_path_identity($root);
        next if $seen{$identity}++;
        push @roots, $root;
    }
    return @roots;
}

# state_root()
# Returns the state root directory.
# Input: none.
# Output: directory path string.
sub state_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'state' ) );
}

# cache_root()
# Returns the cache root directory.
# Input: none.
# Output: directory path string.
sub cache_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'cache' ) );
}

# logs_root()
# Returns the logs root directory.
# Input: none.
# Output: directory path string.
sub logs_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'logs' ) );
}

# dashboards_root()
# Returns the dashboards/bookmarks root directory.
# Input: none.
# Output: directory path string.
sub dashboards_root {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_BOOKMARKS} ) {
        return $self->_ensure_dir( $self->_expand_home($dir) );
    }
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'dashboards' ) );
}

# dashboards_roots()
# Returns the bookmark roots in effective lookup order.
# Input: none.
# Output: ordered list of bookmark root directory path strings.
sub dashboards_roots {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_BOOKMARKS} ) {
        return ( $self->_ensure_dir( $self->_expand_home($dir) ) );
    }
    return map { File::Spec->catdir( $_, 'dashboards' ) } $self->runtime_roots;
}

# dashboards_layers()
# Returns the bookmark roots in inheritance order from home to deepest layer.
# Input: none.
# Output: ordered list of bookmark root directory path strings.
sub dashboards_layers {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_BOOKMARKS} ) {
        return ( $self->_ensure_dir( $self->_expand_home($dir) ) );
    }
    return map { File::Spec->catdir( $_, 'dashboards' ) } $self->runtime_layers;
}

# bookmarks()
# Returns the older bookmark directory alias.
# Input: none.
# Output: directory path string.
sub bookmarks {
    my ($self) = @_;
    return $self->dashboards_root;
}

# bookmarks_root()
# Returns the older bookmark root alias.
# Input: none.
# Output: directory path string.
sub bookmarks_root {
    my ($self) = @_;
    return $self->dashboards_root;
}

# cli_root()
# Returns the user CLI extension directory.
# Input: none.
# Output: directory path string.
sub cli_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'cli' ) );
}

# cli_roots()
# Returns the CLI extension roots in effective lookup order.
# Input: none.
# Output: ordered list of CLI root directory path strings.
sub cli_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'cli' ) } $self->runtime_roots;
}

# cli_layers()
# Returns the CLI extension roots in inheritance order from home to deepest
# layer.
# Input: none.
# Output: ordered list of CLI root directory path strings.
sub cli_layers {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'cli' ) } $self->runtime_layers;
}

# skills_root()
# Returns the isolated root that contains installed dashboard skills.
# Input: none.
# Output: directory path string.
sub skills_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->home_runtime_root, 'skills' ) );
}

# skill_root($name)
# Returns the isolated root directory for one installed skill.
# Input: skill repository name string.
# Output: directory path string.
sub skill_root {
    my ( $self, $name ) = @_;
    die 'Missing skill name' if !defined $name || $name eq '';
    return $self->_ensure_dir( File::Spec->catdir( $self->skills_root, $name ) );
}

# collectors_root()
# Returns the collectors state root directory.
# Input: none.
# Output: directory path string.
sub collectors_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->state_root, 'collectors' ) );
}

# collectors_roots()
# Returns the collector state roots in lookup order from deepest layer to home.
# Input: none.
# Output: ordered list of collector state root directory path strings.
sub collectors_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'state', 'collectors' ) } $self->runtime_roots;
}

# indicators_root()
# Returns the indicators state root directory.
# Input: none.
# Output: directory path string.
sub indicators_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->state_root, 'indicators' ) );
}

# indicators_roots()
# Returns the indicator state roots in lookup order from deepest layer to home.
# Input: none.
# Output: ordered list of indicator state root directory path strings.
sub indicators_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'state', 'indicators' ) } $self->runtime_roots;
}

# sessions_root()
# Returns the sessions state root directory.
# Input: none.
# Output: directory path string.
sub sessions_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->state_root, 'sessions' ) );
}

# sessions_roots()
# Returns the session storage roots in effective lookup order.
# Input: none.
# Output: ordered list of session root directory path strings.
sub sessions_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'state', 'sessions' ) } $self->runtime_roots;
}

# temp_root()
# Returns the runtime temporary directory.
# Input: none.
# Output: directory path string.
sub temp_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'tmp' ) );
}

# config_root()
# Returns the configuration root directory.
# Input: none.
# Output: directory path string.
sub config_root {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_CONFIGS} ) {
        return $self->_ensure_dir( $self->_expand_home($dir) );
    }
    return $self->_ensure_dir( File::Spec->catdir( $self->runtime_root, 'config' ) );
}

# config_roots()
# Returns the configuration roots in effective lookup order.
# Input: none.
# Output: ordered list of configuration root directory path strings.
sub config_roots {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_CONFIGS} ) {
        return ( $self->_ensure_dir( $self->_expand_home($dir) ) );
    }
    return map { File::Spec->catdir( $_, 'config' ) } $self->runtime_roots;
}

# config_layers()
# Returns the configuration roots in inheritance order from home to deepest
# layer.
# Input: none.
# Output: ordered list of configuration root directory path strings.
sub config_layers {
    my ($self) = @_;
    if ( my $dir = $ENV{DEVELOPER_DASHBOARD_CONFIGS} ) {
        return ( $self->_ensure_dir( $self->_expand_home($dir) ) );
    }
    return map { File::Spec->catdir( $_, 'config' ) } $self->runtime_layers;
}

# auth_root()
# Returns the auth configuration directory.
# Input: none.
# Output: directory path string.
sub auth_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->config_root, 'auth' ) );
}

# auth_roots()
# Returns the auth configuration roots in effective lookup order.
# Input: none.
# Output: ordered list of auth root directory path strings.
sub auth_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'auth' ) } $self->config_roots;
}

# repo_dashboard_root()
# Returns the repo-local dashboard root for the active project.
# Input: none.
# Output: directory path string or undef.
sub repo_dashboard_root {
    my ($self) = @_;
    return $self->project_runtime_root;
}

# users_root()
# Returns the helper user storage directory.
# Input: none.
# Output: directory path string.
sub users_root {
    my ($self) = @_;
    return $self->_ensure_dir( File::Spec->catdir( $self->auth_root, 'users' ) );
}

# users_roots()
# Returns the helper-user roots in effective lookup order.
# Input: none.
# Output: ordered list of user storage directory path strings.
sub users_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'users' ) } $self->auth_roots;
}

# runtime_local_lib_roots()
# Returns the runtime-local Perl library roots in lookup order from deepest
# layer to home.
# Input: none.
# Output: ordered list of runtime-local perl5 directory paths.
sub runtime_local_lib_roots {
    my ($self) = @_;
    return map { File::Spec->catdir( $_, 'local', 'lib', 'perl5' ) } $self->runtime_roots;
}

# current_project_root()
# Resolves the current git project root from the current working directory.
# Input: none.
# Output: directory path string or undef.
sub current_project_root {
    my ($self) = @_;
    return $self->project_root_for( cwd() );
}

# project_root_for($start_dir)
# Resolves the nearest git project root for a starting directory.
# Input: starting directory path.
# Output: directory path string or undef.
sub project_root_for {
    my ( $self, $start_dir ) = @_;

    my $dir = $start_dir || cwd();

    while ($dir) {
        return $dir if -d File::Spec->catdir( $dir, '.git' );

        my $parent = dirname($dir);
        last if !$parent || $parent eq $dir;
        $dir = $parent;
    }

    return;
}

# workspace_roots()
# Returns the configured workspace search roots.
# Input: none.
# Output: list of directory path strings.
sub workspace_roots {
    my ($self) = @_;
    return @{ $self->{workspace_roots} };
}

# project_roots()
# Returns the configured project search roots.
# Input: none.
# Output: list of directory path strings.
sub project_roots {
    my ($self) = @_;
    return @{ $self->{project_roots} };
}

# resolve_dir($name)
# Resolves a logical directory name or absolute path.
# Input: logical directory name or absolute path.
# Output: resolved directory path string.
sub resolve_dir {
    my ( $self, $name ) = @_;

    die 'Missing path name' if !defined $name || $name eq '';

    return $name if File::Spec->file_name_is_absolute($name);

    return $self->$name() if $self->can($name);

    if ( exists $self->{named_paths}{$name} ) {
        my $path = $self->{named_paths}{$name};
        $path = $self->_expand_home($path);
        return $path;
    }

    die "Unknown directory name '$name'";
}

# resolve_any(@names)
# Resolves the first existing directory among several logical names.
# Input: list of logical directory names.
# Output: directory path string or undef.
sub resolve_any {
    my ( $self, @names ) = @_;
    for my $name (@names) {
        my $path = eval { $self->resolve_dir($name) };
        next if !$path || !-d $path;
        return $path;
    }
    return;
}

# ls($name)
# Lists child paths under a resolved directory.
# Input: logical directory name.
# Output: sorted list of child path strings.
sub ls {
    my ( $self, $name ) = @_;
    my $dir = $self->resolve_dir($name);
    return if !-d $dir;

    opendir my $dh, $dir or die "Unable to open $dir: $!";
    my @items;
    while ( my $entry = readdir $dh ) {
        next if $entry eq '.' || $entry eq '..';
        push @items, File::Spec->catfile( $dir, $entry );
    }
    closedir $dh;

    return sort @items;
}

# with_dir($name, $code)
# Temporarily changes into a resolved directory while executing a callback.
# Input: logical directory name and code reference.
# Output: callback return value.
sub with_dir {
    my ( $self, $name, $code ) = @_;
    my $dir = $self->resolve_dir($name);
    my $old = cwd();
    chdir $dir or die "Unable to chdir to $dir: $!";
    my @result = eval { $code->($dir) };
    my $error = $@;
    chdir $old or die "Unable to restore cwd to $old: $!";
    die $error if $error;
    return wantarray ? @result : $result[0];
}

# locate_projects(@terms)
# Fuzzy-matches projects under configured roots.
# Input: list of search term strings.
# Output: list of matched project directory paths.
sub locate_projects {
    my ( $self, @terms ) = @_;

    my @roots = grep { defined && -d } ( $self->workspace_roots, $self->project_roots );
    my @found;
    my %seen;

    for my $root (@roots) {
        opendir my $dh, $root or next;
        while ( my $entry = readdir $dh ) {
            next if $entry =~ /^\./;
            my $path = File::Spec->catdir( $root, $entry );
            next if !-d $path;

            my $ok = 1;
            for my $term (@terms) {
                next if !defined $term || $term eq '';
                if ( $entry !~ /\Q$term\E/i ) {
                    $ok = 0;
                    last;
                }
            }

            if ( $ok && !$seen{$path}++ ) {
                push @found, $path;
            }
        }
        closedir $dh;
    }

    return @found;
}

# locate_dirs_under($root, @terms)
# Recursively finds directories beneath one root whose relative path matches
# every supplied keyword.
# Input: search root directory path string plus zero or more keyword strings.
# Output: sorted list of matched directory path strings.
sub locate_dirs_under {
    my ( $self, $root, @terms ) = @_;
    return () if !defined $root || $root eq '' || !-d $root;

    my @wanted = map { $self->_compile_search_regex($_) } grep { defined && $_ ne '' } @terms;
    my $root_id = $self->_path_identity($root);
    my %seen;
    my @found;

    File::Find::find(
        {
            no_chdir => 1,
            wanted   => sub {
                my $path = $File::Find::name;
                return if !-d $path;
                my $path_id = $self->_path_identity($path);
                return if $path_id eq '' || $seen{$path_id}++;

                my $relative = $path_id eq $root_id ? '.' : File::Spec->abs2rel( $path_id, $root_id );
                $relative = '.' if !defined $relative || $relative eq '';
                $relative =~ s{\\}{/}g;
                $relative = $relative eq '.' ? '.' : './' . $relative;

                for my $term (@wanted) {
                    return if $relative !~ $term;
                }

                push @found, $path_id;
            },
        },
        $root,
    );

    return sort @found;
}

# _compile_search_regex($pattern)
# Compiles one path-search token into the regex object used by recursive directory lookups.
# Input: one search token string.
# Output: compiled regex object, or dies when the token is not a valid regex.
sub _compile_search_regex {
    my ( $self, $pattern ) = @_;
    return if !defined $pattern || $pattern eq '';
    my $regex = eval { qr/$pattern/i };
    die "Invalid regex '$pattern': $@\n" if !$regex;
    return $regex;
}

# collector_dir($name)
# Returns the runtime directory for a named collector.
# Input: collector name string.
# Output: directory path string.
sub collector_dir {
    my ( $self, $name ) = @_;
    die 'Missing collector name' if !$name;
    return $self->_ensure_dir( File::Spec->catdir( $self->collectors_root, $name ) );
}

# indicator_dir($name)
# Returns the runtime directory for a named indicator.
# Input: indicator name string.
# Output: directory path string.
sub indicator_dir {
    my ( $self, $name ) = @_;
    die 'Missing indicator name' if !$name;
    return $self->_ensure_dir( File::Spec->catdir( $self->indicators_root, $name ) );
}

# ensure_dir($dir)
# Creates a directory if needed and applies runtime permission hardening.
# Input: directory path string.
# Output: directory path string.
sub ensure_dir {
    my ( $self, $dir ) = @_;
    return $self->_ensure_dir($dir);
}

# is_home_runtime_path($path)
# Checks whether one path lives under the home runtime tree.
# Input: file or directory path string.
# Output: boolean true when the path is inside ~/.developer-dashboard.
sub is_home_runtime_path {
    my ( $self, $path ) = @_;
    return 0 if !defined $path || $path eq '';
    return $self->_same_or_descendant_path( $path, $self->home_runtime_path ) ? 1 : 0;
}

# secure_dir_permissions($dir)
# Tightens one home-runtime directory chain to owner-only mode.
# Input: directory path string.
# Output: directory path string.
sub secure_dir_permissions {
    my ( $self, $dir ) = @_;
    return $dir if !$self->is_home_runtime_path($dir);

    my $home_runtime = $self->home_runtime_path;
    my $path = $home_runtime;
    chmod 0700, $path or die "Unable to chmod $path to 0700: $!" if -d $path;
    return $dir if $dir eq $home_runtime;

    my $suffix = substr( $dir, length($home_runtime) );
    $suffix =~ s{^/}{};
    for my $part ( grep { defined && $_ ne '' } File::Spec->splitdir($suffix) ) {
        $path = File::Spec->catdir( $path, $part );
        next if !-d $path;
        chmod 0700, $path or die "Unable to chmod $path to 0700: $!";
    }

    return $dir;
}

# secure_file_permissions($file, %args)
# Tightens one home-runtime file to owner-only mode, preserving owner execute
# bits when an executable file is expected.
# Input: file path string plus optional executable boolean.
# Output: file path string.
sub secure_file_permissions {
    my ( $self, $file, %args ) = @_;
    return $file if !$self->is_home_runtime_path($file);
    return $file if !-e $file;
    my $mode = $args{executable} ? 0700 : 0600;
    chmod $mode, $file or die sprintf 'Unable to chmod %s to %04o: %s', $file, $mode, $!;
    return $file;
}

# _ensure_dir($dir)
# Creates a directory if needed and returns it.
# Input: directory path string.
# Output: directory path string.
sub _ensure_dir {
    my ( $self, $dir ) = @_;
    if ( !-d $dir ) {
        if ( $self->is_home_runtime_path($dir) ) {
            make_path( $dir, { mode => 0700 } );
        }
        else {
            make_path($dir);
        }
    }
    $self->secure_dir_permissions($dir);
    return $dir;
}

# _expand_home($path)
# Expands leading home shorthands used by config and env values.
# Input: path string that may start with ~ or $HOME.
# Output: expanded path string.
sub _expand_home {
    my ( $self, $path ) = @_;
    return $path if !defined $path;
    $path =~ s/^\$HOME(?=\/|$)/$self->{home}/;
    $path =~ s/^~/$self->{home}/;
    return $path;
}

# _ancestor_runtime_layers()
# Discovers every existing .developer-dashboard layer between the current
# working directory and the configured home directory, excluding the home
# runtime root itself.
# Input: none.
# Output: ordered list of runtime root directory path strings from parentmost
# child layer to the deepest current layer.
sub _ancestor_runtime_layers {
    my ($self) = @_;
    my $cwd = eval { cwd() };
    return () if !defined $cwd || $cwd eq '';
    my $home = $self->home;
    my $home_runtime = $self->home_runtime_path;
    my $project_root = eval { $self->current_project_root } || '';
    my $stop_dir = '';
    if ( $self->_same_or_descendant_path( $cwd, $home ) ) {
        $stop_dir = $home;
    }
    elsif ( $project_root ne '' && $self->_same_or_descendant_path( $cwd, $project_root ) ) {
        $stop_dir = $project_root;
    }
    else {
        return ();
    }

    my @layers;
    my $dir = $cwd;
    while ($dir) {
        my $candidate = File::Spec->catdir( $dir, '.developer-dashboard' );
        push @layers, $candidate if -d $candidate && $self->_path_identity($candidate) ne $self->_path_identity($home_runtime);
        last if $self->_path_identity($dir) eq $self->_path_identity($stop_dir);
        my $parent = dirname($dir);
        last if !$parent || $parent eq $dir;
        $dir = $parent;
    }
    return reverse @layers;
}

# _path_identity($path)
# Normalizes a path for identity and ancestry comparisons without requiring the
# caller to care about symlink aliases such as /var versus /private/var on macOS.
# Input: path string.
# Output: canonical existing path or a stable canonpath string.
sub _path_identity {
    my ( $self, $path ) = @_;
    return '' if !defined $path || $path eq '';
    my $resolved = eval { abs_path($path) };
    return $resolved if defined $resolved && $resolved ne '';
    return File::Spec->canonpath($path);
}

# _same_or_descendant_path($path, $root)
# Checks whether one path is identical to or nested beneath another path after
# canonical normalization.
# Input: candidate path string and root path string.
# Output: boolean.
sub _same_or_descendant_path {
    my ( $self, $path, $root ) = @_;
    return 0 if !defined $path || $path eq '' || !defined $root || $root eq '';
    my $path_id = $self->_path_identity($path);
    my $root_id = $self->_path_identity($root);
    return 1 if $path_id eq $root_id;
    return index( $path_id, $root_id . '/' ) == 0 ? 1 : 0;
}

# _runtime_layers_from_env()
# Reads an explicit runtime-layer chain from the process environment when one
# is provided by the parent runtime process.
# Input: none.
# Output: ordered list of runtime root directory path strings from home to
# deepest layer.
sub _runtime_layers_from_env {
    my ($self) = @_;
    my $raw = $ENV{DEVELOPER_DASHBOARD_RUNTIME_LAYERS} || '';
    return () if $raw eq '';
    return grep { defined $_ && $_ ne '' } split /\n/, $raw;
}

1;

__END__

=head1 NAME

Developer::Dashboard::PathRegistry - logical directory registry

=head1 SYNOPSIS

  my $paths = Developer::Dashboard::PathRegistry->new(home => $ENV{HOME});
  my $root  = $paths->current_project_root;

=head1 DESCRIPTION

This module provides the central logical directory registry used across the
dashboard runtime, shell helpers, and background services.

=head1 METHODS

=head2 new

Construct the path registry.

=head2 resolve_dir, resolve_any, locate_projects, locate_dirs_under, current_project_root, project_root_for

Resolve and discover project-related directories.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module is the authoritative path model for the runtime. It discovers the layered runtime roots from home to the current project, resolves standard runtime directories, manages named path aliases, and performs project and directory searches such as the regex-based narrowing used by C<cdr>.

=head1 WHY IT EXISTS

It exists because C<DD-OOP-LAYERS> is a cross-runtime contract, not a convenience helper. One path registry has to own how home and project runtimes participate, which layer is writable, and how named paths and directory searches behave on top of that model.

=head1 WHEN TO USE

Use this file when changing layered runtime discovery, the writable runtime root, named alias behavior, project lookup, or directory-search semantics used by shell navigation helpers.

=head1 HOW TO USE

Construct it with the current home and cwd context, then ask it for runtime roots, named paths, or search results. Avoid rebuilding runtime path math elsewhere; other modules should consume this registry instead.

=head1 WHAT USES IT

It is used throughout the runtime by file, config, page, collector, prompt, shell-bootstrap, and CLI path logic, plus the tests that verify layered runtime behavior.

=head1 EXAMPLES

Example 1:

  perl -Ilib -MDeveloper::Dashboard::PathRegistry -e 1

Do a direct compile-and-load check against the module from a source checkout.

Example 2:

  prove -lv t/07-core-units.t t/21-refactor-coverage.t

Run the focused regression tests that most directly exercise this module's behavior.

Example 3:

  HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr t

Recheck the module under the repository coverage gate rather than relying on a load-only probe.

Example 4:

  prove -lr t

Put any module-level change back through the entire repository suite before release.


=for comment FULL-POD-DOC END

=cut
