Prepare the ConfigKit v0.10.0 release

This commit is contained in:
2026-10-24 19:45:33 +00:00
parent 0369111d30
commit 5959537d1a
7 changed files with 90 additions and 1118 deletions

View File

@@ -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 load consumes the description. Applications remain responsible for selecting
conventions and for their schema and domain requirements. conventions and for their schema and domain requirements.
The [configuration contract](../config.md) will own implemented behavior, the The [configuration contract](../config.md) owns implemented behavior, and the
[component overview](../internal/overview.md) will describe implementation [component overview](../internal/overview.md) describes implementation ownership.
ownership, and the [implementation plan](../roadmap/implementation.md) tracks The completed implementation plan and remediation record are preserved in Git
delivery progress. history at `0369111` under `docs/roadmap/`.

View File

@@ -183,6 +183,12 @@ Current behavior belongs to the configuration contract's
The completed remediation record, including the algorithm-audit findings, is The completed remediation record, including the algorithm-audit findings, is
preserved at `ea46001:docs/roadmap/remediation.md`. 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). Potential enhancements are tracked in [future features](roadmap/future.md).
Install the Go version required by `go.mod` or a compatible newer toolchain. Install the Go version required by `go.mod` or a compatible newer toolchain.

77
docs/releases/v0.10.0.md Normal file
View File

@@ -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)

View File

@@ -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 remediation plan here. These structural changes do not close a finding merely
because its function moved or became smaller. because its function moved or became smaller.
The pending [discovery roadmap](discovery.md) remains separate feature work. [Configuration discovery](../config.md#configuration-discovery) is implemented.
Import cleanup should leave clear private boundaries for future use without Import cleanup should preserve its source-acquisition boundary and behavior as
implementing discovery behavior as part of this refactor. part of this refactor.
## Work Needed And Validation ## Work Needed And Validation

View File

@@ -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 | `<appname>.yml`, `<appname>.yaml` |
| Generic | `config.yml`, `config.yaml`, `main.yml`, `main.yaml` |
`<appname>` 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/<appname>` | `[Named, Generic]` |
| `/etc/<appname>` | `[Named, Generic]` |
| `/etc` | `[Named]` |
This produces 14 candidates: six in each application-specific directory, then
`/etc/<appname>.yml` and `/etc/<appname>.yaml`. For example,
`/usr/local/etc/<appname>/main.yaml` precedes
`/etc/<appname>/<appname>.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 | `<user-config-dir>/<appname>` | `[Named, Generic]` |
| User configuration | `<user-config-dir>` | `[Named]` |
| Home directory | `<home>/.<appname>` | `[Named, Generic]` |
| Home directory | `<home>` | `[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 `<home>/.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.

View File

@@ -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.

View File

@@ -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.