NAME
Developer::Dashboard::PathRegistry - logical directory registry
SYNOPSIS
my $paths = Developer::Dashboard::PathRegistry->new(home => $ENV{HOME});
my $root = $paths->current_project_root;
DESCRIPTION
This module provides the central logical directory registry used across the dashboard runtime, shell helpers, and background services.
METHODS
new
Construct the path registry.
resolve_dir, resolve_any, locate_projects, locate_dirs_under, current_project_root, project_root_for
Resolve and discover project-related directories.
resolve_dir() (DD-1005) additionally creates a registered alias's target directory (with parents) on first resolution when that alias was saved with create => 1 (see Developer::Dashboard::Config's save_global_path_alias()/save_skill_path_alias() %opts), chmod'ing it to the alias's stored octal mode when one was given. named_paths() collapses such an alias back to a bare path string for display consumers (dashboard path list, prefix completion) that have no use for the metadata; resolve_dir() itself always reads the raw internal registration, so the create/mode metadata is never lost between the two.
current_working_directory, cwd, getcwd
Report the effective working directory: the explicit constructor cwd when one was supplied, otherwise a lookup that uses absolute PWD only when its device and inode match the process directory, preserving valid logical symlink paths; otherwise it falls back to Cwd::cwd (undef when the directory is unavailable). cwd remains the object-level accessor consumed by the file registry. The package-level getcwd function preserves the historical imported Cwd::getcwd name while using the verified fast lookup.
_ensure_state_dir
Creates a missing state directory with mode 0700. For an existing directory, it inspects the mode from the directory check and calls chmod 0700 only when the complete permission/special-bit mask is not already exactly 0700. The directory remains private after either path; inspection or chmod errors are reported instead of being ignored. It accepts a directory path and returns that same path on success.
validated_path_segments
Package function that splits one untrusted relative path into its separator-delimited segments and rejects any shape able to escape the directory it will be joined onto: missing or empty paths, NUL or other control bytes, backslash separators, absolute paths, Windows drive prefixes, and empty, current-directory, or parent-directory segments. Returns the validated segment list (segment count in scalar context) or an empty list (undef in scalar context) on rejection. skill_layers requires exactly one validated segment for a skill name, and the skill dispatcher and web layer reuse this guard for every skill-namespaced route, ajax, and static asset path.
installed_skill_entries
Returns each effective installed skill once with its deepest selected root and all participating roots in home-to-project precedence order. The optional include_disabled flag follows installed_skill_roots semantics. This grouped inventory is intended for readers that need each skill's layered metadata; it avoids rescanning every runtime's skill directory once per skill.
skill_layers
Returns the participating roots for one validated top-level skill name in home-to-leaf precedence order. A disabled first effective layer masks the skill unless include_disabled => 1 is supplied; disabled later layers are skipped in normal lookup. During an explicit installed-skill inventory scope, directory names and validated name segments are reused, but every candidate directory and disabled marker is checked on each call. Outside that scope, the method resolves runtime skill roots afresh so later filesystem changes are visible. Input is a skill name and optional include_disabled boolean; output is an ordered list of skill-root paths, or an empty list for missing, invalid, or effectively disabled skills.
my @layers = $paths->skill_layers('example-skill');
my @all = $paths->skill_layers('example-skill', include_disabled => 1);
runtime_root, cache_root, home_runtime_root, home_cache_root
Report the layered runtime directories. runtime_root and cache_root follow the deepest discovered layer, which is the write target for layered runtime state. At each depth, both existing .d2/ and .developer-dashboard/ roots are independently discoverable; .developer-dashboard/ wins same-depth lookup precedence and remains the write target when both exist. home_runtime_root and home_cache_root stay pinned to the selected home write root, for per-user artifacts that a fixed external consumer reads from one well-known path.
runtime_layers refreshes filesystem discovery on an unscoped call so a retained registry notices project runtime-layer creation and removal. Callers may share its result only within an operation by localizing _runtime_layer_candidate_scope; all_paths and the path CLI establish such scopes. A scope snapshots each parent's existing .d2 and .developer-dashboard directories, while unscoped calls always check the filesystem again. The private _existing_layer_dirs_for helper requires a defined, non-empty parent path and reports invalid input before touching the filesystem. Direct Dancer routes establish request-local path-identity and runtime-layer scopes so repeated spellings are canonicalized once and directory existence is checked once per parent during that request. runtime_root also stores the deepest root in that active scope so repeated accessor calls avoid rebuilding a list from runtime_layers; outside a scope, it resolves a fresh list on every call. All request-local data is discarded before the next request; these filesystem-derived values are never retained across operations or requests.
_write_state_metadata
Record the runtime-root identity in runtime.json under the private shared state tree. Recreate missing directories or metadata after temporary cleanup. An existing file is left untouched only if it is a regular file owned by the current user, has mode 0600, and contains the exact expected JSON. Stale or loosely permissioned metadata is rewritten and secured; symlinks, non-regular files, and files owned by another user are refused with an explicit error. The canonical expected JSON string is kept in a bounded one-entry object cache for the latest exact runtime-root/application-name pair, avoiding redundant encoding. Every call still checks the live directory and file, validates ownership, mode, and type, and reads the on-disk bytes before accepting the metadata as unchanged. Filesystem inspection, open, write, and close failures are returned as explicit errors rather than being ignored. The small _state_metadata_lstat, _open_state_metadata_file, _print_state_metadata, and _close_state_metadata_file helpers preserve those operation results for deterministic failure-path tests.
config_root, config_roots, config_layers
config_root selects the writable config directory. When DEVELOPER_DASHBOARD_CONFIG_OVERLAY is set, that private directory becomes the write target; config_roots still returns the normal layered config directories and prepends the overlay so its values have the highest lookup priority. config_layers appends it as the final inherited layer. This lets container commands store a path-alias override without writing through a bind-mounted host config or hiding existing config values. The older DEVELOPER_DASHBOARD_CONFIGS setting remains a replacement config root. config_roots and skills_roots also accept an internal runtime_roots => \@roots snapshot. Request-scoped discovery uses this to derive sibling directories from one consistent layer inventory instead of re-discovering runtime layers for each accessor. Calls without the argument retain normal live discovery behavior.
alias_cache_key
Build the cache key that tells configured-alias consumers whether the runtime layout they cached against is still the current one. Called as a plain function rather than a method, because it must return an empty key for a missing or unblessed argument instead of dying on a method call. Developer::Dashboard::File and Developer::Dashboard::Folder both delegate to it; each previously carried its own identical copy of the computation.
PURPOSE
This module is the authoritative path model for the runtime. It discovers both supported runtime directory names 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 cdr.
WHY IT EXISTS
It exists because DD-OOP-LAYERS is a cross-runtime contract, not a convenience helper. One path registry has to own how home and project runtimes participate, how parallel .d2/ and .developer-dashboard/ roots are ordered, which layer is writable, and how named paths and directory searches behave on top of that model.
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.
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.
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.
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.