From 424c89780e4a1c18fa1e718ddf8a677060c811e6 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 24 Oct 2026 15:04:02 +0000 Subject: [PATCH] Plan configuration discovery implementation --- docs/roadmap/discovery.md | 68 ++--- docs/roadmap/implementation.md | 449 +++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+), 41 deletions(-) create mode 100644 docs/roadmap/implementation.md diff --git a/docs/roadmap/discovery.md b/docs/roadmap/discovery.md index 320bc92..00b62fd 100644 --- a/docs/roadmap/discovery.md +++ b/docs/roadmap/discovery.md @@ -2,8 +2,9 @@ Status: proposal with OD-1 through OD-5 and the final review clarifications settled and integrated below. -This document describes future work, not an implemented contract. A staged -implementation plan will follow review of the complete roadmap. +This document describes future work, not an implemented contract. The +[implementation plan](implementation.md) owns the staged work and concrete API +decisions. ## Purpose And Ownership @@ -144,7 +145,9 @@ between all presets. Selecting one does not automatically select any others. ### Illustrative Consumer API -Names below are illustrative and are not implemented Go 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. @@ -155,9 +158,11 @@ source := configkit.SearchConfig("myapp", configkit.UnixSystemDirectories()) // A consumer-selected working-directory, user, then system search. source := configkit.SearchConfig( "myapp", - configkit.WorkingDirectory(), - configkit.UserConfigDirectory(), - configkit.UnixSystemDirectories(), + configkit.Searches( + configkit.WorkingDirectory(), + configkit.UserConfigDirectory(), + configkit.UnixSystemDirectories(), + ), ) ``` @@ -165,8 +170,10 @@ source := configkit.SearchConfig( // Explicit custom candidates followed by a selected conventional fallback. source := configkit.SearchConfig( "myapp", - configkit.Paths("/run/myapp/config.yml", "/opt/myapp/config.yaml"), - configkit.UnixSystemDirectories(), + configkit.Searches( + configkit.Paths("/run/myapp/config.yml", "/opt/myapp/config.yaml"), + configkit.UnixSystemDirectories(), + ), ) ``` @@ -340,41 +347,20 @@ 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. -## Work Needed From The Baseline +## Target Integration And Documentation -The subsequent implementation plan should cover: +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. -- Define filename-group, directory-search, preset, and search-source representations, - slug validation, and a search-wide requiredness option. Keep construction - separate from acquisition and the simple preset selection concise. Distinguish - omitted and explicitly empty directory-search filename selections. -- Implement ordered expansion and concatenation for presets, filename groups, - custom searches, and explicit file candidates, followed by first-match selection - through existing file access. Resolve each source once per consumption before - entering its parse and composition lifecycle. -- Integrate selected and absent outcomes with ordinary and layered main inputs, - placement, imports, application version checking, and overlay contexts according - to OD-3, including root-only host contexts for absent main inputs and normal - file-context checks when a file is selected. Preserve ownership and close - acquired handles on success and failure. -- Preserve real selected-source metadata and safe diagnostics without treating - absent candidates as configuration contributions. -- Exercise preset, directory, filename-group, and filename ordering, reversed - group precedence, the Unix system preset's 14 candidates and named-only `/etc` - search, custom-list composition, both filename extensions, no-match behavior, - empty components and searches, omitted versus empty filename selections, - skipped directory resolution for empty filename selections, slug-dependent and - slug-independent searches, literal custom paths and filename components, - required searches, unavailable preset bases, errors that must stop fallback, - repeated loads, and untouched explicit `File` - behavior. Cover imports, overlays, underlays, defaults, CLI, source versions, - source contexts, and source-list/inspection evidence at their boundaries. -- Clarify the architecture policy's consumer-selected discovery boundary. Record - any warranted durable ownership decision in an ADR; keep preset filenames and - API mechanics in their canonical contract when implemented. Update consumer - examples and the configuration contract when the feature lands. Explain the - three-part discovery model and its mathematical terminology in the eventual - documentation, with simple preset selection and custom composition examples. +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 diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..a67f91f --- /dev/null +++ b/docs/roadmap/implementation.md @@ -0,0 +1,449 @@ +# Configuration Discovery Implementation Plan + +Status: ready for implementation. No stages 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 + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +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.