Demonstrate entry-declared import destinations

This commit is contained in:
2026-10-14 17:41:35 +00:00
parent a20ba0af27
commit 08e03f6a38
14 changed files with 143 additions and 28 deletions

View File

@@ -17,7 +17,7 @@ sanitized shapes using only ConfigKit and the Go standard library:
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file | | Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem | | Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource | | Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
| Mounted collections | Literal input destinations, peer prompt/profile fragments, local inheritance, imported collection metadata, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources | | Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset | | Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation | | Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
@@ -109,12 +109,21 @@ and may fail independently of the completed configuration load. The
[configuration contract](../config.md#source-resource-contexts) owns exact [configuration contract](../config.md#source-resource-contexts) owns exact
lifetime and precedence rules. lifetime and precedence rules.
[`LoadPromptBundle`](../../examples/consumers/layered.go) is the ordinary-root
counterpart: its entry opts into imports through `Load`, imports metadata at the
root, and places a prompt beneath `prompts` using YAML. The executable example
and behavior test are in
[`layered_test.go`](../../examples/consumers/layered_test.go).
## Input destinations and mounted collections ## Input destinations and mounted collections
Main and underlay inputs use the same destination field; imported application Main and underlay inputs use the same destination field. An import without a
contributions inherit the declaring input's destination. A destination is an `destination` inherits its declaring document's effective destination; an
application mapping path, independent of filesystem context. The configuration explicit sequence of key strings appends a relative suffix. Nested imports
contract owns the complete behavior and validation rules. append to their immediate parent's effective destination, while filesystem
paths remain relative to the declaring file. Destinations are application
mapping paths, independent of filesystem context. The configuration contract
owns the complete behavior and validation rules.
The small [`Example_inputDestination`](../../examples/consumers/destinations_test.go) The small [`Example_inputDestination`](../../examples/consumers/destinations_test.go)
mounts one mapping source beneath literal collection/prompt keys and loads a mounts one mapping source beneath literal collection/prompt keys and loads a
@@ -123,12 +132,13 @@ default. Consumers pass paths, not filenames or namespace strings for ConfigKit
to interpret. to interpret.
[`LoadCollectionBundle`](../../examples/consumers/destinations.go) assembles an [`LoadCollectionBundle`](../../examples/consumers/destinations.go) assembles an
embedded collection manifest with a source-root import, separate prompt and embedded collection manifest mounted beneath a literal collection path. The
profile peers, and a higher-priority input that changes only one prompt's entry's YAML places nested metadata imports at the collection root and prompt
ordinary `version` field. The main document alone supplies the application and profile peers beneath their own keys. A higher-priority input changes only
schema version. A prompt's local `extends` relationship resolves after all one prompt's ordinary `version` field. The main document alone supplies the
tiers, so `custom` inherits the final `brief` content and fields while retaining application schema version. A prompt's local `extends` relationship resolves
its own profile selection. after all tiers, so `custom` inherits the final `brief` content and fields while
retaining its own profile selection.
The resolver follows each effective `content_file` origin to its declaring The resolver follows each effective `content_file` origin to its declaring
source context. This also works for `custom`'s inherited resource. The source context. This also works for `custom`'s inherited resource. The

View File

@@ -380,7 +380,7 @@ removed descendants. No limitations.
## Stage 8 — Demonstrate ordinary and mounted collection entries ## Stage 8 — Demonstrate ordinary and mounted collection entries
Status: planned. Scope: maintained public examples and their behavior tests. Status: complete. Scope: maintained public examples and their behavior tests.
1. Add a compact ordinary-root example: its entry imports metadata and a fragment 1. Add a compact ordinary-root example: its entry imports metadata and a fragment
with `destination: [prompts]`, using existing `Load` import opt-in and no with `destination: [prompts]`, using existing `Load` import opt-in and no
@@ -402,6 +402,17 @@ Validate: `go test ./examples/consumers`, applicable executable `Example` tests,
local links, and everyday check. Done: ordinary and layered workflows operate local links, and everyday check. Done: ordinary and layered workflows operate
without the deferred source-header proposal. without the deferred source-header proposal.
Evidence (2026-10-14): based on `a20ba0a`, using `go1.26.8 linux/amd64`.
`go test ./examples/consumers`, root executable examples, local documentation
links, and `./scripts/check.sh` passed. The ordinary-root sample uses `Load`
with import opt-in and entry-authored root/prompt placement. The mounted
collection example now declares metadata, prompt, and profile destinations in
its entry YAML and exercises nested imports, while Go selects only the mount
and main-owned schema policy. Behavior checks cover strict peer composition,
post-tier prompt/profile inheritance, the version-only operator tier, effective
source placement, and reading inherited prompt content from its source context.
No limitations.
## Stage 9 — Reconcile documentation and audit the complete feature ## Stage 9 — Reconcile documentation and audit the complete feature
Status: planned. Scope: final consistency and evidence; no publication work. Status: planned. Scope: final consistency and evidence; no publication work.

View File

@@ -43,9 +43,9 @@ type CollectionProfile struct {
} }
// LoadCollectionBundle demonstrates one layered collection load. Its main // LoadCollectionBundle demonstrates one layered collection load. Its main
// document owns the application schema version; imported collection metadata, // document owns the application schema version; the mounted entry declares
// prompt/profile peers, and an operator prompt-version override use ordinary // relative destinations for collection metadata, prompt/profile peers, and
// destinations and domain fields. // their nested imports. The operator version override remains a separate tier.
func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) { func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
files, err := fs.Sub(bundledFiles, "testdata/collection-bundle") files, err := fs.Sub(bundledFiles, "testdata/collection-bundle")
if err != nil { if err != nil {
@@ -54,7 +54,6 @@ func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
bundle := configkit.NewFilesystem("collection bundle", files) bundle := configkit.NewFilesystem("collection bundle", files)
collectionDestination := configkit.Keys("collections", "weatherreporter") collectionDestination := configkit.Keys("collections", "weatherreporter")
promptDestination := collectionDestination.Key("prompts") promptDestination := collectionDestination.Key("prompts")
profileDestination := collectionDestination.Key("profiles")
return configkit.LoadLayered( return configkit.LoadLayered(
configkit.Input{Source: configkit.Bytes("application main", []byte("version: 1\n"))}, configkit.Input{Source: configkit.Bytes("application main", []byte("version: 1\n"))},
configkit.LayeredOptions[CollectionBundleConfig]{ configkit.LayeredOptions[CollectionBundleConfig]{
@@ -69,13 +68,9 @@ func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
Validate: validateCollectionBundle, Validate: validateCollectionBundle,
}, },
Underlays: []configkit.Underlay{ Underlays: []configkit.Underlay{
{Inputs: []configkit.Input{ {Inputs: []configkit.Input{{
{Source: bundle.File("collections/weather/collection.yml"), Imports: true, Destination: collectionDestination}, Source: bundle.File("collections/weather/collection.yml"), Imports: true, Destination: collectionDestination,
{Source: bundle.File("collections/weather/prompts/brief.yml"), Destination: promptDestination}, }}},
{Source: bundle.File("collections/weather/prompts/custom.yml"), Destination: promptDestination},
{Source: bundle.File("collections/weather/profiles/compact.yml"), Destination: profileDestination},
{Source: bundle.File("collections/weather/profiles/weather.yml"), Destination: profileDestination},
}},
{Inputs: []configkit.Input{{ {Inputs: []configkit.Input{{
Source: bundle.File("operators/weather-prompt-version.yml"), Destination: promptDestination, Source: bundle.File("operators/weather-prompt-version.yml"), Destination: promptDestination,
}}}, }}},

View File

@@ -69,22 +69,43 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
t.Fatalf("ordinary collection version metadata = %#v, %v", collectionVersion, ok) t.Fatalf("ordinary collection version metadata = %#v, %v", collectionVersion, ok)
} }
var metadataSource configkit.SourceInfo var metadataSource configkit.SourceInfo
var collectionEntrySource configkit.SourceInfo
var versionSource configkit.SourceInfo
var parentPromptSource configkit.SourceInfo var parentPromptSource configkit.SourceInfo
var promptIndexSource configkit.SourceInfo
var profileIndexSource configkit.SourceInfo
var promptVersionSource configkit.SourceInfo var promptVersionSource configkit.SourceInfo
for _, source := range result.Sources() { for _, source := range result.Sources() {
switch source.Path { switch source.Path {
case "collections/weather/collection.yml":
collectionEntrySource = source
case "collections/weather/metadata.yml": case "collections/weather/metadata.yml":
metadataSource = source metadataSource = source
case "collections/weather/version.yml":
versionSource = source
case "collections/weather/prompts/index.yml":
promptIndexSource = source
case "collections/weather/prompts/brief.yml": case "collections/weather/prompts/brief.yml":
parentPromptSource = source parentPromptSource = source
case "collections/weather/profiles/index.yml":
profileIndexSource = source
case "operators/weather-prompt-version.yml": case "operators/weather-prompt-version.yml":
promptVersionSource = source promptVersionSource = source
} }
} }
collectionVersionLocation := collectionVersion.Origin.Locations[0] collectionVersionLocation := collectionVersion.Origin.Locations[0]
if metadataSource.ID == "" || collectionVersionLocation.SourceID != metadataSource.ID || if collectionEntrySource.ID != "source-2" || collectionEntrySource.Destination.String() != "/collections/weatherreporter" ||
metadataSource.Destination.String() != "/collections/weatherreporter" || collectionVersionLocation.Line != 1 { metadataSource.ID == "" || metadataSource.Destination.String() != "/collections/weatherreporter" ||
t.Fatalf("imported collection version source = %#v, %#v", metadataSource, collectionVersionLocation) metadataSource.ImportedFrom == nil || metadataSource.ImportedFrom.SourceID != collectionEntrySource.ID ||
versionSource.ID == "" || collectionVersionLocation.SourceID != versionSource.ID || versionSource.Destination.String() != "/collections/weatherreporter" ||
versionSource.ImportedFrom == nil || versionSource.ImportedFrom.SourceID != metadataSource.ID || collectionVersionLocation.Line != 1 {
t.Fatalf("nested collection version provenance = entry %#v, metadata %#v, version %#v, location %#v", collectionEntrySource, metadataSource, versionSource, collectionVersionLocation)
}
if promptIndexSource.ID == "" || promptIndexSource.Destination.String() != "/collections/weatherreporter/prompts" ||
promptIndexSource.ImportedFrom == nil || promptIndexSource.ImportedFrom.SourceID != collectionEntrySource.ID ||
profileIndexSource.ID == "" || profileIndexSource.Destination.String() != "/collections/weatherreporter/profiles" ||
profileIndexSource.ImportedFrom == nil || profileIndexSource.ImportedFrom.SourceID != collectionEntrySource.ID {
t.Fatalf("nested prompt/profile placement = prompt %#v profile %#v", promptIndexSource, profileIndexSource)
} }
if promptVersionSource.ID == "" || promptVersionSource.Destination.String() != "/collections/weatherreporter/prompts" || promptVersionSource.UnderlayIndex != 2 { if promptVersionSource.ID == "" || promptVersionSource.Destination.String() != "/collections/weatherreporter/prompts" || promptVersionSource.UnderlayIndex != 2 {
t.Fatalf("prompt version source = %#v", promptVersionSource) t.Fatalf("prompt version source = %#v", promptVersionSource)

View File

@@ -9,7 +9,7 @@ import (
configkit "gitea.maximumdirect.net/eric/ConfigKit" configkit "gitea.maximumdirect.net/eric/ConfigKit"
) )
//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle //go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports
var bundledFiles embed.FS var bundledFiles embed.FS
// BundledConfig is a small configuration loaded directly from an embedded file. // BundledConfig is a small configuration loaded directly from an embedded file.
@@ -18,6 +18,30 @@ type BundledConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
} }
type PromptBundleConfig struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Description string `yaml:"description"`
Prompts map[string]BundlePrompt `yaml:"prompts"`
}
type BundlePrompt struct {
Summary string `yaml:"summary"`
}
// LoadPromptBundle demonstrates ordinary Load with opt-in imports whose
// destinations are declared relative to the entry document.
func LoadPromptBundle() (*configkit.Result[PromptBundleConfig], error) {
files, err := fs.Sub(bundledFiles, "testdata/ordinary-imports")
if err != nil {
return nil, err
}
return configkit.Load[PromptBundleConfig](
configkit.NewFilesystem("prompt bundle", files).File("config.yml"),
configkit.Options[PromptBundleConfig]{Imports: true},
)
}
// LoadBundled demonstrates a native filesystem source without layered options. // LoadBundled demonstrates a native filesystem source without layered options.
func LoadBundled() (*configkit.Result[BundledConfig], error) { func LoadBundled() (*configkit.Result[BundledConfig], error) {
files, err := fs.Sub(bundledFiles, "testdata/native-source") files, err := fs.Sub(bundledFiles, "testdata/native-source")

View File

@@ -20,6 +20,34 @@ func Example_nativeFilesystem() {
// Output: embedded false // Output: embedded false
} }
func Example_rootRelativeImports() {
result, err := consumers.LoadPromptBundle()
if err != nil {
fmt.Println(err)
return
}
source := result.Sources()[2]
fmt.Println(result.Config.Name, result.Config.Version, result.Config.Prompts["brief"].Summary, source.Destination.String(), err)
// Output: Root import example 1.0 A root-loaded prompt. /prompts <nil>
}
func TestPromptBundleUsesRelativeImportDestinations(t *testing.T) {
result, err := consumers.LoadPromptBundle()
if err != nil {
t.Fatal(err)
}
if result.Config.Name != "Root import example" || result.Config.Version != "1.0" || result.Config.Prompts["brief"].Summary != "A root-loaded prompt." {
t.Fatalf("root import result = %#v", result.Config)
}
sources := result.Sources()
if len(sources) != 3 || sources[0].Destination.String() != configkit.Keys().String() ||
sources[1].Path != "metadata.yml" || sources[1].Destination.String() != configkit.Keys().String() ||
sources[2].Path != "prompts/brief.yml" || sources[2].Destination.String() != "/prompts" ||
sources[2].ImportedFrom == nil || sources[2].ImportedFrom.SourceID != sources[0].ID {
t.Fatalf("root import sources = %#v", sources)
}
}
func Example_layeredCatalog() { func Example_layeredCatalog() {
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml")) result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
if err != nil { if err != nil {

View File

@@ -1,4 +1,9 @@
configkit: configkit:
imports: imports:
- path: metadata.yml - path: metadata.yml
destination: []
- path: prompts/index.yml
destination: [prompts]
- path: profiles/index.yml
destination: [profiles]
name: Weather Reporter name: Weather Reporter

View File

@@ -1,2 +1,4 @@
version: "2.4.0" configkit:
imports:
- path: version.yml
description: Imported collection metadata. description: Imported collection metadata.

View File

@@ -0,0 +1,4 @@
configkit:
imports:
- path: compact.yml
- path: weather.yml

View File

@@ -0,0 +1,4 @@
configkit:
imports:
- path: brief.yml
- path: custom.yml

View File

@@ -0,0 +1 @@
version: "2.4.0"

View File

@@ -0,0 +1,6 @@
configkit:
imports:
- path: metadata.yml
- path: prompts/brief.yml
destination: [prompts]
name: Root import example

View File

@@ -0,0 +1,2 @@
version: "1.0"
description: Metadata imported into the root document.

View File

@@ -0,0 +1,2 @@
brief:
summary: A root-loaded prompt.