Demonstrate entry-declared import destinations
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
}}},
|
}}},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
version: "2.4.0"
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: version.yml
|
||||||
description: Imported collection metadata.
|
description: Imported collection metadata.
|
||||||
|
|||||||
4
examples/consumers/testdata/collection-bundle/collections/weather/profiles/index.yml
vendored
Normal file
4
examples/consumers/testdata/collection-bundle/collections/weather/profiles/index.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: compact.yml
|
||||||
|
- path: weather.yml
|
||||||
4
examples/consumers/testdata/collection-bundle/collections/weather/prompts/index.yml
vendored
Normal file
4
examples/consumers/testdata/collection-bundle/collections/weather/prompts/index.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: brief.yml
|
||||||
|
- path: custom.yml
|
||||||
1
examples/consumers/testdata/collection-bundle/collections/weather/version.yml
vendored
Normal file
1
examples/consumers/testdata/collection-bundle/collections/weather/version.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
version: "2.4.0"
|
||||||
6
examples/consumers/testdata/ordinary-imports/config.yml
vendored
Normal file
6
examples/consumers/testdata/ordinary-imports/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: metadata.yml
|
||||||
|
- path: prompts/brief.yml
|
||||||
|
destination: [prompts]
|
||||||
|
name: Root import example
|
||||||
2
examples/consumers/testdata/ordinary-imports/metadata.yml
vendored
Normal file
2
examples/consumers/testdata/ordinary-imports/metadata.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
version: "1.0"
|
||||||
|
description: Metadata imported into the root document.
|
||||||
2
examples/consumers/testdata/ordinary-imports/prompts/brief.yml
vendored
Normal file
2
examples/consumers/testdata/ordinary-imports/prompts/brief.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
brief:
|
||||||
|
summary: A root-loaded prompt.
|
||||||
Reference in New Issue
Block a user