From 18af2d213092c02437514987e05a5d467a294164 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 9 Nov 2026 20:47:44 +0000 Subject: [PATCH] Document main-relative companion loading --- docs/adr/0022-main-relative-context.md | 47 ++++++++++++++++++++++++++ docs/config.md | 41 ++++++++++++++++++---- docs/consumers/README.md | 6 ++-- docs/development.md | 1 + docs/internal/imports.md | 5 +++ docs/internal/overview.md | 2 +- docs/policy/architecture.md | 5 +++ docs/roadmap/implementation.md | 2 ++ docs/roadmap/main-relative-inputs.md | 4 +-- layered.go | 14 +++++--- 10 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 docs/adr/0022-main-relative-context.md diff --git a/docs/adr/0022-main-relative-context.md b/docs/adr/0022-main-relative-context.md new file mode 100644 index 0000000..73969cf --- /dev/null +++ b/docs/adr/0022-main-relative-context.md @@ -0,0 +1,47 @@ +# ADR 0022: Bind Companion Contexts After Main Selection + +## Status + +Accepted. + +## Date + +2026-11-09 + +## Context + +Applications choose a main-file search and may also declare companion imports +and a selected overlay. The main directory is only known after discovery, so +supplying every dependent context before loading duplicates selection or requires +a preliminary load. ADR 0019 keeps main discovery explicit and inside loading. + +## Decision + +Extend layered loading to bind application-declared non-file underlays to the +resolved main context after acquisition. An explicit underlay context overrides +the binding, and an explicit fallback or require-base option controls absence. +Selected layered overlays choose an explicit context, the resolved main context, +or a fallback in that order. Derived and fallback lookup use the context base; +the inherited root continues to bound containment. Ordinary and explicit +overlay lookup retain their established root-relative behavior. + +Applications remain responsible for choosing searches, import declarations, +and overlay names. Selection failures are terminal; no second search or +application context callback is introduced. + +## Alternatives Considered + +- Require applications to discover the main file separately. This repeats + discovery and may run a complete load more than once. +- Implicitly scan conventional companion directories. This would impose + application-specific conventions without an explicit declaration. +- Add only a relative file constructor. This would not cover mapping or bytes + sources with existing import, placement, and composition rules. + +## Consequences + +The load owns the resolved binding and keeps namespace, containment, source +identity, and handle lifetime in the existing acquisition lifecycle. A missing +base skips optional imports while preserving the declaring payload; required +imports and require-base retain explicit failures. The configuration contract +and public declarations describe the observable rules. diff --git a/docs/config.md b/docs/config.md index 30e055a..2a4cfa8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -75,6 +75,8 @@ or the supported directory presets, then pass it to `SearchConfig`. Search descriptions are immutable and reusable; each load resolves dynamic directories and checks current candidate files again. `Searches` is ordered concatenation. It preserves the supplied description order and does not deduplicate candidates. +Layered companion inputs can use the selected main's context without a second +search, as described under [layered loading](#layered-loading). `NamedFilenames` selects `.yml`, then `.yaml`. `GenericFilenames` selects `config.yml`, `config.yaml`, `main.yml`, then @@ -316,13 +318,33 @@ typed configuration before application resolution. Syntax, strict-composition, inheritance, and decoding errors cannot be masked by a CLI override. Resolution, secret lookup, validation, and publication run once on the resulting values. -An active layered overlay requires `LayeredOptions.OverlayFilesystem` explicitly, -independent of the main source's namespace. Its name and root-relative lookup -follow the selected-overlay rules below; inactive overlays ignore the context. +An active layered overlay selects `OverlayFilesystem` first, then the resolved +main context, then `OverlayFallback`. An invalid selected context fails without +trying another. An inactive overlay ignores both supplied contexts. The same path in a different filesystem namespace is not a repeated file. Ordinary `Load` infers overlay context from sources that provide one; with `NoSource`, it requires the explicit `Options.Filesystem` context. +An underlay `Input.MainRelative` requests the resolved main context for a +non-file source. An explicit `Input.Filesystem` overrides it entirely. File, +native file, and search underlays cannot use an active binding, nor can the +main input bind to itself. `MainRelative()` creates a fresh default description. +The binding does not enable imports. If the main has no usable context, its +`Fallback` is selected if supplied; otherwise optional imports are skipped and +the input's own payload remains. Required named targets, directory and glob +selections fail without a base, while an empty `path: []` remains a no-op. +`RequireBase` requires a usable main or fallback context even without imports. +Invalid declarations still fail, and skipped imports create no search evidence. +Selected main failures never trigger fallback or a second discovery attempt. + +Derived contexts preserve the main namespace and containment root, while lookup +starts at its resolved base. A host file's base is its canonical target directory, +including when the selected filename is a symlink. A wider root permits contained +parent-relative imports without moving the lookup base. Explicit non-file and +absent-main contexts can provide a base; an absent host input may use its root as +base. An unused fallback is not opened or checked for existence. See the +[consumer example](../examples/consumers/main_relative.go). + Source IDs follow acquisition order—main and its depth-first imports, then each underlay tier's inputs and depth-first imports, then overlay and CLI—even though history follows precedence order. Default main-version checking happens after @@ -567,7 +589,7 @@ one context for a composite origin or an application-derived value. `ResourceContext.FS` is rooted at the source's configured root, and `BaseDir` is its slash-relative directory inside that root. This includes imported sources, selected overlays, host files even without enabled imports, native -files, and byte, reader, or mapping inputs with an explicit context. CLI and +files, and byte, reader, or mapping inputs with an explicit or main-derived context. CLI and contextless synthetic inputs have no resource context. To open a relative resource, join `BaseDir` with the application resource name using slash-path operations and open that path through `FS`. @@ -1489,12 +1511,15 @@ claim records rather than mutating or aliasing an input document. `Options.Overlay` selects at most one overlay by name. Names must match `[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name `default` select no overlay, and `overlays/default.yml` is never loaded implicitly. -Every other name resolves only to `/overlays/.yml`, with +For ordinary loads and explicit layered overlay contexts, every other name resolves +to `/overlays/.yml`, with no extension fallback, case normalization, inheritance between overlay files, or overlay stack. A selected missing overlay is an error. +For a layered main-derived or fallback context, lookup instead starts at the +resolved base directory; the configured root still bounds containment. -With `NoSource`, the main input stays absent and the explicit filesystem context -supplies the overlay root. The overlay is recorded as the first acquired source +With ordinary `Load` and `NoSource`, the main input stays absent and the explicit +filesystem context supplies the overlay root. The overlay is recorded as the first acquired source when no other source exists. Host contexts may use the root-only form described under [single-source loading](#single-source-loading); native contexts retain their complete namespace, base, and root requirements. @@ -1629,6 +1654,8 @@ config: Each selected document follows the same strict combination rules. A missing optional input can be absent; invalid declarations, unreadable selected files, and conflicting settings still fail loading. +Main-relative underlays follow the same import rules once a context is selected; +their no-base behavior is described under [layered loading](#layered-loading). | Option | `path` | `dir` | `glob` | | --- | --- | --- | --- | diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 6b38cdf..10c464b 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -38,8 +38,8 @@ Start with one file, then choose the tasks your application needs: - Use `LoadLayered` when several inputs should supply lower-priority defaults beneath the main source. Each underlay group combines its inputs strictly. Each [`Input`](../../layered.go) owns its source, filesystem, destination, and - import choice. The overlay filesystem context is selected independently in - [`LayeredOptions`](../../layered.go). + import choice. Main-relative bindings and overlay context selection are + available in [`LayeredOptions`](../../layered.go). - Load-wide rules belong to [`Options`](../../lifecycle.go) or [`LayeredOptions`](../../layered.go): the application definition (including version policy), import defaults, and explicit overrides. These do not turn @@ -53,6 +53,8 @@ file and layered cases. For import-focused adoption, see the [prompt catalog](../../examples/consumers/prompt_catalog.go); for explicit discovery composed with sequences, see the [sequence catalog](../../examples/consumers/sequence_catalog.go). +For discovered main files with companion imports and an overlay, see the +[main-relative example](../../examples/consumers/main_relative.go). ## Happy path diff --git a/docs/development.md b/docs/development.md index f5371a9..2fa108e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -66,6 +66,7 @@ Accepted decisions: - [Keep main-file discovery explicit and composable](adr/0019-explicit-configuration-discovery.md) - [Scope configuration controls to document roots](adr/0020-document-root-controls.md) - [Match registered records during resolution inspection](adr/0021-sequence-resolution-correspondence.md) +- [Bind companion contexts after main selection](adr/0022-main-relative-context.md) For cross-cutting changes, follow every applicable row. Completed implementation and remediation plans are preserved in Git history at `cb5b435`; use current diff --git a/docs/internal/imports.md b/docs/internal/imports.md index 55f07be..d20146f 100644 --- a/docs/internal/imports.md +++ b/docs/internal/imports.md @@ -20,6 +20,11 @@ patterns to discovered names, and form a set union of matching candidates across overlapping roots. Exclusions are applied as set difference after discovery and candidate normalization. They do not suppress discovery or resource errors. +Layered assembly passes the resolved main context to underlay acquisition, +replacing tier-local identity state. Contextless bindings prepare declaration +policies and assemble the declaring payload without opening access or inventing +selection evidence. Selected contexts use the normal acquisition and handle path. + Directory traversal tracks canonical directories on the active traversal path. It adds an identity when descending and removes it when the frame is popped, so only a back-edge to an active directory is a traversal cycle; a later alias is diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 456c668..b7c7c39 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -11,7 +11,7 @@ Implemented component owners: explicitly selected configuration discovery. A search resolves once during shared acquisition; the selected concrete file or absent-input state then follows the existing composition, metadata, and resource-context lifecycle. - It also owns grouped strict composition, recursive precedence assembly, + It also owns main-relative context selection, grouped strict composition, recursive precedence assembly, per-input mapping and sequence-item placement, YAML collection declarations and operations for identified sequences, and application identity registrations for strict composition, inheritance, and final validation. It applies the selected diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 0bca337..0ee7e91 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -151,6 +151,11 @@ initialization, and resource checks. See the [configuration discovery contract](../config.md#configuration-discovery) and [ADR 0019](../adr/0019-explicit-configuration-discovery.md). +The layered loader may bind application-selected dependent locations after main +selection. Applications still choose search conventions and must explicitly +select an overlay. [ADR 0022](../adr/0022-main-relative-context.md) records +the deferred context decision. + The engine applies each input's mapping destination within the existing source assembly, applies immediate-parent-relative YAML destinations to imports, and retains each effective placement with source identity and inspection records; diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0978689..8bbb083 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -227,6 +227,8 @@ incidental call counts. ## Stage 8 — Document and verify the completed sprint +**Status:** Complete. Focused integration and consumer checks, the everyday checker, the race suite, and changed local-link checks passed. + Update the public declaration comments and the discovery, layered loading, imports, resource context and selected overlay sections of [the configuration contract](../config.md). Document lookup versus diff --git a/docs/roadmap/main-relative-inputs.md b/docs/roadmap/main-relative-inputs.md index 4ce4ae5..f7ac91d 100644 --- a/docs/roadmap/main-relative-inputs.md +++ b/docs/roadmap/main-relative-inputs.md @@ -1,6 +1,6 @@ # Main-relative companion imports and overlays -Status: design settled; not implemented. The [implementation plan](implementation.md) +Status: implemented. The [implementation plan](implementation.md) owns the concrete API spelling, execution order, and verification. Baseline: `191015e`. ## Purpose @@ -19,7 +19,7 @@ Reuse existing optional-file, directory, recursion, pattern, destination, and composition behavior. A new file constructor alone would not cover this workflow, and a second fragment-discovery implementation is unnecessary. -## Current baseline +## Prior baseline The [discovery contract](../config.md#configuration-discovery) resolves `SearchConfig` inside loading to one host file or absent input. Selected-file diff --git a/layered.go b/layered.go index 582426a..da5ebd9 100644 --- a/layered.go +++ b/layered.go @@ -19,7 +19,9 @@ type Input struct { PlacementMode PlacementMode Imports bool Filesystem *FilesystemContext - MainRelative *MainRelativeContext + // MainRelative binds a non-file underlay to the main input's context when + // Filesystem is nil. It does not enable imports. + MainRelative *MainRelativeContext } // MainRelativeContext selects the main input's resolved context for an underlay. @@ -37,8 +39,9 @@ func MainRelative() *MainRelativeContext { return &MainRelativeContext{} } type Underlay struct{ Inputs []Input } // LayeredOptions selects lower-priority tiers and final application rules. -// An active Overlay requires an explicit OverlayFilesystem; an inactive overlay -// ignores it. Overrides remain explicit CLI records, not general mappings. +// An active Overlay uses OverlayFilesystem, then the main input's resolved +// context, then OverlayFallback. An inactive overlay ignores both contexts. +// Overrides remain explicit CLI records, not general mappings. // SourceVersionMode applies one declaration policy to main, underlay, import, // and selected overlay documents. Enabled modes interpret the version path // source-locally, consume accepted non-main declarations, and protect main's @@ -53,8 +56,9 @@ type LayeredOptions[T any] struct { Underlays []Underlay Overlay string OverlayFilesystem *FilesystemContext - OverlayFallback *FilesystemContext - Overrides []Override + // OverlayFallback supplies a context if the main has no usable context. + OverlayFallback *FilesystemContext + Overrides []Override } // LoadLayered acquires the main input first and strictly composes enabled