436 lines
26 KiB
Markdown
436 lines
26 KiB
Markdown
# Filesystem Sources and Source-Backed Underlays
|
|
|
|
Status: proposed target for design agreement; not implemented. Significant API
|
|
and semantic choices are collected under Open Decisions. A later implementation
|
|
plan will own staged execution, detailed signatures, and acceptance commands.
|
|
|
|
## Purpose and priorities
|
|
|
|
Address the filesystem and layered-input needs in the
|
|
[PromptKit request](feature-request-promptkit.md) as one coherent extension:
|
|
native `fs.FS` sources and explicit source-backed underlays, including
|
|
programmatic input. ConfigKit's recursive combination model takes precedence
|
|
over PromptKit's requested whole-definition replacement. Neither capability
|
|
should require temporary files, a generated root YAML document, or serialization
|
|
through YAML merely to transfer already available values.
|
|
|
|
Design priorities, in order:
|
|
|
|
1. Clear, standardized behavior consistent with ConfigKit's existing conventions.
|
|
PromptKit may adapt its formats and APIs; its historical conventions are not
|
|
new ConfigKit compatibility obligations.
|
|
2. Efficient loading and retained state, aiming for linear or near-linear work
|
|
in actual input and output size. Prefer clarity and maintainability where a
|
|
more elaborate optimization would compromise the first priority.
|
|
3. A small public surface with cohesive private implementation, usable by simple
|
|
applications without requiring them to learn the layered-loading machinery.
|
|
|
|
Consumers load once and receive a complete configuration and its inspection
|
|
metadata. They should not need a second generic merge/defaulting pass, a separate
|
|
presence tracker, or reconstructed provenance. Domain-specific resolution remains
|
|
consumer-owned, but fits into the recorded loading lifecycle before the result
|
|
is validated and returned. Subject to consistent semantics and correct presence
|
|
and provenance, minimizing consumer integration code is an explicit design goal.
|
|
|
|
The [named-inheritance request](feature-request-promptkit-inheritance.md) remains
|
|
separate and optional. This roadmap neither implements it nor adds speculative
|
|
infrastructure for it.
|
|
|
|
## Current baseline
|
|
|
|
Planning baseline: `ec26591b275bd74f89120bdea69c5f6459dfcafb`, following v0.4.2.
|
|
Current behavior is owned by the [configuration contract](../config.md), not by
|
|
these proposals.
|
|
|
|
- `Load[T]` takes one `File`, `Bytes`, `Reader`, or `NoSource` input. Imports
|
|
strictly compose documents; one named overlay uses a root-relative directory
|
|
convention; explicit CLI overrides follow it.
|
|
- Filesystem context uses host directories and an `os.Root`-based implementation.
|
|
Byte and reader labels do not imply filesystem paths. There is no native
|
|
`fs.FS` source or public batch of heterogeneous layers.
|
|
- Composition rejects non-mapping overlaps, including equal values. Overlay
|
|
recursively merges mappings and replaces other values. Empty mappings do not
|
|
clear existing children; null is an explicit value, not omission.
|
|
- An explicitly configured schema-version path currently belongs to the selected
|
|
source. Its checker runs before overlay/CLI input, which cannot alter that path;
|
|
imported fragments cannot contribute it. This roadmap preserves those ownership
|
|
and timing rules.
|
|
- CLI overrides already convert supported programmatic scalars, lists, and
|
|
string-keyed maps directly to the private document representation. They have
|
|
CLI-specific paths, labels, restrictions, and origins, not a general source API.
|
|
- Source records and resolution queries expose acquired-source identity and
|
|
provenance, but do not provide an `fs.FS` resource context.
|
|
- Root-owned public wrappers delegate to `internal/engine`; parser-independent
|
|
trees belong to `internal/document`, and YAML parsing stays in `internal/yaml`.
|
|
- Current mapping assembly uses repeated linear key searches. Overlay application
|
|
clones the accumulated document and provenance; subtree-origin removal scans
|
|
the whole origin index. Straightforward repetition across many layers would
|
|
amplify these costs.
|
|
|
|
Existing source/lifecycle contracts, ownership, secret handling, and diagnostics
|
|
remain the compatibility baseline. This work is not permission to silently
|
|
reinterpret current `Load`, imports, selected overlays, or CLI overrides.
|
|
Redesigning schema-version policy is outside this roadmap's scope.
|
|
|
|
## Target source model
|
|
|
|
### Native filesystem input
|
|
|
|
A caller can supply a filesystem, an explicit file location within it, and a
|
|
meaningful diagnostic label/context. `embed.FS` is an ordinary supported example,
|
|
not a separate parsing mode. Use standard slash-separated `io/fs` path conventions;
|
|
do not apply host drive-letter or separator rules to virtual paths.
|
|
|
|
The source retains the filesystem context needed to acquire its imports, glob
|
|
matches, and selected overlay. Relative imports resolve from the importing
|
|
document; named overlay lookup remains relative to that configuration context's
|
|
root. References must never fall back to the host filesystem simply because a
|
|
virtual path is missing. Imports/globs retain their explicit opt-in, deterministic
|
|
ordering, composition semantics, and actionable errors.
|
|
|
|
Filesystem namespace identity and a normalized path identify a resource; labels
|
|
are presentation, not uniqueness keys. Identical names in different filesystems
|
|
must not collide. Use a reusable load-local context identity, not comparison or
|
|
hashing of arbitrary `fs.FS` interface values. Repeated-import and cycle checks
|
|
operate within the relevant input composition graph; selecting explicit layers
|
|
is distinct from accidentally importing a file twice.
|
|
|
|
Keep the host-file backend's established canonicalization and containment behavior.
|
|
For generic `fs.FS`, describe namespace-relative resolution and the supplied
|
|
filesystem's actual behavior. `fs.FS` does not universally provide symlink
|
|
canonicalization or a security sandbox; do not invent such guarantees or reject
|
|
all implementations that cannot provide host-specific operations. The caller
|
|
chooses the filesystem, its acquisition behavior, and its lifetime. ConfigKit
|
|
closes handles it opens, not a caller-owned filesystem as a whole.
|
|
|
|
### Origins and downstream resources
|
|
|
|
Locations remain meaningful for host files, filesystem-relative files, labeled
|
|
bytes, and programmatic values. Preserve deterministic source IDs and traversal
|
|
order while distinguishing source identity, input kind, and precedence role.
|
|
Do not invent host filenames or line/column coordinates for synthetic inputs.
|
|
|
|
A consumer can follow an effective field's origin to its contributing source and
|
|
obtain the context needed to resolve a relative resource. For a field inherited
|
|
through layering, that is the contributing source's context, not simply the last
|
|
layer's directory. Composite origins remain composite; ConfigKit must not choose
|
|
one contributing directory arbitrarily.
|
|
|
|
Resource-context access is explicit and separate from serializable diagnostic
|
|
records. Querying metadata itself does not read resources. PromptKit owns opening
|
|
and interpreting message text, rendering, and handling resource errors. Context
|
|
retention must not imply that external resources are snapshotted with the loaded
|
|
configuration or remain valid after the caller closes their filesystem.
|
|
|
|
## Target layering model
|
|
|
|
### Explicit operations, not discovery order
|
|
|
|
Extend public loading with explicitly supplied lower-precedence configuration
|
|
sources, using the same typed definition/result lifecycle as ordinary `Load`.
|
|
Support native filesystem, host-file, byte, reader, and programmatic inputs.
|
|
The central workflow is bundled defaults overridden by application configuration,
|
|
not an unrestricted sequence of user-selectable merge algorithms. Underlays can
|
|
form explicitly ordered default tiers; ConfigKit does not discover priorities or
|
|
know about PromptKit's catalog tiers. Source kind does not confer priority:
|
|
programmatic data can supply the appropriate supported input role without being
|
|
misrepresented as CLI flags.
|
|
|
|
The precedence model, from lowest to highest, is:
|
|
|
|
**Typed application defaults < source underlays < strictly composed main
|
|
configuration < selected overlay < explicit CLI overrides.**
|
|
|
|
Typed defaults remain the existing Go-value baseline. Source underlays add
|
|
configuration-backed defaults with imports, filesystem context, and source
|
|
provenance; they are not a replacement for the typed-defaults facility.
|
|
|
|
The selected operation determines how values combine, not their source format
|
|
or loading entry point. Equivalent file, embedded, byte/reader, and programmatic
|
|
input uses the same presence and combination rules. Strictly composing independent
|
|
contributions and intentionally overlaying precedence tiers express different
|
|
intentions; merely changing the input medium must not switch between them.
|
|
This applies to both ordinary `Load` and
|
|
the new layered-loading interface wherever they offer the same operation.
|
|
The existing opt-in version-path ownership rule remains an explicit metadata
|
|
constraint, independent of whether its owning source is a file or programmatic
|
|
input; it is not a source-format-dependent merge rule.
|
|
|
|
Keep strict composition and intentional overriding visibly distinct:
|
|
|
|
| Operation | Desired semantics |
|
|
| --- | --- |
|
|
| Compose independent contributions | Existing strict composition: recurse through mappings and report non-mapping conflicts with relevant source locations. |
|
|
| Overlay settings | Existing recursive mapping overlay: omitted children retain lower input; scalar, sequence, and null values replace. |
|
|
|
|
Underlay is a precedence role, not another combination algorithm. Assemble the
|
|
main document and all its imports by strict composition before applying that
|
|
result over the underlays using recursive overlay. A conflicting main/import
|
|
definition still fails, even if it would overwrite an underlay or be overwritten
|
|
by the selected overlay. Never flatten main and imported documents into a
|
|
last-wins list.
|
|
|
|
An underlay source may bring its explicitly enabled imports and source context.
|
|
Independent contributions within an underlay tier compose strictly; intentional
|
|
precedence between explicitly ordered tiers uses recursive overlay. Discovery
|
|
order does not establish tiers or authorize overriding. Syntax, source, and
|
|
composition failures cannot be hidden by higher-priority input.
|
|
|
|
There is no whole-definition replacement mode, per-layer recursive/atomic switch,
|
|
collection-specific identity matching, implicit sequence merge, profile registry,
|
|
or merge callback in this feature. Existing atomic scalar, sequence, null, and
|
|
cross-kind replacement under overlay remains unchanged.
|
|
|
|
Preserve the main configuration's version check after its strict composition and
|
|
schema construction, before precedence assembly with underlays, selected overlay,
|
|
and CLI contributions. Assemble the effective input before typed defaults/decoding,
|
|
application resolution, secret resolution, semantic validation, and final
|
|
snapshotting. Each lifecycle phase runs once per load, not once per layer.
|
|
Higher layers may replace values that would not fit the final typed schema;
|
|
effective typed validation is not validation of each intermediate snapshot.
|
|
Input syntax, source access, and composition remain validated at acquisition.
|
|
|
|
Recorded application resolution follows input assembly and decoding. Named-overlay
|
|
lookup has an unambiguous filesystem context, separate from version ownership: ordinary
|
|
loading retains its source/root convention; a multi-context load explicitly
|
|
identifies the lookup context when that convenience is used. Other sources retain
|
|
their own import and resource contexts. Callers do not manufacture a primary
|
|
document merely to combine layers, and no overlay lookup context is needed when
|
|
no named overlay is selected.
|
|
|
|
Higher-priority mappings retain omitted lower-input children. Empty mappings do
|
|
not clear those children; sequences, including empty sequences, replace as a
|
|
unit, and null remains explicit subject to the final schema. Typed defaults
|
|
continue to fill fields absent from the assembled input under existing rules.
|
|
No reset/delete operator is added.
|
|
|
|
For example, a bundled profile containing `{model: example-model, timeout: 30}`
|
|
combined with application input `{model: another-model}` retains `timeout: 30`
|
|
from the bundle. Its model origin points to the application and its timeout
|
|
origin points to the underlay. PromptKit must validate that final profile rather
|
|
than assume the application entry discarded all bundled settings. This knowingly
|
|
differs from its requested identity-based whole-definition replacement. Consumer
|
|
examples should adopt the shared semantics, not implement a hidden replacement
|
|
pass after loading. A replacement operation could be considered separately if
|
|
future concrete needs justify it, but is not an open choice in this roadmap.
|
|
|
|
### Preserve existing schema-version semantics
|
|
|
|
An application opts into version checking by configuring a version path. A key
|
|
spelled `version` has no automatic special status. When checking is configured,
|
|
one designated input owns that declaration; fragments cannot contribute it and
|
|
underlays, overlays, or CLI overrides cannot supply or alter it, even to an equal
|
|
value. Preserve the current version-path and ancestor protections when integrating
|
|
the new input role.
|
|
|
|
Keep the checker at its existing lifecycle point: after base composition and
|
|
schema construction, before underlay precedence assembly, selected overlays,
|
|
CLI overrides, defaults, and typed decoding. Preserve supported scalar forms,
|
|
absent/null distinction,
|
|
diagnostics, failure propagation, and version-path integer-to-string decoding.
|
|
An absent declaration remains absent for the checker; an invalid version type
|
|
fails without invoking it. Later input cannot mask a version-check failure.
|
|
|
|
Filesystem sources and layered loading do not require a different version
|
|
policy. The layered API needs a way to identify the version-owning input when a
|
|
`VersionRule` is configured; Decision 3 concerns that integration only. Without
|
|
version checking, no version owner or synthetic primary document is required.
|
|
Named-overlay lookup context is a separate concern, not a reason to impose
|
|
version ownership on unversioned loads.
|
|
|
|
Consumers continue to define version meaning and acceptance. Effective-version
|
|
last-wins validation, independent per-source version checks, schema selection,
|
|
and migration are not added by this feature. A future concrete need can motivate
|
|
a separate version-policy proposal; preserving the current contract here does
|
|
not settle that broader question permanently.
|
|
|
|
### Programmatic presence
|
|
|
|
Programmatic input has a non-secret source label and uses explicit structure to
|
|
represent presence. Omitted mapping keys are absent; present zero, false, empty
|
|
string, empty collection, and nil/null values remain explicit. Use ConfigKit's
|
|
existing canonical scalar and collection conventions, not YAML round trips or
|
|
Go zero-value inference. Decision 1 selects the initial input representation.
|
|
|
|
Do not label generic programmatic sources as CLI input or misuse synthetic flag
|
|
names to fit the existing override API. Preserve parser-independent conversion,
|
|
safe errors, deterministic map ordering, and existing secret-reference semantics;
|
|
this is not a new route for embedding resolved credentials in document trees.
|
|
|
|
Caller-owned mutable input is detached at a clearly documented ingestion
|
|
boundary. Results and inspection remain independently owned under existing
|
|
contracts. This does not promise language-enforced immutability of Go values or
|
|
an immutable filesystem.
|
|
|
|
A final Go value alone need not encode presence: an omitted boolean and an
|
|
explicit `false` can both decode to `false`. The returned configuration together
|
|
with its presence/provenance metadata preserves that distinction through
|
|
defaulting, layering, and recorded resolution. Null, empty values, zero, and
|
|
absence must not be collapsed by source conversion or merging.
|
|
|
|
### Provenance and operational boundaries
|
|
|
|
Origins, presence, history, explanation, and effective rendering agree with the
|
|
final typed result. Overridden and replaced definitions remain explainable without
|
|
retaining stale effective descendants. Replacements identify their scope and
|
|
displaced sources; equal-value explicit overrides are still meaningful history.
|
|
Include typed defaults and application-derived values, not only source-supplied
|
|
values, in final-value provenance. Retain redaction and callback-lifetime behavior.
|
|
|
|
Apply existing resource-accounting principles across the whole load rather than
|
|
resetting allowances for every layer. Audit how bytes, programmatic nodes,
|
|
accepted source occurrences, and glob work are counted without accidental double
|
|
charging. Do not add arbitrary new budgets or infer that efficiency work waives
|
|
existing ones; any needed adjustment must be explicit in the later design.
|
|
|
|
## Efficiency target
|
|
|
|
Measure cost against total acquired bytes/nodes, reference and path data, final
|
|
materialized values, and retained provenance/history. Layer count alone is not
|
|
a useful size measure. Large history or output can legitimately dominate cost.
|
|
|
|
Aim for O(N + H + R) processing and storage where N is total input/visited-path
|
|
work, H is retained history/provenance, and R is produced result data. Account
|
|
separately for deterministic sorting, filesystem traversal, schema processing,
|
|
and caller callbacks. This is an engineering target, not an unconditional public
|
|
complexity guarantee or a fixed performance gate.
|
|
|
|
The target implementation:
|
|
|
|
- Acquires/parses each accepted source occurrence once and directly converts
|
|
programmatic input. It does not decode or serialize accumulated configurations
|
|
between layers. Respect single-consumption readers and mutable file sources;
|
|
no implicit cross-load cache is needed.
|
|
- Uses indexed lookup alongside ordered mapping storage, retaining deterministic
|
|
iteration and diagnostics without repeatedly scanning growing mappings.
|
|
- Builds one load-owned working assembly instead of cloning the accumulated
|
|
configuration and history for every added layer. Detach at meaningful ownership
|
|
boundaries; safely share immutable private history where appropriate.
|
|
- Updates affected subtrees and their provenance rather than scanning all fields
|
|
for every replacement. Avoid repeatedly copying ever-growing ancestor-origin
|
|
lists or rechecking the same conflict group for every member.
|
|
- Reuses one schema/lifecycle evaluation per load and freezes the final result
|
|
once. It does not require global caches, copy-on-write public APIs, or a general
|
|
persistent-tree framework merely to meet an asymptotic target.
|
|
|
|
Use representative benchmarks and allocation measurements for broad keyed
|
|
catalogs, many small underlay tiers, repeated scalar/sequence replacement,
|
|
imported filesystem defaults, and retained history. Compare growth as inputs
|
|
scale, including small common
|
|
loads. Favor changes addressing demonstrated scaling problems; preserve stable
|
|
diagnostic order and ownership rather than trading them for a superficial O(n)
|
|
claim. No timing threshold or additional release gate is requested.
|
|
|
|
## Work needed from the baseline
|
|
|
|
These are responsibility areas, not implementation stages:
|
|
|
|
- Extend the root-owned source/context facade and private acquisition boundary
|
|
for `fs.FS`; adapt import, glob, overlay, identity, and resource-context handling
|
|
without exporting parser or engine types.
|
|
- Add source-backed underlays and programmatic input, sharing one engine path
|
|
with ordinary loading rather than building a parallel configuration system.
|
|
- Resolve the representation and ownership decisions below, then extend source
|
|
records, histories, diagnostics, and rendering consistently across input kinds.
|
|
- Integrate version-owner selection into layered loading while preserving the
|
|
existing version lifecycle and protections. Keep filesystem-context selection
|
|
independent of version checking.
|
|
- Refactor mapping assembly and provenance updates where current algorithms
|
|
would scale poorly under layered workloads. Keep optimization scope tied to
|
|
the new workflow rather than rewriting unrelated library subsystems.
|
|
- Add focused behavioral coverage for mixed sources, relative resources,
|
|
namespace collisions, import failures, composition conflicts, explicit empty
|
|
and null values, caller/result independence, and source-correct redaction and
|
|
explanation. Add comparative benchmarks without making performance a brittle
|
|
pass/fail contract.
|
|
- Compare equivalent logical inputs across supported source kinds and loading
|
|
interfaces. Assert consistent values, presence, operation outcomes, and origin
|
|
meaning while allowing genuine location/label differences. Cover defaulted and
|
|
derived values, recursively retained underlay fields, unchanged atomic-value
|
|
replacement, typed defaults, strict main/import conflicts, and preservation of
|
|
version ownership/check timing across both loading interfaces. Verify that
|
|
unversioned layered loads need no synthetic version-owning root.
|
|
- Provide maintained simple and PromptKit-shaped examples: embedded defaults,
|
|
operator configuration from another context, and labeled programmatic input.
|
|
Show explicitly that a same-name profile recursively combines, rather than
|
|
replacing its whole lower definition.
|
|
Demonstrate exact operations and resource origins without provider calls or
|
|
recreating PromptKit itself. The consumer supplies sources, explicit operations,
|
|
types, and domain rules; it does not implement generic merging, presence
|
|
tracking, or origin reconstruction after loading.
|
|
- Update Go declaration documentation, the configuration contract, consumer
|
|
guidance, component overview, and relevant architecture/deferred-scope records
|
|
as behavior lands. Record significant boundary changes in an ADR without
|
|
rewriting accepted ADR history. Keep named inheritance explicitly separate.
|
|
|
|
Plan an additive minor release for the new public capability, preserving current
|
|
version semantics. If another design choice requires changing documented behavior,
|
|
identify that change expressly and provide migration guidance rather than calling
|
|
it an internal optimization.
|
|
No release, implementation change, or current-policy revision occurs in this pass.
|
|
|
|
## Completion criteria
|
|
|
|
A consumer can load embedded configuration with relative imports, combine it
|
|
with independently located operator configuration and explicit programmatic
|
|
values, and obtain one typed result with accurate presence, sources, history,
|
|
and resource context. No temporary configuration files or serialization round
|
|
trip or subsequent generic configuration processing is needed. Simple `Load`
|
|
usage retains its current behavior, including version ownership and validation
|
|
timing. Layered loading uses the same version policy. Final-value provenance covers input,
|
|
defaults, and recorded domain resolution, and metadata distinguishes explicit
|
|
zero/false from absence even when the typed values are equal. Benchmarks
|
|
demonstrate that adding inputs does not entail avoidable full-collection copying
|
|
or quadratic key lookup. Public contracts and examples clearly distinguish
|
|
strict within-tier composition, recursive cross-tier overlay, and downstream domain
|
|
resolution. Equivalent-input tests and the maintained consumer example establish
|
|
semantic consistency and reduced integration code, not merely API availability.
|
|
|
|
## Open Decisions
|
|
|
|
### 1. Initial programmatic representation
|
|
|
|
**Recommendation:** labeled string-keyed mappings with the scalar/list/map value
|
|
set already supported by explicit overrides. Missing keys express omission and
|
|
nil expresses null. Reuse private conversion machinery, not CLI-specific public
|
|
records. This is predictable, directly expressible in Go, and consistent with
|
|
current dynamic configuration semantics.
|
|
|
|
**Alternative:** typed Go values with a separate explicit-presence description.
|
|
This can reduce downstream mapping code, but introduces reconciliation of field
|
|
bindings, masks, nulls, and typed zero values and a larger public contract. Do not
|
|
infer omission from zero values. Add typed construction only if real examples
|
|
show that the simple mapping representation imposes substantial repetitive work.
|
|
|
|
### 2. How consumers obtain filesystem resource context
|
|
|
|
**Recommendation:** a separate source-context query exposing standard filesystem
|
|
access and a source-relative base location through root-owned API types, while
|
|
ordinary source records remain descriptive. The returned filesystem is a borrowed
|
|
capability, not a cloned or immutable object. This avoids a mandatory downstream
|
|
registry and supports origins from imports as well as initial sources. Make
|
|
lifetime and nil/no-context behavior explicit before implementation.
|
|
|
|
**Alternative:** expose opaque context IDs and relative paths; callers retain a
|
|
registry mapping IDs to their filesystems. Metadata stays capability-free and
|
|
serializable, but every consumer must maintain and correctly reconcile another
|
|
registry. This is viable if limiting retained filesystem capabilities proves
|
|
more important than the additional integration machinery.
|
|
|
|
### 3. Selecting the version-owning input for layered loads
|
|
|
|
**Recommendation:** the designated main source owns the version when configuring
|
|
a `VersionRule`, extending the existing selected-source role rather than letting
|
|
an underlay win ownership by list position. It need not be a host file or a newly
|
|
manufactured root document. Its imports remain fragments and underlays cannot
|
|
declare the application's version. No special version-owner designation is
|
|
needed when checking is not configured, including loads using only underlays.
|
|
|
|
**Alternative:** allow explicit designation of a base input as the version owner
|
|
separately from its main/underlay role. This supports bundles carrying the
|
|
application version, but adds another ownership choice and complicates the
|
|
otherwise clear distinction between defaults and application configuration.
|
|
Either choice must preserve the existing version contract; this decision does
|
|
not reopen effective-version
|
|
or per-document version validation.
|