From 5959537d1adc2516e7f7e8881df72b707ec48c3f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 24 Oct 2026 19:45:33 +0000 Subject: [PATCH] Prepare the ConfigKit v0.10.0 release --- .../0019-explicit-configuration-discovery.md | 8 +- docs/development.md | 6 + docs/releases/v0.10.0.md | 77 +++ docs/roadmap/complexity.md | 6 +- docs/roadmap/discovery.md | 364 -------------- docs/roadmap/implementation.md | 449 ------------------ docs/roadmap/remediation.md | 298 ------------ 7 files changed, 90 insertions(+), 1118 deletions(-) create mode 100644 docs/releases/v0.10.0.md delete mode 100644 docs/roadmap/discovery.md delete mode 100644 docs/roadmap/implementation.md delete mode 100644 docs/roadmap/remediation.md diff --git a/docs/adr/0019-explicit-configuration-discovery.md b/docs/adr/0019-explicit-configuration-discovery.md index 4e59704..010c916 100644 --- a/docs/adr/0019-explicit-configuration-discovery.md +++ b/docs/adr/0019-explicit-configuration-discovery.md @@ -62,7 +62,7 @@ the public symbols. Dynamic bases and file availability are observed when each load consumes the description. Applications remain responsible for selecting conventions and for their schema and domain requirements. -The [configuration contract](../config.md) will own implemented behavior, the -[component overview](../internal/overview.md) will describe implementation -ownership, and the [implementation plan](../roadmap/implementation.md) tracks -delivery progress. +The [configuration contract](../config.md) owns implemented behavior, and the +[component overview](../internal/overview.md) describes implementation ownership. +The completed implementation plan and remediation record are preserved in Git +history at `0369111` under `docs/roadmap/`. diff --git a/docs/development.md b/docs/development.md index 7754d08..d1f0a5a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -183,6 +183,12 @@ Current behavior belongs to the configuration contract's The completed remediation record, including the algorithm-audit findings, is preserved at `ea46001:docs/roadmap/remediation.md`. +The completed configuration-discovery roadmap, implementation plan, and +remediation record are preserved in Git history at `0369111`, for example +`git show 0369111:docs/roadmap/remediation.md`. Current behavior belongs to +[configuration discovery](config.md#configuration-discovery); the public +[discovery declarations](../discovery.go) own the Go API. + Potential enhancements are tracked in [future features](roadmap/future.md). Install the Go version required by `go.mod` or a compatible newer toolchain. diff --git a/docs/releases/v0.10.0.md b/docs/releases/v0.10.0.md new file mode 100644 index 0000000..51d8ca0 --- /dev/null +++ b/docs/releases/v0.10.0.md @@ -0,0 +1,77 @@ +# ConfigKit v0.10.0 + +This minor release adds explicit, composable discovery of a configuration file. +Applications choose the search conventions and their order; ConfigKit selects one +file during loading and returns the final configuration with its normal metadata. +Optional searches that find no file share the expanded `NoSource()` lifecycle. + +## Changes + +- Add `SearchConfig`, immutable `Search` and `FilenameGroup` descriptions, and + search-wide requiredness through `SearchOptions`. Compose explicit `Paths`, + custom `SearchDirectory` descriptions, and built-in Unix system, user-config, + home, or working-directory presets with `Searches`. See the + [public declarations](../../discovery.go) and + [discovery contract](../config.md#configuration-discovery). +- Expand searches in supplied order, trying a directory's filename groups before + moving to the next directory. Named and generic groups provide conventional + YAML filenames; custom groups provide literal names. Directory searches + distinguish omitted filename selection from an explicitly empty selection. + Constructors perform no filesystem access, and each load observes current + directory bases and file availability. +- Select the first existing candidate. Access errors and failures while loading + the selected file terminate the load rather than trying another configuration. + Selected files retain physical source identity, placement, provenance, imports, + and relative-resource context. Discovery works with ordinary loading and + layered inputs, including underlays, without adding a precedence tier. +- Permit enabled imports with an absent main input: there are no main import + declarations to traverse, while actual underlay sources retain normal imports. + Support ordinary selected overlays with an absent main and explicit filesystem + context, including a root-only host context. Layered overlays retain their + independent `OverlayFilesystem` requirement. See + [single-source loading](../config.md#single-source-loading) and + [layered loading](../config.md#layered-loading). +- Preserve ordinary file-context behavior after discovery and include the + failing candidate's file identity in access diagnostics. Use Go's + platform-aware Windows component rules and native drive-root-relative path + resolution. Isolate user-config tests according to the host Go conventions. + +## Compatibility and upgrade + +Existing explicit source constructors and loading APIs remain available. +`File(path)` remains an exclusive selection that fails for a missing file; +discovery is activated only by explicitly supplying `SearchConfig`. + +Searches are optional by default. Set `SearchOptions{Required: true}` when the +application must find a file. An exhausted optional search contributes no main +document or source record; application version checks, defaults, resolution, +and validation still run under their existing rules and may reject incomplete +configuration. A selected overlay is still required to exist. + +The former `NoSource()` rejections of enabled imports and explicit filesystem +context are relaxed. An active ordinary overlay with no main file now loads +when explicit context is supplied. Applications that relied on those combinations +being rejected should review their option handling. Selected host files derive +their own base directory; do not supply an explicit `BaseDir` or native filesystem +context for a search that selects a host file. + +Applications may replace their own fallback-loading loops with the new search +descriptions. The maintained [consumer examples](../consumers/README.md#explicit-main-file-discovery) +show simple preset selection and custom-first discovery alongside existing +configuration tiers. Composition, collection operations, inheritance, CLI +precedence, and the complete-on-return ownership model retain their existing +semantics. + +Select this version and rebuild: + +```sh +go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.0 +``` + +The consumer minimum remains Go 1.26.0. Release validation uses Go 1.26.10. +The YAML dependency remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`, and the maintainer +tool remains ReleaseKit v0.4.1. Linux amd64 and arm64 remain release-validated +targets; macOS remains best-effort. Publication follows the existing +[release procedure](../release.md). + +[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/docs/oss/attribution.md) diff --git a/docs/roadmap/complexity.md b/docs/roadmap/complexity.md index b5f68d7..3b167e0 100644 --- a/docs/roadmap/complexity.md +++ b/docs/roadmap/complexity.md @@ -173,9 +173,9 @@ overlapping work and select its starting revision rather than duplicating the remediation plan here. These structural changes do not close a finding merely because its function moved or became smaller. -The pending [discovery roadmap](discovery.md) remains separate feature work. -Import cleanup should leave clear private boundaries for future use without -implementing discovery behavior as part of this refactor. +[Configuration discovery](../config.md#configuration-discovery) is implemented. +Import cleanup should preserve its source-acquisition boundary and behavior as +part of this refactor. ## Work Needed And Validation diff --git a/docs/roadmap/discovery.md b/docs/roadmap/discovery.md deleted file mode 100644 index 4f04808..0000000 --- a/docs/roadmap/discovery.md +++ /dev/null @@ -1,364 +0,0 @@ -# Composable Configuration Discovery Roadmap - -Status: implemented. This document preserves the design rationale and agreed -direction. The [configuration contract](../config.md#configuration-discovery) -owns current behavior, [ADR 0019](../adr/0019-explicit-configuration-discovery.md) -records the decision, and the [implementation plan](implementation.md) records -the completed work sequence. - -## Purpose And Ownership - -Make ordinary file-based loading concise for applications that want conventional -configuration locations. ConfigKit supplies useful built-in candidate lists and -an ordered selection operation. Consumers continue to own main-file search -conventions: they choose which lists apply, their order, custom candidates, and -whether a configuration file is required. - -A consumer can select one preset for a simple application or compose several -presets and custom path lists. The application supplies an explicit slug where -needed to construct conventional paths. ConfigKit does not infer application -identity from the executable name or enable discovery for existing explicit -sources. - -Discovery selects one source for the existing loading lifecycle. It does not -create additional configuration tiers or merge every matching file. Existing -layered loading remains available when consumers intentionally combine sources. - -## Baseline Before Implementation - -Before this work, sources included explicit host files, native filesystem files, -bytes, readers, mappings, and `NoSource()`, with no conventional main-file search. -That baseline also restricted `NoSource()` with imports and overlays. The -implemented discovery behavior now lets an exhausted optional search follow the -absent-main lifecycle, including enabled imports and an active overlay when its -required explicit context is supplied. The current [source contract](../config.md#sources), -[single-source loading](../config.md#single-source-loading), and -[layered loading](../config.md#layered-loading) sections define the behavior; -this baseline is retained only to explain the original design problem. - -Applications still choose search conventions and ordering. The architecture -policy now distinguishes those consumer choices from explicitly selected search -descriptions resolved during loading, while continuing to exclude implicit -discovery and network acquisition. - -## Agreed Direction - -### Filename Groups, Directory Searches, And Search Presets - -Organize discovery around three composable descriptions. These are ordered lists: -their order determines precedence. - -**Filename groups** provide reusable, ordered filename templates. The built-in -groups are: - -| Group | Filenames, in order | -| --- | --- | -| Named | `.yml`, `.yaml` | -| Generic | `config.yml`, `config.yaml`, `main.yml`, `main.yaml` | - -`` is the application slug supplied by the consumer. - -**Directory searches** pair a directory with an ordered list of filename groups. -The default selection is `[Named, Generic]`, producing the six filenames above -in that order. A directory search may select different groups or reverse their -precedence, such as `[Generic, Named]`, while retaining each group's internal -order. Consumers can also supply custom filenames, subject to the literal -single-component rules under OD-5 below. - -**Search presets** bundle ordered directory searches. Consumers can select a -complete preset for the simple case or compose presets, custom directory searches, -and explicit file candidates in their desired order. Explicit file candidates -are literal paths and receive no filename expansion. - -### Ordered Expansion And Concatenation - -The selection model is **ordered expansion and concatenation**, followed by -first-match selection: - -1. Expand the selected presets into directory searches in their supplied order. -2. For each directory search, concatenate its selected filename groups in order, - substitute the application slug where needed, and join each filename to that - directory. -3. Concatenate the resulting candidate lists, preserving the positions of custom - directory searches and explicit file candidates. -4. Try candidates in order, stopping at the first selected file or an error as - described below. - -Directory priority comes before filename priority: exhaust one directory's -filenames before trying the next directory. When several directories share a -filename list, their expansion is a Cartesian product traversed directory first. -Different directories may select different lists, so ordered expansion and -concatenation describes the complete model. Set union does not express its -precedence semantics. - -The [configuration contract](../config.md#configuration-discovery) explains this -model using ordered lists, ordered concatenation, and the directory-first -Cartesian product where a shared filename list applies. These terms describe -ordering without requiring eager construction of every candidate. - -### Unix System Preset - -The agreed Unix system preset is: - -| Directory, in search order | Filename groups, in order | -| --- | --- | -| `/usr/local/etc/` | `[Named, Generic]` | -| `/etc/` | `[Named, Generic]` | -| `/etc` | `[Named]` | - -This produces 14 candidates: six in each application-specific directory, then -`/etc/.yml` and `/etc/.yaml`. For example, -`/usr/local/etc//main.yaml` precedes -`/etc//.yml`. Selecting only `Named` for `/etc` is an ordinary -directory-search description; the selection algorithm needs no special exception. - -### Additional Presets (OD-1 — Settled) - -Provide the following independently selectable presets alongside Unix system. -Within each preset, search directories in the order shown: - -| Preset | Directory | Filename groups, in order | -| --- | --- | --- | -| User configuration | `/` | `[Named, Generic]` | -| User configuration | `` | `[Named]` | -| Home directory | `/.` | `[Named, Generic]` | -| Home directory | `` | `[Named]` | -| Working directory | Current working directory | `[Named]` | - -Resolve the user-configuration base with -[`os.UserConfigDir`](https://pkg.go.dev/os#UserConfigDir), the home base with -[`os.UserHomeDir`](https://pkg.go.dev/os#UserHomeDir), and the working-directory -base with [`os.Getwd`](https://pkg.go.dev/os#Getwd). Adopt the Go helpers' -platform-specific directory and environment behavior, including their errors; -the settled OD-2 rules below govern how discovery handles resolution errors. - -The goal is to support familiar conventions through idiomatic Go. This feature -does not add a separate XDG compatibility layer or XDG system preset. A selected -user-configuration base has no additional hard-coded `/.config` fallback. -The Home directory preset is a separate, explicit choice. - -The working-directory preset searches only that directory and uses application-named -files; it does not walk ancestor directories. The consumer chooses precedence -between all presets. Selecting one does not automatically select any others. - -### Illustrative Consumer API - -Names below are illustrative and are not implemented Go API. The -[implementation plan](implementation.md#public-api) fixes the concrete signatures -and the `Searches` constructor for concatenating several searches: - -```go -// A simple system-wide application. -source := configkit.SearchConfig("myapp", configkit.UnixSystemDirectories()) -``` - -```go -// A consumer-selected working-directory, user, then system search. -source := configkit.SearchConfig( - "myapp", - configkit.Searches( - configkit.WorkingDirectory(), - configkit.UserConfigDirectory(), - configkit.UnixSystemDirectories(), - ), -) -``` - -```go -// Explicit custom candidates followed by a selected conventional fallback. -source := configkit.SearchConfig( - "myapp", - configkit.Searches( - configkit.Paths("/run/myapp/config.yml", "/opt/myapp/config.yaml"), - configkit.UnixSystemDirectories(), - ), -) -``` - -Use the resulting source with ordinary loading or as a layered main input. Custom -searches and file lists participate at their specified position; they do not -silently append unselected presets. Reordering presets or their directory -searches changes search priority. - -### Selection And Failure Behavior - -| Situation | Selected behavior | -| --- | --- | -| Candidate is absent | Continue to the next candidate. | -| A candidate file is selected | Stop searching and load that file. | -| Candidate is inaccessible, unreadable, or not an accepted file type | Fail using normal source diagnostics. | -| Selected file fails parsing, version checks, composition, decoding, or validation | Fail without trying another candidate. | -| Entire search has no match | Absence is permitted by default; follow the shared absent-main lifecycle settled under OD-3 below. | -| Entire search has no match and is required | Fail with a diagnostic explaining the unsuccessful search. | -| Caller explicitly uses `File(path)` | Retain existing exclusive-file behavior, including errors for a missing file. | - -Requiredness applies to the complete search, not to each preset or directory. -Permitting an absent file does not waive application version, schema, or domain -requirements; the remaining lifecycle may still reject an incomplete configuration. - -Perform discovery before parsing the chosen main file. Do not implement fallback -by repeatedly running complete loads. Defaults, application resolution, secret -lookup, and final validation retain their existing lifecycle execution rules. -Once a file has been selected, later failure does not restart discovery. - -### Construction And Filesystem Behavior - -Constructors describe the search without filesystem access. Resolve dynamic base -directories and access candidate files during loading, so reusable descriptions -can observe changes on subsequent loads. Do not change the process working -directory or environment. - -Reuse the host-file acquisition rules for selected files, including regular-file -checks, symlink handling, resource ownership, and source limits. The feature does -not require a new filesystem traversal engine, recursive directory search, or -new file-size limits. Candidate filenames and groups are bounded by their -explicit descriptions, rather than by scanning unrelated directories. - -The exact Go names, constructors, and representation of search-wide options -belong in the implementation plan. Keep the public surface root-owned and keep -the single-preset case concise. - -### Unavailable Bases And Platform Applicability (OD-2 — Settled) - -Resolve directory bases only as the ordered search reaches them. Once an earlier -candidate is selected, later searches do not resolve their bases. Static invalid -search arguments still fail ordinary option validation before source acquisition. -An explicitly empty filename selection skips directory resolution as specified -under OD-4; it has no candidates to locate. - -If a selected directory's base cannot be resolved, stop and report the error. -A missing directory or candidate file beneath a resolved base allows the search -to continue; a directory-resolution error does not. This distinction also applies -to optional searches: permitting no match does not suppress errors encountered -while searching. Preserve the agreed errors for inaccessible or unreadable -candidates. - -Treat using the Unix system preset on an unsupported platform as an explicit -configuration error when that preset is reached. Specify its supported platforms -in the implementation plan. Do not translate the Unix preset into a different -platform's convention. User, home, and working-directory presets use their native -Go directory helpers. - -This keeps search priority predictable when directory resolution fails. Consumers -select the presets appropriate to their application and environment; there is no -option to skip unavailable bases or unsupported presets. - -### No Match, Imports, And Selected Overlays (OD-3 — Settled) - -Use a shared absent-main lifecycle for optional no-match and explicit `NoSource()`. -Allow imports to be enabled when the main file is absent: there are no main-file -import declarations to process, and enablement does not introduce automatic -directory scanning. Other actual sources retain their normal import behavior. -In layered loads, process underlays normally and retain the existing explicit -`OverlayFilesystem` requirement for an active overlay. - -For ordinary loads with an active overlay and no main file, require an explicit -filesystem context to locate the overlay. Do not infer an overlay root from the -first or last searched directory. A missing selected overlay remains an error; -absence of a main file does not silently disable a requested overlay. - -For an absent main input, a host filesystem context specifying only `Root` is -sufficient to locate the overlay; no main-file `BaseDir` is needed. If discovery -selects a host file, retain the normal file-context rules: the selected file -determines its base directory, an explicit `BaseDir` is rejected, and the file -must be contained within any supplied root. This allows one root-only context -to work with both selected and absent main inputs, subject to those existing -root and containment checks. An invalid context is an error, not a reason to -resume discovery. - -Defaults, CLI overrides, application resolution, and validation continue through -the normal lifecycle. Main-version checking receives an absent main input and -may reject it under the application's existing policy. Create no synthetic empty -document or source record for the absent main file. - -This deliberately relaxes the former `NoSource()` restrictions on imports and -on overlays with explicit context. Both ways of obtaining an absent main input -have the same behavior, as defined in the [configuration contract](../config.md#configuration-discovery). - -### Empty Composition And Implicit Preset Selection (OD-4 — Settled) - -Require explicit preset or search selection to activate a convention. Supplying -no presets or custom searches produces no candidates and activates no implicit -fallback. Empty custom path lists and explicitly empty filename-group selections -also contribute no candidates. An empty component does not prevent later -components from contributing candidates. - -A directory search with an explicitly empty filename selection does not resolve -its directory base. Static argument validation still applies. This gives empty -selections their no-candidate meaning without introducing directory-resolution -errors for a search that cannot select a file. - -If the complete search is empty, an optional search follows the absent-main -lifecycle settled under OD-3; a required search fails as exhausted. Empty lists -are valid, supporting consumers that construct their searches programmatically. - -Distinguish omission from an explicitly empty filename selection for a directory -search. Omitting the selection uses the agreed `[Named, Generic]` default; -explicitly selecting an empty list means no filenames. The eventual API must -represent both intentions clearly. Presets retain their specified filename groups. - -Selecting a single preset keeps the simple case concise. Search-wide requiredness -provides the check for consumers that must find a configuration file. - -### Slugs And Custom Path Interpretation (OD-5 — Settled) - -Require a nonempty single-component application slug whenever a selected search -uses it to construct a directory or filename. Reject path separators, `.` and -`..`, and forms unusable as that component on the target platform. Preserve -spelling and case; do not trim, lowercase, or impose a kebab-case naming scheme. -Validation keeps the slug from changing the intended directory structure without -imposing a separate naming style. - -Searches that do not use the slug do not require one. Examples include explicit -file candidates and a custom directory searched with only Generic filenames. - -Treat custom paths as literal host paths. Resolve relative paths against the -working directory at load time. Do not expand shell variables, `~`, or wildcard -syntax. Consumers can construct expanded paths themselves; User and Home presets -provide the common directory-resolution conveniences. These rules keep custom -paths consistent with explicit file loading. - -Custom filenames within directory searches are literal single filename -components. They cannot be absolute paths or contain subdirectory components; -use custom directory searches or explicit file candidates for those layouts. -They receive no application-name placeholder expansion. The Named group supplies -application-name expansion, while consumers may construct custom filenames -themselves before supplying them. - -### Placement, Provenance, And Existing Features - -The selected file participates as the actual file source. Preserve its physical -identity, effective placement, source origin, and passive resource context. -Relative imports and relative application resources use the selected file's -normal context. Discovery does not relocate overlay or CLI coordinates. - -Missing candidates are not acquired sources and do not create source IDs, -configuration history, or explicit input presence. An entirely absent search -contributes no synthetic empty configuration file. Diagnostics should distinguish -an exhausted required search from a failed candidate. Existing selected-source -inspection remains the foundation; a new public per-attempt trace is not part of -the implemented API. - -Preserve collection policies, inheritance, source-version rules, CLI ordering, -final validation, and frozen inspection after selection. Search-list order does -not change precedence among underlays, the main input, an overlay, and CLI values. -An explicitly selected overlay is not another main-file fallback candidate. - -## Target Integration And Documentation - -Discovery belongs at the source-acquisition boundary. A selected file retains -the existing file-loading behavior and optional exhaustion shares the absent-main -lifecycle above. Public search descriptions remain root-owned while private -implementation handles selection and loading. The feature's implemented -configuration contract and maintained consumer examples will explain the -three-part discovery model, ordering, and context behavior. - -The architecture policy should recognize explicitly consumer-selected discovery -conventions while preserving consumer ownership of search policy. An ADR records -that durable responsibility boundary; preset filenames and API mechanics belong -in their canonical contracts once implemented. Execution steps and validation -coverage are defined in the [implementation plan](implementation.md). - -This roadmap does not change implementation, policy, or ADRs by itself. The -released v0.9.0 code, including the completed sequence integration and remediation -work, provides the implementation baseline. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md deleted file mode 100644 index 5e73bd8..0000000 --- a/docs/roadmap/implementation.md +++ /dev/null @@ -1,449 +0,0 @@ -# Configuration Discovery Implementation Plan - -Status: complete. Stages 1 through 10 are complete. - -Implement the target behavior in [the discovery roadmap](discovery.md). That -document owns the selected conventions and OD-1 through OD-5; this plan owns the -API decisions, implementation sequence, and validation. Each numbered stage is a -separate Luna-sized unit. Complete stages in order and keep each commit buildable. - -## Baseline And Working Rules - -- Start from the released v0.9.0 implementation, with the roadmap clarification - at `92309ddcc3495c88c06d3a4dab414912a83acfae`. Inspect the current worktree and - preserve unrelated changes. The sequence integration and remediation are done. -- Follow [development guidance](../development.md) and the applicable - [agent](../policy/agents.md), [architecture](../policy/architecture.md), - [testing](../policy/testing.md), and [documentation](../policy/documentation.md) - policies. Use graph tools for code discovery. Read only the relevant owners. -- Keep the public API root-owned, with explicit delegation to `internal/engine`. - Add cohesive discovery files rather than growing the existing large lifecycle - functions. Reuse file acquisition, composition, and inspection machinery. -- Discovery chooses one source. Keep strict composition within tiers, precedence - between tiers, collection rules, inheritance, version policy, CLI ordering, - secrets, and complete-on-return ownership unchanged after selection. -- The intentional lifecycle extension is shared absent-main behavior for - `NoSource()` and optional discovery without a match. Do not invent mandatory - files, new limits, retries, background activity, naming styles, alternate merge - behavior, or release gates. Do not implement the separate complexity roadmap. -- Add tests at the owning boundary. Update current contracts when their behavior - lands, rather than leaving contradictory documentation until the last stage. - Commit each validated, nonempty stage with a plain English message and mark its - completion here. Do not publish a release or update downstream dependencies. - -## Decisions Used Throughout The Stages - -### Public API - -Use these names and signatures. The two opaque types below have private state; -their actual representation must satisfy the existing facade boundary tests. - -```go -type Search struct { /* private state */ } -type FilenameGroup struct { /* private state */ } - -type SearchOptions struct { - Required bool -} - -type DirectorySearchOptions struct { - Filenames []FilenameGroup -} - -func NamedFilenames() FilenameGroup -func GenericFilenames() FilenameGroup -func CustomFilenames(names ...string) FilenameGroup - -func SearchDirectory(directory string, supplied ...DirectorySearchOptions) Search -func Paths(paths ...string) Search -func Searches(searches ...Search) Search - -func UnixSystemDirectories() Search -func UserConfigDirectory() Search -func HomeDirectory() Search -func WorkingDirectory() Search - -func SearchConfig(application string, search Search, supplied ...SearchOptions) Source -``` - -`Searches` is ordered concatenation, not set union. It permits the simple -`SearchConfig("myapp", UnixSystemDirectories())` and a mixed search such as: - -```go -source := configkit.SearchConfig("myapp", configkit.Searches( - configkit.Paths("/run/myapp/config.yml"), - configkit.WorkingDirectory(), - configkit.UserConfigDirectory(), - configkit.UnixSystemDirectories(), -), configkit.SearchOptions{Required: true}) -``` - -This uses the existing convention of zero or one options value without mixing -search-wide controls into the candidate list. More than one options value is a -deferred option error. `SearchConfig` returns the existing `Source`, which works -through ordinary loading and `Input`; do not introduce a main-only source ban. -An underlay input retains its existing tier and contribution-version rules. - -For directory searches, omitted options or `Filenames == nil` use Named followed -by Generic. A non-nil empty `Filenames` slice means no names. Groups concatenate -in the supplied order; `CustomFilenames` is one group of literal names. An empty -custom group contributes none. A directory whose expanded filename list is empty -does not resolve its base. Presets retain the roadmap's fixed group selections. - -The zero `Search` and `Searches()` describe an empty candidate list; zero -`FilenameGroup` is an empty group. `Paths()` and `CustomFilenames()` are empty. -An explicitly supplied empty path or filename string is invalid, as opposed to -an empty list. The zero `Source` remains invalid. - -Snapshot caller-owned slices, including nested filename groups and option -slices, during construction. Descriptions remain immutable and reusable; dynamic -directories and candidate availability are observed again on each consumption. -Constructor failures are retained and reported during load, following opaque -source conventions. Validate all static search arguments before acquisition, -including invalid later components that an earlier match would otherwise hide. - -### Literal Components And Platform Behavior - -Apply the roadmap's component rules to application slugs and custom filenames. -A needed slug and each literal filename must be nonempty, distinct from `.` and -`..`, and one usable host filename component: no NUL, absolute/volume prefix, or -host path separator. Use standard path helpers; account for Windows reserved -names and invalid filename characters on Windows. Apply platform-specific rules -only on the relevant platform. Preserve case and spelling, including spaces and -non-ASCII names that are valid there. Do not impose a cross-platform ASCII or -kebab-case subset. Do not add speculative length limits; actual filesystem -failures follow normal diagnostics. - -Require a slug only if the description uses it in a directory or a Named group. -Explicit paths and Generic-only/custom-only literal directory searches need none. -Do not substitute placeholders in custom names or expand shell syntax in paths. -Resolve relative custom paths against the working directory at consumption. - -The Unix system preset is available on Go's `unix` platform set: aix, android, -darwin, dragonfly, freebsd, hurd, illumos, ios, linux, netbsd, openbsd, and solaris. -Use a small private platform predicate, preferably the standard `unix` build -constraint with its complement. Reaching it elsewhere reports an option error; -an earlier selected candidate prevents reaching it. This describes preset -applicability, not an expansion of ConfigKit's platform support or release gates. -Other presets call the three Go directory helpers specified in the roadmap. - -### Selection, Diagnostics, And Resource Ownership - -Resolve a search to a concrete host `File` or `NoSource` before parsing it. -Visit candidates incrementally in directory-first order. Resolve each reached -directory description once for its filenames; do not eagerly resolve later -bases, list directories, or globally deduplicate/reorder descriptions. - -Use the same absolute-path and symlink canonicalization rules as host files. -During candidate probing, only errors matching `fs.ErrNotExist` mean absence. -This includes a missing target reached through ordinary symlink resolution. -Other path-resolution/stat errors terminate selection. An existing target is -selected; its regular-file preflight, open, read, and parse follow existing file -rules. Any failure after selection is terminal, including a file disappearing -before open. Do not decide fallback by inspecting a completed load's diagnostics. - -A small stat/canonicalization probe followed by the existing file reader is -sufficient. Read and parse the selected file through the ordinary path once; -do not read it to probe and then reopen it for parsing. Existing preflight and -post-open checks remain. No stronger filesystem snapshot or race guarantee is -introduced. Preserve existing symlink and containment contracts. - -Use existing diagnostic codes: `CodeOptionsInvalid` for invalid descriptions and -reached unsupported presets, `CodeSourceMissing` for required exhaustion, and -normal source access/read codes for candidate and directory-helper failures. -Required exhaustion has a search-specific, value-free explanation and no -fabricated source location. Directory-helper failures have no acquired source; -retain normal selected-candidate records for file failures. Do not expose raw -OS errors, environment contents, or YAML values in messages. No new public -attempt trace, source kind, or per-candidate history is needed. - -### Absent Input And Filesystem Context - -After resolution, selected files use existing host-file context rules: derive -the base from the actual file, reject explicit `BaseDir` or a native `FS`, and -enforce any supplied root. Such errors fail without resuming discovery. - -For absence, imports are permitted but have no main declarations to traverse. -Retain a nil main document and no main source record. Version checking receives -the existing absent-main input; required source-version mode does not fabricate -a participating document. Actual underlays, imports, and overlays keep their -normal checks and accounting. - -An absent input may have an explicit filesystem context. A host `Root` must be -an absolute existing directory; `BaseDir` may be omitted, in which case use that -root only as internal filesystem context. If provided, validate `BaseDir` using -the existing absolute-directory and containment rules. Complete native contexts -continue to use the existing namespace, `BaseDir`, and `Root` rules. The required -new shorthand is root-only host context, not a new native-context shorthand. -Validate supplied contexts even without imports or an active overlay. - -For a search, validate static context syntax before acquisition where it does -not depend on the outcome; apply selected-file versus absent-input compatibility -checks after resolution. Do not reject an otherwise valid absent-input context -merely because selection could have produced a file. An invalid context never -causes fallback. - -An ordinary active overlay with absent main requires explicit context. Load it -from that context's normal root-relative overlay location. Never infer context -from attempted directories. Layered overlays still require their independent -`OverlayFilesystem` with its existing full context rules. Internal empty assembly -or context descriptors are acceptable; they must not become an acquired main -document, source ID, or explicit-presence contribution. - -## Stage 1 — Define Immutable Search Descriptions And Validation (Complete) - -Read [the facade ADR](../adr/0006-public-facade.md), the roadmap, and the source, -options, filesystem, and lifecycle owners named in the component overview. - -1. Add engine-owned descriptions and search/group constructors corresponding to - the chosen API in a focused `internal/engine/discovery.go` file. Leave the - source-producing `SearchConfig` constructor for Stage 6. Store flat ordered - search entries rather than a recursively interpreted concatenation tree. - Snapshot nested lists and retain constructor errors without filesystem access. -2. Implement pure description validation, default/empty filename handling, and - slug/component validation. Distinguish a zero/empty description from an - explicitly invalid supplied argument. Ensure concatenation preserves errors - as well as entries; do not validate irrelevant, unused slug text. -3. Add engine tests for supplied-option arity, snapshots, zero/empty components, - omitted versus empty filenames, reordered groups, slug-independent searches, - and platform-appropriate component validation. Do not duplicate standard - library path tests or introduce a large filename-validation framework. -4. Write ADR 0019, `0019-explicit-configuration-discovery.md`, recording the - accepted ownership decision: consumers select conventions and order; - ConfigKit selects one real source within synchronous loading; optional - exhaustion shares the absent-main lifecycle. Use the repository ADR format. - Explain the chosen decision and reasoning directly; keep alternatives in - Alternatives Considered. Leave implementation status in these roadmap files - and filename inventories/API mechanics in the eventual canonical contract. - -Acceptance: descriptions and validation are testable without acquisition; the -public loading behavior is unchanged. Run the new focused engine tests and the -everyday check. Mark and commit the stage. - -## Stage 2 — Expand Ordered Candidates And Resolve Preset Bases (Complete) - -1. Implement incremental ordered expansion for literal paths, directory entries, - groups, and presets. Preserve component positions and filename order exactly. - Keep requiredness on the whole search. Do not allocate a complete candidate - product merely to consume its first member. -2. Implement presets exactly as the roadmap tables specify, including all 14 - Unix system candidates and the Named-only `/etc` search. Implement user, - home, and working-directory bases with the Go helpers. Resolve a base only - when its nonempty directory search is reached. -3. Provide only a narrow private test seam for directory helpers/platform - applicability so failure and short-circuit behavior are deterministic. Bind - normal loads to the standard library; do not expose environment providers or - mutable process-global function replacements as public API. -4. Test expansion order and reversed groups, preset composition, custom paths, - empty entries followed by nonempty entries, and unsupported/unavailable bases. - Assert the contractual preset list at this owner. Use controlled helper - results for pure preset tests; do not create files under real `/etc` or home. - Demonstrate that stopping consumption prevents later base resolution and - that empty filename selections never resolve a base. - -Acceptance: deterministic ordered candidates, with no load integration yet. -Work is linear in description size plus candidates visited, excluding OS path -operations and selected-file processing. No benchmark gate is needed. Run focused -discovery tests and the everyday check; mark and commit. - -## Stage 3 — Select A Concrete File Through Host Acquisition Rules (Complete) - -1. Add a private resolver taking an immutable search description and returning - the concrete source or safe diagnostics. Non-search sources pass through. - Keep the selection state local to a consumption, never cached in `Source`. -2. Reuse or narrowly extract existing host-path canonicalization/probing from - `internal/engine/source.go`. Delegate selected-file reading to `readSource` - and `readRegularFile`; do not add a parallel reader or parser. -3. Return `NoSource()` on optional exhaustion and a required-search diagnostic on - required exhaustion, including a completely empty search. Missing attempts - create no source evidence and consume no acquisition budget. -4. Test with real temporary files: first match, later match, both extensions, - directory/nonregular target, contained symlink, required/optional exhaustion, - and a reached error preventing selection of a later good file. Use the - existing narrow filesystem failure seams for permission/read failures that - cannot reliably be reproduced when tests run as a privileged user. Reuse - existing FIFO and bounded-reader tests rather than duplicating them. -5. Check relative paths and literal `~`, variable, and wildcard characters in - controlled directories. Verify that a later base is untouched after success. - -Acceptance: selection returns one ordinary file or absence, without parsing or -running defaults/resolution. A selected source's later read failure is not a -fallback signal. Run focused discovery/source tests and the everyday check; -mark and commit. - -## Stage 4 — Unify Absent Input Acquisition And Context Validation (Complete) - -This stage changes `NoSource()` deliberately, before attaching discovery to loads. - -1. In `options.go`, remove the blanket rejection of imports for `sourceNone`. - In `filesystem.go`, replace blanket context rejection with the absent-context - rules above, separating static checks from directory access as needed. -2. In `assembleBaseInputTrackedForRole` and its collaborators, handle absent - acquisition without calling the import traversal that requires a main - document. Validate any supplied context separately. Preserve nil main state, - zero main source IDs, zero input accounting, and existing version callbacks. -3. Keep actual underlay and transitive-source imports unchanged. Absence in an - underlay contributes nothing and does not add an absent-main version callback. -4. Adjust tests that asserted the intentionally retired rejections; add focused - cases for `NoSource` with imports, valid root-only context, invalid context, - ordinary defaults/explicit zero and false CLI values, and layered underlays - whose imports still load. Cover absent-main version acceptance and rejection - at the existing version-test owner, including enabled source-version modes. -5. Update the `NoSource`/context/import portions of declaration comments and - `docs/config.md` for the behavior now implemented. Keep the active ordinary - overlay restriction until Stage 5, so this intermediate commit is accurate. - -Acceptance: imports can be enabled on absent input without synthetic sources, -while invalid supplied contexts still fail. Run focused load, filesystem, -version, and grouped tests, then the everyday check; mark and commit. - -## Stage 5 — Load Ordinary Overlays With An Absent Main (Complete) - -1. Replace the ordinary `NoSource` active-overlay rejection with the explicit - context requirement. Resolve absent-input overlay access from its context; - do not invent a selected main file to satisfy `importFilesystem`. -2. Adapt the smallest shared context/overlay helpers needed in - `filesystem.go`, `selected_overlay.go`, `load_input.go`, and - `source_context.go`. Keep layered overlay selection's independent context - contract unchanged. Reuse the existing overlay reader and accumulator. -3. Preserve source records and contexts for the actual overlay, including its - physical path, layer, ID, and resource base. With no other acquired sources - it is `source-1`. Missing main contributes no source or presence. Missing - selected overlay and invalid/outside-root contexts remain errors. -4. Add ordinary tests for absent-main overlay success, missing explicit context, - missing overlay, and overlay resource context. Exercise a complete native - context through existing filesystem machinery and root-only host context. - Reuse layered overlay tests to protect its independent root behavior. -5. Update owning declarations and contract text for the completed absent-main - overlay behavior. Review cleanup on success and each new failure path; - acquired file/root handles remain confined to the load call. - -Acceptance: shared absent-main behavior is complete for explicit `NoSource`. -Run focused overlay/context/load tests, `go test -race ./internal/engine`, and -the everyday check. Mark and commit. - -## Stage 6 — Integrate Search Resolution Into Engine Loading (Complete) - -1. Add a private discovery source kind/state to engine `Source` and have the - engine `SearchConfig` constructor capture the validated description. Add - static search validation to `selectOptions`; layered validation must inspect - every supplied input before acquisition, as it does for other options. -2. Resolve each discovery source once at the shared input-acquisition boundary, - before `parseSource`. Carry the concrete selection in a load-local field of - `assembledInput`, initialized for every source kind. Use it for import access, - source admission, and contexts within acquisition. Update the ordinary main - request to that concrete source before overlay application and context - reconstruction in `executeLoadRequest`. -3. Apply outcome-specific context checks after selection, without rerunning the - search. Audit remaining kind switches and callers so no downstream access - helper receives an unresolved discovery descriptor. Keep all existing early - validation that is independent of selection; do not change schema/version - callback ordering to implement discovery. -4. Use the same path for layered main and underlay inputs. Preserve per-tier - repetition checks, main-first acquisition order, placement, and budgets. A - selected source is `SourceKindFile`, not a new public discovery kind. -5. Add engine lifecycle cases comparing a searched source to its explicit `File` - counterpart, and exhausted optional search to `NoSource`, with the same - options. Compare actual paths, destinations, source IDs/layers, presence, and - context rather than internal descriptor shapes. Assert malformed selected - YAML and version/decode failures never fall back to a later good file. -6. Cover reuse after file/env/working-directory changes using isolated tests; - do not run environment-mutating tests in parallel. Verify static invalid - later components fail before probing, whereas dynamic later base failures - are skipped after an earlier selection. - -Acceptance: engine ordinary and layered loading consume searches through one -lifecycle and preserve real-source evidence. Run focused discovery/load/import/ -version/overlay tests, engine race tests, and the everyday check; mark and commit. - -## Stage 7 — Expose The Root Facade And Verify Public Integration (Complete) - -1. Add the specified root-owned discovery types, functions, comments, and - explicit conversions, preferably in root `discovery.go`. Keep engine types - out of exported fields/signatures and do not add bridge methods or aliases. - Convert nested lists without losing nil versus non-nil empty filenames. -2. Update `Source` constructor guidance. Add the implemented discovery contract - to `docs/config.md`: public declarations own symbol details; the contract - owns ordered selection, failure behavior, absence, context, and metadata. -3. Add root facade tests for the simple preset/custom-directory case, mixed - ordered searches with requiredness, absent-main parity, slice ownership, - and ordinary/layered use. Exercise custom Generic-only names with an empty - slug and explicit empty filenames through the public conversion boundary. -4. Test the important selected/absent context pair with one root-only host - context: selected file derives its base, absence permits an overlay at the - supplied root. Test a selected file outside that root fails without fallback. - Retain errors for selected files with explicit `BaseDir` or native `FS`. -5. Reuse existing public boundary and dependency tests; update an explicit symbol - inventory only if needed to recognize the deliberately added API. - -Acceptance: consumers can use the feature entirely through the root package. -Run focused facade discovery/context tests, public/dependency boundary tests, -and the everyday check. Mark and commit. - -## Stage 8 — Complete Cross-Feature Coverage And Consumer Examples (Complete) - -1. Add a small maintained consumer example/test for a simple explicitly selected - preset with test-controlled directories, and another for custom-first search - used with underlays/imports/overlay/CLI. Keep illustrative snippets short and - link to the maintained examples. No example should depend on real host config. -2. Review the integration coverage against the roadmap. Add only missing risk - cases: selected relative imports/resources; placement and sequence-item - placement; absence creating no item; actual collection/inheritance semantics; - defaults and callbacks running once; invalid chosen content preventing later - fallback; final metadata/rendering agreeing with the selected source. - Reuse existing structural, secret, removed-history, and lifetime tests where - discovery introduces no new path through them. -3. Review all new branches for independently owned descriptions/results and - cleanup. Use a narrow controlled acquisition failure test if an introduced - failure branch's closure behavior is not already covered. Do not turn this - into a cross-platform handle-counting framework or a new support gate. - -Acceptance: representative consumers need no application-side fallback loader -or provenance reconstruction. Run maintained consumer tests, relevant executable -examples, and the everyday check. Mark and commit. - -## Stage 9 — Align Architecture And Contributor Documentation (Complete) - -1. Update the architecture policy's responsibility boundary narrowly: applications - choose main-file conventions; explicit library-provided searches execute - inside loading. Preserve the exclusion of implicit discovery and network - acquisition. Link ADR 0019 and the canonical discovery contract. -2. Update the internal overview for discovery ownership and resolved-source - propagation, the development guide's focused test map/ADR list, and consumer - guidance. Add only concise README navigation or an example if useful; retain - the current minimal explicit-source example. Explain ordered concatenation - and the directory-first Cartesian product where applicable in the contract. -3. Review the declaration comments and discovery contract added in earlier - stages against the completed feature. Keep one canonical owner per fact and - fix stale claims about `NoSource`, implicit discovery, or overlay context. - -Acceptance: documentation describes implemented behavior and links the API, -contract, ADR, and maintained examples. Run the documentation link test and -whitespace review. Execute any newly changed behavior-bearing examples; reuse -Stage 8 results for unchanged examples. Mark and commit. - -## Stage 10 — Review The Complete Feature And Validate The Candidate (Complete) - -1. Compare the full implementation with the roadmap and this plan. Check every - OD, the API/default/empty distinctions, preset lists, lazy failures, context - outcome checks, source admission, callback execution, and complete-on-return - ownership. Ensure explicit `File` behavior and all existing precedence rules - remain intact. -2. Look specifically for accidental contract expansion: implicit presets, - global canonical deduplication, application-name normalization, extra file - restrictions, suppressed access errors, no-match version bypass, fallback - after selected-content errors, or new human/CI validation requirements. -3. Run `go test -race ./...` and `./scripts/check.sh` under the installed compatible - toolchain with dependencies already available, following the development - guide. Reuse successful checks for unchanged content; investigate failures - rather than weakening assertions. No timing/benchmark threshold, CI wait, - native macOS run, or release publication is part of this implementation. -4. Review status and staged diff, mark this plan complete only after the - acceptance criteria are satisfied, and commit any final nonempty unit. - Report validation and remaining limitations. Keep pending roadmap documents; - completed-plan retirement belongs to later release preparation. - -## Open Questions - -None. OD-1 through OD-5 are settled in the feature roadmap; this plan fixes the -remaining API and integration details without changing those decisions. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md deleted file mode 100644 index 842adc1..0000000 --- a/docs/roadmap/remediation.md +++ /dev/null @@ -1,298 +0,0 @@ -# Configuration Discovery Review Findings - -Status: complete. All five findings have been addressed in separate commits. - -Reviewed commit: `e50fee2`, including the discovery implementation commits after -`424c897`. The review compared the implementation with the -[discovery roadmap](discovery.md), [implementation plan](implementation.md), -[ADR 0019](../adr/0019-explicit-configuration-discovery.md), and the existing -configuration and architecture contracts. - -## 1. Defer Absence-Only Context Requirements Until The Search Outcome Is Known - -Priority: P2. Status: complete. - -**Completed:** search prevalidation now defers absence-only root requirements -until selection. Ordinary, layered-main, and underlay regression cases compare -searched files with explicit files under an empty context and retain errors for -absent inputs. The regression failed before the fix. Focused context/absence -tests and `./scripts/check.sh` passed with Go 1.26.10 on Linux. - -**Location:** [filesystem.go](../../internal/engine/filesystem.go), lines 77–123, -and the outcome-specific validation in -[load_input.go](../../internal/engine/load_input.go), lines 103–122. - -The `sourceSearch` branch calls `absentFilesystemOptionDiagnostics` before -selection. That helper requires a nonempty host `Root`, even though a selected -host file can derive both its base and default root from its own path. - -**Confirmed reproduction:** with an existing file containing `value: selected` -and `Options[Config]{Filesystem: &FilesystemContext{}}`, ordinary `File(path)` -loading succeeds. Replacing only the source with -`SearchConfig("", Paths(path))` fails with `options.invalid` and the message -"absent-input filesystem root and base directories must be absolute host paths". -No candidate is acquired. The same source distinction affects layered inputs -because their validation uses the same helper. - -This applies an absent-input requirement to a selected-file workflow and -unintentionally strengthens its contract. The roadmap requires selected files -to retain normal file-context behavior; the plan explicitly separates static -syntax checks from outcome-dependent compatibility checks. - -**Required correction:** validate only outcome-independent context syntax before -discovery. Once the outcome is known, apply normal host-file rules to selection -and absent-input rules to exhaustion. Preserve the required explicit root for an -absent input with a supplied host context, selected-file rejection of `BaseDir` -or native `FS`, containment checks, and terminal context failures. - -**Verification:** add ordinary and layered selected-file equivalence cases with -an explicit empty context. Also verify that optional exhaustion with that same -context still fails, and that the existing root-only selected/absent overlay -cases continue to pass. Do not change `File` to reject its currently valid context -or require callers to omit an otherwise valid options value. - -## 2. Preserve The Failing Candidate's Identity In Access Diagnostics - -Priority: P2. Status: complete. - -**Completed:** terminal probe failures retain the candidate descriptor and gain -normal file-source evidence at the acquisition boundary. Regression tests compare -ordinary and later-underlay failures with explicit-file errors, including source -IDs, placement, layers, and skipped missing candidates. A controlled permission -probe checks the diagnostic-to-file link. The load regression failed before the -fix; focused discovery/failure tests and `./scripts/check.sh` passed on Linux. - -**Location:** [discovery.go](../../internal/engine/discovery.go), lines 517–528, -and the early resolver-error return in -[load_input.go](../../internal/engine/load_input.go), lines 105–109. - -When symlink resolution or stat fails for a reached candidate with an error other -than absence, the resolver returns a diagnostic with an empty source ID and -discards the candidate. The acquisition boundary returns no source evidence. -Consequently neither `LoadError.Sources()` nor the diagnostic identifies which -candidate failed. The permission-probe test in -[discovery_test.go](../../internal/engine/discovery_test.go) also asserts a nil -primary location, preserving this omission rather than the intended normal -source diagnostics. - -**Confirmed reproduction:** create a regular `app.yml`, then search paths -`app.yml/child.yml` followed by the valid `app.yml`. The first path fails with -`ENOTDIR`. Explicit `File(badPath)` returns `source.read` with a primary source -location and a file record identifying `badPath`. The equivalent search correctly -stops, but returns `source.read` with no primary location and an empty source -list. An operator cannot locate the problem from the structured error. - -The roadmap calls for normal candidate access diagnostics, and the plan requires -normal candidate records for file failures. The decision to omit a public attempt -trace does not require discarding the one failing file's identity. - -**Required correction:** retain the failing candidate and expose its normal -file-source diagnostic identity at the acquisition boundary, with the appropriate -source ID, safe path/label, and input placement/layer. Keep missing attempts -absent from records and budgets. Required exhaustion and directory-helper errors -still have no invented file location. Preserve terminal access errors and avoid -adding a public attempt history, raw OS error strings, or YAML values. - -**Verification:** cover a deterministic real candidate-resolution failure and a -controlled permission/stat failure. Check the diagnostic's location against the -returned file record, including an underlay after earlier acquired sources. -Assert that no later candidate is loaded and that skipped missing candidates -still produce no records. Revise the existing nil-primary assertion accordingly. - -## 3. Isolate User-Configuration Preset Tests Using The Platform's Go Convention - -Priority: P2. Status: complete. - -**Completed:** the environment-change test selects the environment variable used -by Go on Windows, macOS/iOS, Plan 9, or other Unix hosts, and obtains both fixture -bases through `os.UserConfigDir`. It remains nonparallel and restores the -environment. The redundant relative-XDG failure assumption was removed; existing -private-helper tests retain portable failure/short-circuit coverage. Focused -tests and `./scripts/check.sh` passed on Linux. The macOS branch was checked -against Go's helper implementation; no native macOS gate was added. - -**Location:** [discovery_load_test.go](../../internal/engine/discovery_load_test.go), -`TestSearchSourceObservesUserConfigEnvironmentChanges`, lines 231–279. - -The new test treats every non-Windows platform as using `XDG_CONFIG_HOME`, writes -both fixtures beneath that variable, and leaves the actual home environment -unchanged. On macOS, `os.UserConfigDir` uses -`$HOME/Library/Application Support` and ignores `XDG_CONFIG_HOME`. The first load -therefore searches the real user's configuration directory instead of the -fixture, and the environment change cannot select the second fixture. This makes -the test fail on an ordinary macOS checkout and potentially depend on local files. - -This is confirmed by inspection of the selected Go 1.26.10 standard library's -`os.UserConfigDir` implementation and the test. A native macOS run was not -performed or required. Production preset resolution correctly delegates to Go; -the defect is in the test setup. - -**Required correction:** control the environment consumed by the platform's Go -helper, then obtain fixture bases through `os.UserConfigDir` rather than assuming -the XDG layout. On macOS, use a temporary home and its derived configuration -directory; retain appropriate isolation on Linux and Windows. Keep environment -changes restored and the test nonparallel. Use the existing private helper seam -for portable directory-resolution failure tests rather than assuming that a -relative environment value fails on every platform. - -**Verification:** the environment-change test must observe both controlled -fixtures and avoid the real home/config directory. Review the macOS branch -against Go's helper behavior. Keep macOS best-effort support and the existing -release procedure; do not introduce native macOS validation as a gate. - -## 4. Use Platform-Aware Windows Component Validation - -Priority: P3. Status: complete. Algorithmic audit: correctness finding. - -**Completed:** Windows validation now delegates reserved-name decisions to -`filepath.IsLocal` while preserving the selected component/character checks. The -manual device table was removed. Tests cover console names, platform-dependent -extension behavior, and invalid later search components; Unix tests retain their -own filename rules. Focused tests and `./scripts/check.sh` passed on Linux. The -Windows engine test binary cross-compiled successfully; it was not run natively. - -**Location:** [discovery.go](../../internal/engine/discovery.go), -`validWindowsFilenameComponent`, lines 266–281. - -The hand-maintained device-name switch omits `CONIN$` and `CONOUT$`. Both are -Windows console device names, not ordinary filename components. The helper -accepts them, so on Windows a slug or custom filename using one can pass static -validation. That conflicts with the selected usable-host-component rule and can -defer an invalid description until acquisition or hide it behind an earlier -successful candidate. - -**Evidence:** an external Go test overlay calling the current private helper -failed for both names: `validWindowsFilenameComponent` returned true. The Go -1.26.10 Windows implementation of `filepath.IsLocal` recognizes both as reserved, -and its own tests include both spellings. The overlay ran on Linux and exercised -the helper directly; it was not a native Windows load test. No repository test -file was added. - -**Complexity and refactoring opportunity:** the current predicate takes O(L) -time for a component of length L, with O(L) temporary storage for case conversion. -No asymptotic optimization is needed. Prefer the standard library's -platform-aware component/local-path checks within the Windows branch, retaining -the selected single-component and invalid-character checks as needed. This -avoids maintaining an incomplete device table. Go's Windows reserved-name logic -also accounts for OS-dependent extension behavior; do not replace it with a -broader blanket naming prohibition. - -**Verification:** add appropriate Windows-specific cases for console names and -ordinary names, including invalid later components behind an earlier match. -Keep those tests consistent with the host Go helpers rather than imposing their -Windows behavior on Unix. This is a correction to existing platform-specific -code, not a new Windows support promise or native-platform release gate. - -## 5. Preserve Drive-Root-Relative Windows Path Meaning - -Priority: P3. Status: complete. Algorithmic audit: correctness finding. - -**Completed:** drive-root-relative Windows paths now use `filepath.Abs` alongside -drive-relative paths, preserving native host interpretation. Windows regression -tests compare candidates and loaded files with explicit `File` for relative, -drive-root-relative (both separators), drive-relative, and absolute paths, through -both `Paths` and `SearchDirectory`. Existing Unix path/short-circuit tests and -`./scripts/check.sh` passed. The Windows engine test binary cross-compiled; no -native Windows execution or new release gate was introduced. - -**Location:** [discovery.go](../../internal/engine/discovery.go), -`absoluteCandidate`, lines 380–391; also used for literal directory searches. - -On Windows, a path such as `\config.yml` has no volume name and is not absolute -according to `filepath.IsAbs`, but it is rooted at the current drive's root. -The helper treats it as an ordinary relative path and joins it to the whole -working directory. With a working directory of `C:\work`, discovery therefore -probes `C:\work\config.yml` instead of `C:\config.yml`. The same issue affects -slash-rooted Windows spellings and custom directory searches. - -This can select a different file from explicit `File` loading, which uses -`filepath.Abs`. The roadmap requires literal host paths consistent with explicit -file loading; it does not select an alternate interpretation of drive-root paths. - -**Evidence:** static tracing against the selected Go 1.26.10 Windows `IsAbs`, -`VolumeName`, `Join`, and `Abs` implementations establishes the branch and result. -The first two checks route this path into `Join`, which appends it beneath the -working directory; Windows `Abs` delegates to the native full-path operation. -A native Windows execution was not performed or required. - -**Complexity and refactoring opportunity:** both current normalization and the -correct standard-library route are linear in the path spelling length, apart -from OS helper work. This is a correctness refactoring, not a scaling problem. -Route Windows drive-root-relative paths through `filepath.Abs`, as the helper -already does for volume-qualified relative paths, or use a narrow private -absolute-path helper that delegates to Go in production. Preserve lazy base -resolution and the ordinary-relative-path test seam. No new public API or -hand-written Windows path parser is needed. - -**Verification:** on Windows, compare discovery's candidate interpretation with -`filepath.Abs` and explicit `File` for ordinary relative, drive-root-relative, -drive-relative, and absolute paths. Cover both `Paths` and a literal -`SearchDirectory`, and retain the existing Unix literal-path tests. Do not add a -new native Windows validation requirement to the release procedure. - -## Algorithmic Audit Scope And Conclusions - -The additional audit reviewed new production code from `424c897` through -`e50fee2`; the current code at `68cf5c4` is unchanged from that implementation. -It preserved the expressly planned flat snapshots, ordered first-match search, -directory-first expansion, whole-description static validation, and reuse of -ordinary file acquisition. It did not benchmark or reopen those choices. - -- Let S be the description data processed by an operation, including entries, - filename groups, custom names, retained issues, and supplied option lists. - Snapshot/concatenation construction is O(S) per construction operation with - O(S) owned storage. - The nested clone loops visit each stored group/name once; they are not repeated - scans of the same list for each candidate. Repeated immutable prefix rebuilding - by a caller follows the explicitly planned flat-snapshot representation and - was not treated as a new algorithm finding. -- Validation is O(S) plus the existing canonical diagnostic ordering cost, - O(E log E) comparisons for E errors. Validation occurs a fixed number of times - per consumed description, not once per candidate. The audit found no new - repeated sorting or quadratic validation pass. -- Iteration advances persistent entry/group/name indexes. Across the search it - visits each reached group and candidate a constant number of times, with no - restart of earlier candidate lists. Traversal is O(S + C) in descriptor and - candidate counts, plus generated path bytes and ordinary filesystem work, - with O(1) iterator state beyond the description and current path storage. - Descriptions are not expanded into a complete candidate product in memory. -- Each attempt performs a bounded number of standard path/probe operations; - selection stops on success or a terminal error. Selected-file acquisition and - the final lifecycle run once. The added absent-context handling checks a fixed - number of directories and does not introduce scans over prior configuration - contributions. - -No unplanned quadratic algorithm or other actionable asymptotic regression was -identified. Findings 4 and 5 record the additional ordinary correctness issues -found while examining those algorithms; findings 1–3 were carried forward from -the earlier review. Existing configuration-tree algorithms outside the discovery -changes were not re-audited as new feature work. - -## Review Validation - -- `./scripts/check.sh` passed with Go 1.26.10 on Linux: formatting/whitespace, - all tests, vet, and build. -- `go test -race ./...` passed on the same toolchain. -- An external temporary Go program reproduced findings 1 and 2 through the - public API, comparing explicit file loading with discovery. No production or - test files were modified for these reproductions. -- Findings 1–3 remained open at the original review despite the suites passing. - The broader discovery architecture is implemented: immutable root-owned - descriptions, incremental ordered selection during shared source acquisition, - concrete selected-source propagation, and shared absent-input handling before - the existing composition, inheritance, decoding, resolution, and publication - lifecycle. No additional release gates or new composition semantics were found. -- The algorithmic audit used source-level complexity analysis and the temporary - helper regression overlay described in finding 4. It collected no timing or - allocation measurements. Findings 4 and 5 are platform-specific correctness - findings, not claims of a native Windows validation run. - -## Completed Remediation Validation - -All five fixes include focused regression coverage and per-issue completion -markers above. The final `./scripts/check.sh` and `go test -race ./...` passed -with Go 1.26.10 on Linux. The Windows engine tests also cross-compiled with -`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go test -c`, with the binary kept outside -the repository. No native Windows or macOS run was performed or made a gate. -The implementation preserves the public API, candidate ordering, optional -absence, and existing composition/version/ownership contracts.