Document main-relative companion loading

This commit is contained in:
2026-11-09 20:47:44 +00:00
parent bc3fc91296
commit 18af2d2130
10 changed files with 110 additions and 17 deletions

View File

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

View File

@@ -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 `<application>.yml`, then `<application>.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 `<configuration-root>/overlays/<name>.yml`, with
For ordinary loads and explicit layered overlay contexts, every other name resolves
to `<configuration-root>/overlays/<name>.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` |
| --- | --- | --- | --- |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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