Compare commits
435 Commits
315f464dfc
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| c88aac990b | |||
| 48c50e49d7 | |||
| 7ed4629c8d | |||
| c63f4ff687 | |||
| 68ec552b52 | |||
| e5475ea7f8 | |||
| cf9e2fd677 | |||
| eb87a10e1c | |||
| 54bdc24865 | |||
| ad5b859337 | |||
| 1fd0adc571 | |||
| 5b03ca6278 | |||
| b9f41d698e | |||
| fb8d56cfef | |||
| b27fe0598a | |||
| 74bff9704e | |||
| 1e69abc88c | |||
| 6c60f02b5a | |||
| d211537873 | |||
| 6db441893d | |||
| e74c26a4b3 | |||
| 08bb3a20c6 | |||
| b3bb57eda0 | |||
| 94d1e333d4 | |||
| ff10d03978 | |||
| b4477a236a | |||
| d818b08556 | |||
| 587f407a32 | |||
| 335bd7bc31 | |||
| 331195202e | |||
| 21c9bc0ad0 | |||
| 68400ffdfb | |||
| 5d5d51ab11 | |||
| 69bd9ead53 | |||
| 2ff588eb64 | |||
| c96197c417 | |||
| 24ab623cb2 | |||
| 1ce147c9f6 | |||
| 9833ca3aa3 | |||
| eafbd32489 | |||
| 9ac0053590 | |||
| 791cf16136 | |||
| 31ae30372e | |||
| 7ee9e92ef5 | |||
| 556b10b2cd | |||
| 07e4ae5d92 | |||
| 7548fa101a | |||
| 197567177e | |||
| 7330107389 | |||
| ad6d3748d9 | |||
| 1ee508ba02 | |||
| e9155fced6 | |||
| 14c83a6515 | |||
| 3ebb848ead | |||
| 4289f9ff7f | |||
| a050986916 | |||
| fb71421956 | |||
| b281adb751 | |||
| 1448f9b19d | |||
| 731149fc6d | |||
| 9ff873f54c | |||
| 3f389662e9 | |||
| 906ca34b6d | |||
| 17216a40f3 | |||
| 896c1cde08 | |||
| 91769e9f9b | |||
| 98c8b7c842 | |||
| 1954052631 | |||
| 3ffaef7ede | |||
| 6a1447c327 | |||
| 12fd0a7191 | |||
| 8155465322 | |||
| 408dd13c24 | |||
| 738c8a9cc7 | |||
| 4b4e15fc58 | |||
| 76521f7cd8 | |||
| cacc558288 | |||
| 231ae66857 | |||
| da74d4190d | |||
| 11f7154874 | |||
| 3828b117e8 | |||
| 2419e63717 | |||
| a893c7716c | |||
| 28ef9b6200 | |||
| 0dd16aa8a9 | |||
| f6d54a094a | |||
| f9ec66e867 | |||
| be5cdeb502 | |||
| 1f64673d0d | |||
| 01b08e76a3 | |||
| 453da684d7 | |||
| fb6c254d38 | |||
| a71413ce67 | |||
| d66338a1c5 | |||
| 643f43ce6a | |||
| c25cf39d19 | |||
| 7d18c93339 | |||
| b1d89810ca | |||
| bb5540b62e | |||
| 80cf23e710 | |||
| ac48557535 | |||
| b4af900c10 | |||
| f132cd6468 | |||
| 75e209eb3b | |||
| 1dd94a3cc6 | |||
| d172a5e46a | |||
| 3a1b6ff355 | |||
| 68383f34de | |||
| 549fca239d | |||
| 883244936c | |||
| a8ae2e9d5e | |||
| f18582106b | |||
| d8bf6abc2b | |||
| 4bf940c579 | |||
| 4fd3bee986 | |||
| 6f6130fd12 | |||
| 3122e2f32d | |||
| e4a5ecb4be | |||
| 826062aa17 | |||
| 08f2a8f3e9 | |||
| 12d350a292 | |||
| 4fcff5f453 | |||
| 247a12bc50 | |||
| 119b42c810 | |||
| 0f9ecf12ed | |||
| 7bf2f0c687 | |||
| 8aba43da38 | |||
| 877c497325 | |||
| 08a8c25542 | |||
| ea27f17f5c | |||
| ae3408a096 | |||
| deb9aa5c69 | |||
| 2736ddfbf2 | |||
| 7889ebddfb | |||
| 4a5316b7fc | |||
| e1e94eeec7 | |||
| 176e9a5d90 | |||
| 23c1976ef1 | |||
| aab79d9508 | |||
| ecd5d5d460 | |||
| b1c278b38b | |||
| eb5a82f1f3 | |||
| b30c9f167d | |||
| 5925066d35 | |||
| 0cca633a67 | |||
| a80273433e | |||
| 320d0c5f43 | |||
| 5098b56655 | |||
| 47944d22d1 | |||
| 50199eb0af | |||
| 34cbc0d63b | |||
| 5e12b2d360 | |||
| 8dfaf44d1d | |||
| 64be8b4df5 | |||
| e585decdb2 | |||
| 6440a500f7 | |||
| d898cb193d | |||
| 557e006a4a | |||
| 3fd83b6b2b | |||
| 0fbce5d141 | |||
| ac4ce242a2 | |||
| fa6e876ab5 | |||
| c5275df1fc | |||
| f404168900 | |||
| 729c6fb5d5 | |||
| d314a269d5 | |||
| 2c3eb3308b | |||
| 6bc625b68f | |||
| b915dd776a | |||
| 9a64a65b6c | |||
| 6158e9db3b | |||
| 0d149e51f0 | |||
| bd51b9c3f2 | |||
| d05426d8a2 | |||
| 3f0efcb4ff | |||
| 19f5c2a2d5 | |||
| 455a4a355d | |||
| 8ab1568352 | |||
| 8a99e61070 | |||
| de7513d488 | |||
| 26771dc03e | |||
| fbaaaf18cd | |||
| c9574ef3c1 | |||
| 5959537d1a | |||
| 0369111d30 | |||
| 2fedb0307f | |||
| 087050fa25 | |||
| 1ac1f8bc9e | |||
| 4dc3e70877 | |||
| f64497cb13 | |||
| 68cf5c489b | |||
| e50fee2ca5 | |||
| eec214944c | |||
| 4f2c113237 | |||
| 2d93a5c703 | |||
| 2f6c1eca00 | |||
| 8642337966 | |||
| 53ce6c63d8 | |||
| 2f0900535d | |||
| 4748313105 | |||
| e2634c8815 | |||
| 424c89780e | |||
| 92309ddcc3 | |||
| 7ff204bf3d | |||
| b3ec860fac | |||
| 435dea102f | |||
| ea46001f78 | |||
| a0e5693d07 | |||
| 5c5f2af6cc | |||
| 8253993005 | |||
| d0848290da | |||
| e94a0642ff | |||
| 571d063675 | |||
| d0a605afbb | |||
| 7a83704f43 | |||
| ac5bf29ddc | |||
| cce8356b8e | |||
| b746dcc609 | |||
| 9c32b8d3b5 | |||
| c73daf891d | |||
| ae038a5165 | |||
| 367bbaaa1b | |||
| fafc28421f | |||
| 1a673753b9 | |||
| 988e49bb09 | |||
| 5bb4f364b7 | |||
| af84107905 | |||
| f94dd9b961 | |||
| 146e7e56d9 | |||
| d2e20a304a | |||
| 8d40983271 | |||
| 25cf7ce05c | |||
| 57edb8ec19 | |||
| 42663cf07c | |||
| 085b57eeb6 | |||
| dcf0379e02 | |||
| 8ecfedc91b | |||
| f7624ae92a | |||
| a035df9845 | |||
| e6598810e0 | |||
| f8cbb2ed87 | |||
| af80ccb90c | |||
| 0be7cdb00e | |||
| e01aeca761 | |||
| d4133f23f5 | |||
| 5af71a7533 | |||
| 41b5dff69b | |||
| ecfb78d84d | |||
| 041c4cff02 | |||
| 23f22d8982 | |||
| 0023617a07 | |||
| 33a51606da | |||
| 268fd03579 | |||
| 9d4d2e2a17 | |||
| 36ff9170b7 | |||
| 9e75496664 | |||
| 80d8c1dc82 | |||
| acd9e9a9fa | |||
| 89a9684f0d | |||
| 9a7027a04c | |||
| dfeaf90320 | |||
| d03e56398b | |||
| 628fc53d2c | |||
| 5fec16a768 | |||
| f6b02dec03 | |||
| 7b04dc6a50 | |||
| 12fdf0d331 | |||
| b202dd187f | |||
| 0314e587b6 | |||
| 9d7635d1e4 | |||
| 298e70b552 | |||
| 64c51647f0 | |||
| bc6b977585 | |||
| d06aae5461 | |||
| 4f941f8ba4 | |||
| 9a49833339 | |||
| 749cf6868c | |||
| a5f0b92ee9 | |||
| 6ec23b2de1 | |||
| 3298eb916c | |||
| 40430e787c | |||
| f434fa159f | |||
| 8ddf73ede3 | |||
| 844a15a3cb | |||
| 58f4c39d6f | |||
| 8a54cecd1d | |||
| 0e430e8077 | |||
| 26caa6024e | |||
| 8691a93ebf | |||
| c563f80934 | |||
| 27797d2392 | |||
| 6a4e57e5d3 | |||
| 29f94d89e1 | |||
| 12f6045da0 | |||
| 6eb80b06e3 | |||
| 86feb6d755 | |||
| 8bf49f6ea6 | |||
| f3d0fe6411 | |||
| cba3a0efb0 | |||
| b58cd75c98 | |||
| c530a989df | |||
| 2baf14d9a5 | |||
| 6903c9e51a | |||
| 31e10af9cb | |||
| 0d841d9d73 | |||
| 4bcefbc9d4 | |||
| 91db99b967 | |||
| 5747ec49c6 | |||
| d3a714a1cb | |||
| 7f8ad81065 | |||
| 75da54150f | |||
| 38fb315415 | |||
| 1c79ec3aa1 | |||
| 01e1b2fc2d | |||
| 15943e233c | |||
| f6a6b0184b | |||
| 898035d83a | |||
| de3666aed3 | |||
| c00811da2e | |||
| 55abcf8340 | |||
| 9d7c79cf0b | |||
| 24131617c7 | |||
| 05b67cbfcf | |||
| e433988962 | |||
| 05a28d1178 | |||
| 1e5bffc0e6 | |||
| f5a689b578 | |||
| e4abff4164 | |||
| 1a8f1cc3a5 | |||
| 9b99463187 | |||
| f69fa24b40 | |||
| 77d97296ea | |||
| 032679bf34 | |||
| 55a59de6db | |||
| a0d13962bd | |||
| 5f33e1e90c | |||
| b750b0fec6 | |||
| b498e5bee5 | |||
| ba577429e6 | |||
| 014b99504b | |||
| 263dd2f1bd | |||
| af43d6f438 | |||
| a4cbd6390e | |||
| 5ed0f55970 | |||
| 91ab37a39d | |||
| fbe9ae6d8b | |||
| 1aa121df71 | |||
| 6e40c166d6 | |||
| a56d4691ff | |||
| 2718fbb8fe | |||
| 2d52421919 | |||
| 167fcd475f | |||
| aa93aeda3f | |||
| 94381a67fd | |||
| c26b34147b | |||
| c9f9c98fbe | |||
| f6bf4454b0 | |||
| a7ca2c59a2 | |||
| 10c14aa794 | |||
| e9774b252b | |||
| 19fe621df1 | |||
| 82b289f4fe | |||
| 137769f3f1 | |||
| ef28eabaf8 | |||
| 008d2ba9c8 | |||
| 8e3a0892d4 | |||
| 023e428b9c | |||
| 527a38daf1 | |||
| 30bcd8eeb6 | |||
| 884215c403 | |||
| 8a89806e44 | |||
| 26720b98f0 | |||
| 4633a1b8a2 | |||
| 87986a12d2 | |||
| 0858af1029 | |||
| acddce8e2f | |||
| 77c5d9482a | |||
| adef16ccdd | |||
| c19825620c | |||
| 190431740a | |||
| 25cf6d32b6 | |||
| 1a04bb3835 | |||
| 1cf3b0d78f | |||
| 66dced0823 | |||
| b1b5163cc3 | |||
| bb9bde799a | |||
| dc7a70811a | |||
| ff212885c4 | |||
| 90c2d7e409 | |||
| 60e6e2697f | |||
| 20511773fc | |||
| 31fb6a9bd1 | |||
| 103e10639a | |||
| 97c5a0390a | |||
| 8b547765f0 | |||
| 5215cf5c4f | |||
| e66c37161c | |||
| e4270c9b14 | |||
| e69ad0f32d | |||
| 0a2246bdbe | |||
| e5a6df7af7 | |||
| 25cb3c47e7 | |||
| 020d72084d | |||
| 358b28c421 | |||
| b0550af6a3 | |||
| 43c994a32a | |||
| 1e47ee21cd | |||
| fed591183b | |||
| 1f95cf905a | |||
| 15f5b591b9 | |||
| 184f29c7af | |||
| 813d6a38c9 | |||
| 23ae700194 | |||
| 49218a836b | |||
| 2fcf892324 | |||
| 8645cab79a | |||
| 70c95bf660 | |||
| d9f08ff719 | |||
| 2c91e41d10 | |||
| e3860036f8 | |||
| b3fc6a5950 | |||
| a858a066e4 | |||
| 10edd95d4d | |||
| 08e03f6a38 | |||
| a20ba0af27 | |||
| 6458dbd3aa | |||
| 43e55dfa85 | |||
| 430985c629 | |||
| 4635ef08d5 | |||
| 49c0c6d908 | |||
| a8beb3e620 | |||
| 934c1022e0 | |||
| 7248a8bd95 | |||
| b2db3233b2 | |||
| edb6d05ec1 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -28,6 +28,9 @@ go.work.sum
|
||||
# Local codebase-memory tooling artifacts
|
||||
/.codebase-memory/
|
||||
|
||||
# Local release credentials and tooling
|
||||
/.releasekit/
|
||||
|
||||
# ---> Linux
|
||||
*~
|
||||
|
||||
|
||||
@@ -18,8 +18,3 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
Third-party code and assets are subject to their respective license terms.
|
||||
|
||||
Policy documents adapted from Standard Repository Policies, the root AGENTS.md
|
||||
orientation, and the Policy Reading Guide in docs/development.md remain licensed
|
||||
under CC-BY-SA-4.0. See docs/oss/attribution.md for the affected material,
|
||||
upstream credit, and license links.
|
||||
|
||||
106
README.md
106
README.md
@@ -1,32 +1,18 @@
|
||||
# ConfigKit
|
||||
|
||||
An opinionated Go library for applications that use YAML configuration files.
|
||||
ConfigKit loads YAML into your application's Go configuration type, combines the
|
||||
inputs you choose, and lets you inspect where the final values came from. You
|
||||
supply the types, defaults, and application rules; ConfigKit handles loading,
|
||||
combination, strict decoding, and inspection.
|
||||
|
||||
Linux is release-validated; macOS support is best-effort. See the
|
||||
[platform support policy](docs/policy/release.md#platform-support) for details.
|
||||
[platform support policy](docs/policy/release.md#platform-support).
|
||||
|
||||
ConfigKit's simple `Load` accepts one strict YAML source (or explicit
|
||||
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
|
||||
source groups with an independently selected main input. Sources can be host
|
||||
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
|
||||
Enabled path and glob imports compose strictly within their group; precedence
|
||||
between groups combines mappings recursively. Both entry points return a complete,
|
||||
validated configuration with detached metadata and safe diagnostics.
|
||||
Applications may perform audited defaulting and normalization through a resolver,
|
||||
then report domain errors and warnings through read-only semantic validation. Secret
|
||||
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
||||
references resolve environment names before validation, and explicit
|
||||
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
||||
CLI override records apply above overlays. Value explanation is available through
|
||||
detached structured records and deterministic redacted output. Built-in
|
||||
`extends` relationships let a mapping inherit from a named sibling during the
|
||||
same load, retaining explicit presence and source/derivation history. The root
|
||||
`Diff` API compares frozen effective values from two successful same-type loads;
|
||||
the [configuration contract](docs/config.md#effective-configuration-diffs) and
|
||||
[consumer guide](docs/consumers/README.md#effective-configuration-diffs) describe
|
||||
its safe changed-region records and maintained workflows.
|
||||
## Load a configuration file
|
||||
|
||||
```go
|
||||
import "go.maximumdirect.net/configkit"
|
||||
|
||||
type Config struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Workers int `yaml:"workers"`
|
||||
@@ -41,41 +27,57 @@ func ReadConfig(path string) (Config, error) {
|
||||
}
|
||||
```
|
||||
|
||||
Existing consumers should follow the
|
||||
[module-path migration guidance](docs/consumers/README.md#vanity-module-path-migration).
|
||||
|
||||
## Add the features you need
|
||||
|
||||
Start with one file. Optional features use the same loading and inspection model:
|
||||
|
||||
- [Choose inputs](docs/consumers/README.md#choosing-a-loading-entry-point): read
|
||||
files, an application-owned `fs.FS`, bytes, a reader, or a Go mapping. Use
|
||||
explicit search descriptions when the application should look in several places.
|
||||
- [Supply lower-priority settings](docs/consumers/README.md#native-filesystem-and-layered-input):
|
||||
use `LoadLayered` for bundled defaults that application configuration can override.
|
||||
- [Combine configuration](docs/config.md#explicit-imports): add declared imports,
|
||||
select one overlay, or pass values explicitly set by your CLI parser.
|
||||
- [Reuse settings](docs/config.md#sibling-inheritance): let one named entry inherit
|
||||
from a sibling. [Identified lists](docs/config.md#strict-document-composition)
|
||||
also support record inheritance and replace, append, or upsert operations.
|
||||
- [Apply application rules](docs/config.md#recorded-application-resolution): derive
|
||||
or normalize values in a resolver, then check the final configuration in a validator.
|
||||
- [Handle credentials](docs/config.md#typed-secret-references): use environment-name
|
||||
references and choose which configuration fields to hide in inspection output.
|
||||
- [Inspect a load](docs/config.md#layered-metadata): examine sources, presence,
|
||||
history, safe YAML output, or [changes between two loads](docs/config.md#effective-configuration-diffs).
|
||||
|
||||
The [consumer guide](docs/consumers/README.md) walks through these tasks and links
|
||||
to maintained examples. The [configuration reference](docs/config.md) defines the
|
||||
exact YAML, combination, and lifecycle rules.
|
||||
|
||||
## Design philosophy
|
||||
|
||||
ConfigKit favors one coherent, predictable configuration model over flexibility
|
||||
for reproducing each consumer's conventions. Its goal is to let applications
|
||||
load once, inspect where final values came from, and distinguish explicit values
|
||||
from omissions with minimal configuration bookkeeping. Applications retain their
|
||||
domain rules and execution responsibilities.
|
||||
ConfigKit favors consistent, predictable rules across applications. A load returns
|
||||
the final configuration and metadata that distinguish supplied values such as
|
||||
`false` and `0` from omissions. Applications keep their domain rules and runtime
|
||||
initialization.
|
||||
|
||||
Loading is synchronous and complete on return. Applications normally load once
|
||||
near startup and then use the final configuration and metadata; ConfigKit stays
|
||||
inactive until explicitly called again. Applications may initiate additional
|
||||
loads as needed. See the [loading lifecycle](docs/config.md#completion-and-resource-lifetime)
|
||||
for completion and ownership details.
|
||||
Within one layer, independent contributions combine strictly: conflicting
|
||||
values are errors. Across layers, higher-priority settings can replace
|
||||
lower-priority settings, while mappings combine by key. Identified lists have
|
||||
their declared combination rules. See the [worked combination
|
||||
examples](docs/config.md#strict-document-composition) for the details and
|
||||
exceptions.
|
||||
|
||||
Within a configuration tier, independent contributions compose strictly: shared
|
||||
mappings combine, while overlapping non-mapping definitions are errors. Between
|
||||
precedence tiers, mappings combine recursively, retaining omitted lower-priority
|
||||
children; other values replace lower-priority values. The main document and its
|
||||
imports form one strictly composed tier. Explicit underlay groups sit below it
|
||||
and above typed defaults; a selected overlay and explicit CLI overrides sit
|
||||
above it. The [configuration contract](docs/config.md#strict-document-composition)
|
||||
owns the detailed combination rules and input-specific constraints.
|
||||
Loading finishes before the call returns. ConfigKit then stays inactive until you
|
||||
call it again; inspection uses the saved result. See
|
||||
[completion and resource lifetime](docs/config.md#completion-and-resource-lifetime).
|
||||
|
||||
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
||||
these priorities and how genuine new needs can justify extending the shared
|
||||
model. The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions.
|
||||
|
||||
The [configuration contract](docs/config.md) describes implemented behavior,
|
||||
including [input destinations](docs/config.md#input-destinations). The
|
||||
[consumer adoption guide](docs/consumers/README.md) provides maintained simple,
|
||||
layered, and mounted-collection examples alongside the migration shapes. The
|
||||
accepted placement decision is recorded in [ADR 0011](docs/adr/0011-input-destinations.md).
|
||||
See the [development guide](docs/development.md) for repository orientation and
|
||||
the [release procedure](docs/release.md) for maintainer-only candidate and
|
||||
publication steps.
|
||||
The [architecture goals](docs/policy/architecture.md#success-criteria) and
|
||||
[ADRs](docs/adr) explain design decisions, including shared import and inheritance
|
||||
ordering. [Future work](docs/roadmap/future.md) lists possible extensions.
|
||||
Contributors can start with the [development guide](docs/development.md);
|
||||
release maintainers use the [release procedure](docs/release.md).
|
||||
|
||||
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
func fromEngineLocation(value engine.Location) Location {
|
||||
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
|
||||
@@ -43,10 +43,61 @@ func toEngineLocations(values []Location) []engine.Location {
|
||||
return result
|
||||
}
|
||||
func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
||||
return SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination), Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex, ImportedFrom: fromEngineLocationPointer(value.ImportedFrom)}
|
||||
return SourceInfo{
|
||||
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
|
||||
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
|
||||
DestinationIsFallback: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
||||
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
|
||||
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionEvidence: fromEngineSourceVersionEvidence(value.VersionCheck),
|
||||
EmptyImportSelections: fromEngineEmptyImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
||||
}
|
||||
}
|
||||
func toEngineSourceInfo(value SourceInfo) engine.SourceInfo {
|
||||
return engine.SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Destination: toEnginePath(value.Destination), Layer: engine.Layer(value.Layer), Kind: engine.SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex, ImportedFrom: toEngineLocationPointer(value.ImportedFrom)}
|
||||
func fromEngineEmptyImportSelections(values []engine.ImportSelectionForFacade) []EmptyImportSelection {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]EmptyImportSelection, len(values))
|
||||
for index, value := range values {
|
||||
result[index] = EmptyImportSelection{
|
||||
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
|
||||
Selector: ImportSelector(value.Selector),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toEngineOverrides(values []Override) []engine.Override {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]engine.Override, len(values))
|
||||
for index, value := range values {
|
||||
result[index] = engine.Override{
|
||||
Path: toEnginePath(value.Path), Value: value.Value, Flag: value.Flag,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
|
||||
return engine.ImportDefaults{
|
||||
Required: value.Required,
|
||||
Follow: value.FollowSymlinks,
|
||||
Hidden: value.IncludeHidden,
|
||||
Recursive: value.Recursive,
|
||||
Pattern: value.Pattern,
|
||||
}
|
||||
}
|
||||
|
||||
func fromEngineSourceVersionEvidence(value *engine.SourceVersionCheck) *SourceVersionEvidence {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &SourceVersionEvidence{
|
||||
Path: fromEnginePath(value.Path),
|
||||
Version: Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
|
||||
Location: fromEngineLocationPointer(value.Location), Redacted: value.Redacted,
|
||||
}
|
||||
}
|
||||
func fromEngineOrigin(value engine.Origin) Origin {
|
||||
result := Origin{Layer: Layer(value.Layer), Locations: fromEngineLocations(value.Locations), Operation: ChangeKind(value.Operation)}
|
||||
@@ -61,19 +112,6 @@ func fromEngineOrigin(value engine.Origin) Origin {
|
||||
}
|
||||
return result
|
||||
}
|
||||
func toEngineOrigin(value Origin) engine.Origin {
|
||||
result := engine.Origin{Layer: engine.Layer(value.Layer), Locations: toEngineLocations(value.Locations), Operation: engine.ChangeKind(value.Operation)}
|
||||
if value.Inheritance != nil {
|
||||
result.Inheritance = make([]engine.InheritanceStep, len(value.Inheritance))
|
||||
for index, step := range value.Inheritance {
|
||||
result.Inheritance[index] = engine.InheritanceStep{
|
||||
From: toEnginePath(step.From), To: toEnginePath(step.To), Reference: toEnginePath(step.Reference),
|
||||
Locations: toEngineLocations(step.Locations),
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
func fromEngineDiagnostic(value engine.Diagnostic) Diagnostic {
|
||||
return Diagnostic{Code: Code(value.Code), Severity: Severity(value.Severity), Path: fromEnginePath(value.Path), Message: value.Message, Primary: fromEngineLocationPointer(value.Primary), Related: fromEngineLocations(value.Related)}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package configkit
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
"go.maximumdirect.net/configkit/internal/engine"
|
||||
)
|
||||
|
||||
// LoadError reports safe structured diagnostics for a failed configuration load.
|
||||
@@ -50,6 +50,21 @@ func (e *LoadError) Sources() []SourceInfo {
|
||||
return result
|
||||
}
|
||||
|
||||
// Source returns a detached record from the sanitized source evidence retained
|
||||
// before loading failed, including placement evidence available at that point.
|
||||
// IDs are local to the failed load. It does not acquire sources or access the
|
||||
// filesystem. Empty or unknown IDs return false.
|
||||
func (e *LoadError) Source(id string) (SourceInfo, bool) {
|
||||
if e == nil || e.value == nil {
|
||||
return SourceInfo{}, false
|
||||
}
|
||||
value, ok := e.value.Source(id)
|
||||
if !ok {
|
||||
return SourceInfo{}, false
|
||||
}
|
||||
return fromEngineSourceInfo(value), true
|
||||
}
|
||||
|
||||
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
|
||||
// control characters and propagates writer errors, including short writes.
|
||||
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
|
||||
|
||||
22
diff.go
22
diff.go
@@ -1,15 +1,14 @@
|
||||
package configkit
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
// Difference describes one canonical changed region between two successful
|
||||
// loads. Left is the earlier value and Right is the later value. Each snapshot
|
||||
// is detached; Present distinguishes absence from an explicit null, and
|
||||
// Redacted reports whether combined sensitivity affected that side. Mappings
|
||||
// are compared recursively, while sequences are reported as atomic values.
|
||||
// Records are ordered by canonical JSON pointer. HiddenChanged indicates that
|
||||
// a value concealed by combined sensitivity changed, without revealing which
|
||||
// concealed member changed.
|
||||
// Difference describes one changed value or subtree between two successful loads.
|
||||
// Left is the earlier value and Right is the later value. Snapshots are copies;
|
||||
// Present distinguishes absence from explicit null. Redacted reports hiding
|
||||
// based on sensitivity from either load. Mappings compare by key; a changed
|
||||
// sequence is reported as a whole value. Records are ordered by canonical JSON
|
||||
// pointer. HiddenChanged reports an actual hidden change, without identifying
|
||||
// the hidden member. Redaction alone does not set it.
|
||||
type Difference struct {
|
||||
Path Path
|
||||
Left ValueSnapshot
|
||||
@@ -17,8 +16,9 @@ type Difference struct {
|
||||
HiddenChanged bool
|
||||
}
|
||||
|
||||
// Diff compares the frozen effective values of two successful loads of the
|
||||
// same configuration type. It does not reread sources, environment variables,
|
||||
// Diff compares the saved final values of two successful loads of the same
|
||||
// configuration type, independently of later changes to Result.Config.
|
||||
// It does not reread sources, environment variables,
|
||||
// or rerun defaults or resolvers. Equal values produce no records even when
|
||||
// their sources, defaults, or resolution histories differ. Credential contents
|
||||
// are not compared: typed secret references compare by reference name only.
|
||||
|
||||
90
discovery.go
Normal file
90
discovery.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package configkit
|
||||
|
||||
import "go.maximumdirect.net/configkit/internal/engine"
|
||||
|
||||
// Search is an immutable ordered description of possible host-file inputs.
|
||||
// It is consumed only when passed to SearchConfig and used by a load.
|
||||
type Search struct{ value engine.Search }
|
||||
|
||||
// FilenameGroup selects filenames in order for a directory search.
|
||||
// Construct groups with NamedFilenames, GenericFilenames, or CustomFilenames.
|
||||
type FilenameGroup struct{ value engine.FilenameGroup }
|
||||
|
||||
// SearchOptions controls whether a search must select a file.
|
||||
type SearchOptions struct {
|
||||
// Required reports a missing-source error if the complete search is exhausted.
|
||||
Required bool
|
||||
}
|
||||
|
||||
// DirectorySearchOptions selects the filename groups used by SearchDirectory.
|
||||
type DirectorySearchOptions struct {
|
||||
// Filenames concatenates groups in order. A nil slice uses Named then Generic;
|
||||
// a non-nil empty slice selects no filenames.
|
||||
Filenames []FilenameGroup
|
||||
}
|
||||
|
||||
// NamedFilenames selects <application>.yml followed by <application>.yaml.
|
||||
func NamedFilenames() FilenameGroup { return FilenameGroup{value: engine.NamedFilenames()} }
|
||||
|
||||
// GenericFilenames selects config.yml, config.yaml, main.yml, and main.yaml.
|
||||
func GenericFilenames() FilenameGroup { return FilenameGroup{value: engine.GenericFilenames()} }
|
||||
|
||||
// CustomFilenames selects literal filename components in the supplied order.
|
||||
// Names cannot be empty or contain a directory component.
|
||||
func CustomFilenames(names ...string) FilenameGroup {
|
||||
return FilenameGroup{value: engine.CustomFilenames(names...)}
|
||||
}
|
||||
|
||||
// SearchDirectory describes a search within directory. Omitted options or a nil
|
||||
// Filenames slice uses Named then Generic groups. Explicitly empty Filenames
|
||||
// selects no names and does not resolve the directory base.
|
||||
func SearchDirectory(directory string, supplied ...DirectorySearchOptions) Search {
|
||||
options := make([]engine.DirectorySearchOptions, len(supplied))
|
||||
for index, option := range supplied {
|
||||
if option.Filenames == nil {
|
||||
continue
|
||||
}
|
||||
options[index].Filenames = make([]engine.FilenameGroup, len(option.Filenames))
|
||||
for groupIndex, group := range option.Filenames {
|
||||
options[index].Filenames[groupIndex] = group.value
|
||||
}
|
||||
}
|
||||
return Search{value: engine.SearchDirectory(directory, options...)}
|
||||
}
|
||||
|
||||
// SearchPaths describes literal host-file candidates in the supplied order.
|
||||
func SearchPaths(paths ...string) Search { return Search{value: engine.Paths(paths...)} }
|
||||
|
||||
// Searches concatenates descriptions in order and snapshots their entries.
|
||||
func Searches(searches ...Search) Search {
|
||||
values := make([]engine.Search, len(searches))
|
||||
for index, search := range searches {
|
||||
values[index] = search.value
|
||||
}
|
||||
return Search{value: engine.Searches(values...)}
|
||||
}
|
||||
|
||||
// UnixSystemDirectories describes the conventional Unix system directories.
|
||||
// Loading on an unsupported platform fails if this preset is reached.
|
||||
func UnixSystemDirectories() Search { return Search{value: engine.UnixSystemDirectories()} }
|
||||
|
||||
// UserConfigDirectory describes searches rooted at the Go user-config directory.
|
||||
func UserConfigDirectory() Search { return Search{value: engine.UserConfigDirectory()} }
|
||||
|
||||
// HomeDirectory describes searches rooted at the Go home directory.
|
||||
func HomeDirectory() Search { return Search{value: engine.HomeDirectory()} }
|
||||
|
||||
// WorkingDirectory describes application-named files in the current directory.
|
||||
func WorkingDirectory() Search { return Search{value: engine.WorkingDirectory()} }
|
||||
|
||||
// SearchConfig creates a source that selects the first matching host file when
|
||||
// consumed by Load or LoadLayered. Without Required, exhausting the search uses
|
||||
// the absent-input lifecycle. Description and option errors are reported while
|
||||
// loading.
|
||||
func SearchConfig(application string, search Search, supplied ...SearchOptions) Source {
|
||||
options := make([]engine.SearchOptions, len(supplied))
|
||||
for index, option := range supplied {
|
||||
options[index] = engine.SearchOptions{Required: option.Required}
|
||||
}
|
||||
return Source{value: engine.SearchConfig(application, search.value, options...)}
|
||||
}
|
||||
22
doc.go
22
doc.go
@@ -1,9 +1,17 @@
|
||||
// Package configkit provides deterministic, application-owned configuration
|
||||
// loading for Go programs.
|
||||
// Package configkit loads YAML into an application's Go configuration type and
|
||||
// lets the application inspect where the final values came from.
|
||||
//
|
||||
// Load supports file, byte, reader, and explicit no-input sources with strict YAML,
|
||||
// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached
|
||||
// metadata, recorded application resolution, typed secret references, safe effective
|
||||
// rendering, read-only semantic validation, and structured load errors. The
|
||||
// configuration contract is documented in docs/config.md.
|
||||
// Start with Load and a File, Bytes, Reader, Mapping, or Filesystem.File source.
|
||||
// Use NoSource for deliberately absent input or SearchConfig for an explicitly
|
||||
// chosen search. Applications supply types, defaults, version rules, and callbacks
|
||||
// for deriving and validating values. LoadLayered adds lower-priority input groups.
|
||||
//
|
||||
// A successful load returns the final configuration and saved inspection state.
|
||||
// Metadata tracks supplied values, including false and zero; explanations retain
|
||||
// safe earlier values. WriteEffective and Diff use the saved state even if the
|
||||
// application later changes Result.Config. Secret references resolve credentials
|
||||
// during loading, while inspection follows the configured hiding rules.
|
||||
//
|
||||
// The consumer guide is in docs/consumers/README.md. The exact YAML, combination,
|
||||
// lifecycle, and ownership rules are documented in docs/config.md.
|
||||
package configkit
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# ADR 0005: Record explicit CLI overrides and safe value explanation
|
||||
|
||||
Status: accepted
|
||||
Status: Partially superseded by
|
||||
[ADR 0018](0018-cli-after-structural-resolution.md) for override-before-decoding
|
||||
ordering. The remaining ownership and safe explanation decisions remain accepted.
|
||||
|
||||
Date: 2026-09-06
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed draft for review. Completed planning and remediation records are
|
||||
Proposed draft for review; its whole-sequence precedence replacement rule is
|
||||
superseded by [ADR 0017](0017-collection-sequence-policy.md). The remainder
|
||||
retains its proposed status. Completed planning and remediation records are
|
||||
preserved in Git history at `b604aa1` under `docs/roadmap/`; the
|
||||
[configuration contract](../config.md#layered-loading) owns current behavior.
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented. The [configuration contract](../config.md#sibling-inheritance)
|
||||
owns current behavior. The completed implementation plan is preserved in Git
|
||||
Partially superseded by [ADR 0017](0017-collection-sequence-policy.md) for
|
||||
sequence combination and the addition of identified-sequence sibling scopes,
|
||||
and [ADR 0018](0018-cli-after-structural-resolution.md) for CLI-before-inheritance
|
||||
ordering. Existing mapping-sibling scopes and the remaining ownership and
|
||||
inspection decisions remain accepted.
|
||||
|
||||
The [configuration contract](../config.md#sibling-inheritance) owns implemented
|
||||
behavior. The completed implementation plan is preserved in Git
|
||||
history at `83c59ea`.
|
||||
|
||||
## Date
|
||||
|
||||
@@ -2,9 +2,16 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. The [input-destinations roadmap](../roadmap/input-destinations.md)
|
||||
owns implementation status and the [implementation plan](../roadmap/implementation.md)
|
||||
owns sequencing and completion evidence.
|
||||
Accepted and implemented. Current placement behavior is owned by the
|
||||
[configuration contract](../config.md#input-destinations). The accepted
|
||||
relative-import extension is recorded in
|
||||
[ADR 0012](0012-relative-import-destinations.md). The completed roadmap, consumer
|
||||
request, and review records are preserved at `e4270c9` under `docs/roadmap/` in
|
||||
Git history; the original input-destination implementation plan remains at
|
||||
`315f464:docs/roadmap/implementation.md`.
|
||||
|
||||
[ADR 0020](0020-document-root-controls.md) amends the scope of the control-name
|
||||
reservation. The other placement decisions in this record remain in force.
|
||||
|
||||
## Date
|
||||
|
||||
|
||||
78
docs/adr/0012-relative-import-destinations.md
Normal file
78
docs/adr/0012-relative-import-destinations.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# ADR 0012: Place Imported Configuration at Relative Destinations
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented. The
|
||||
[configuration contract](../config.md#input-destinations) owns current behavior.
|
||||
The completed roadmap and review records are preserved at `e4270c9` under
|
||||
`docs/roadmap/` in Git history; the relative-import implementation plan remains
|
||||
at `d9f08ff:docs/roadmap/implementation.md`.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-14
|
||||
|
||||
## Context
|
||||
|
||||
ConfigKit can place an input document at a literal mapping path while retaining
|
||||
source identity, composition, and inspection in its normal loading lifecycle.
|
||||
An importing document can currently bring in other documents, but those
|
||||
contributions inherit only the importing input's destination. Consumers that
|
||||
assemble a collection therefore cannot declare the layout of imported
|
||||
fragments alongside the import itself without rewriting source documents or
|
||||
performing another merge outside ConfigKit.
|
||||
|
||||
The extension should reuse input placement and the existing import lifecycle.
|
||||
Logical application placement must remain independent from physical path
|
||||
resolution, source identity, precedence, and resource ownership.
|
||||
|
||||
## Decision
|
||||
|
||||
Add an optional `destination` field to each YAML import declaration. Its value
|
||||
is a sequence of strings representing literal mapping-key segments. Omission
|
||||
and an empty sequence add no segments. A non-empty destination appends to the
|
||||
effective destination of the immediately importing document; nested imports
|
||||
continue from the resulting child destination. Every match of one glob
|
||||
declaration receives the same computed destination.
|
||||
|
||||
Use the existing input-destination validation and reserved-key rules. Do not
|
||||
interpret destination segments as filesystem paths, separators, sequence
|
||||
indices, or root-reset instructions. Physical import paths continue to resolve
|
||||
from their declaring source under existing containment rules.
|
||||
|
||||
Imports remain in their existing strict-composition group. A destination does
|
||||
not grant precedence, bypass cycle or repeated-source rules, or change source
|
||||
identity. Existing placement, version ownership, source records, diagnostics,
|
||||
resource contexts, inheritance, history, and inspection mechanisms continue to
|
||||
govern the placed contributions. No public Go entry point or independent
|
||||
specification-header behavior is introduced.
|
||||
|
||||
This is additive YAML functionality and is minor-release work. Version
|
||||
selection, release notes, roadmap retirement, and publication remain separate
|
||||
release-preparation work.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Require complete application-root destinations on every import.** This
|
||||
avoids inherited context but repeats collection prefixes in nested source
|
||||
declarations and does not express placement relative to the importing
|
||||
document.
|
||||
- **Encode destinations as dotted or slash-delimited strings.** This introduces
|
||||
separator and escaping rules even though existing paths already represent
|
||||
literal mapping keys as segments.
|
||||
- **Rewrite or merge imported documents in consumers.** This duplicates source
|
||||
placement, strict composition, attribution, and inspection behavior outside
|
||||
ConfigKit's loading lifecycle.
|
||||
- **Add independent specification headers or collection-specific validation.**
|
||||
These are separate design questions and are not required to declare relative
|
||||
placement.
|
||||
|
||||
## Consequences
|
||||
|
||||
Import declarations can describe the logical placement of their contributions
|
||||
while preserving the physical source and existing composition lifecycle.
|
||||
Placement is immediate-parent-relative, so nested imports accumulate their
|
||||
declared segments and sibling declarations remain independent. The feature is
|
||||
implemented behind the existing public loading surface; detailed validation,
|
||||
diagnostic, accounting, and inspection requirements are tracked by the
|
||||
implementation roadmap.
|
||||
92
docs/adr/0013-shared-source-version-checks.md
Normal file
92
docs/adr/0013-shared-source-version-checks.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# ADR 0013: Check Source Versions with One Shared Policy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented. The
|
||||
[configuration contract](../config.md#shared-source-version-checks) owns current
|
||||
behavior. The completed roadmap, implementation plan, consumer request, and
|
||||
resolved review findings are preserved at `e4270c9` under `docs/roadmap/` in
|
||||
Git history; the [development guide](../development.md#baseline-validation)
|
||||
provides retrieval examples.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-15
|
||||
|
||||
## Context
|
||||
|
||||
Applications that assemble one configuration from independently authored
|
||||
documents need a way to require that each contributing document declares a
|
||||
version accepted by the application's existing `Definition.Version` checker.
|
||||
Checking only the eventual composed value lets another source hide a rejected
|
||||
declaration, while separate consumer-side parsing duplicates ConfigKit's source
|
||||
handling and diagnostics.
|
||||
|
||||
The extension must preserve one application-owned acceptance policy and the
|
||||
existing loading lifecycle. Source declarations are compatibility evidence, not
|
||||
independent fragment schemas or values to compare for equality. Existing
|
||||
composition, placement, precedence, source identity, diagnostics, and
|
||||
sensitivity rules remain authoritative.
|
||||
|
||||
## Decision
|
||||
|
||||
Provide a load-wide source-version mode for ordinary and layered loading. The
|
||||
zero-value mode preserves current behavior. The enabled modes check declarations
|
||||
when present or require them, using the existing `Definition.Version` path and
|
||||
checker. Interpret the configured path relative to each participating source
|
||||
before placement or composition. Different declarations may be accepted by the
|
||||
same checker; ConfigKit adds no equality, SemVer, or expected-version rule.
|
||||
|
||||
Apply the selected policy to the actual main document, direct underlays,
|
||||
imports, and selected overlays. Missing non-main declarations are skipped in
|
||||
if-present mode and rejected in required mode. Existing main absence behavior
|
||||
remains, with required mode additionally rejecting an absent declaration on an
|
||||
actual main document. `NoSource`, unmatched optional inputs, synthetic field
|
||||
operations, and empty groups do not become documents that need declarations.
|
||||
|
||||
Reuse the existing version vocabulary and validation boundary: absence, null,
|
||||
exact strings, and strict decimal integers retain their established meanings;
|
||||
unsupported present values fail version-type validation. Warnings remain
|
||||
nonfatal, and source declarations do not replace final application decoding or
|
||||
domain validation.
|
||||
|
||||
After an accepted non-main declaration is checked, consume exactly that node
|
||||
before its remaining application content is composed or overlaid. Preserve the
|
||||
main declaration and its value semantics. Keep the declaration, source-local
|
||||
path, physical location, and checked absence as detached source evidence,
|
||||
separate from effective application values and history. Apply established
|
||||
sensitivity rules to that evidence, including original mounted paths and
|
||||
ancestors; retain existing safe failure disclosure.
|
||||
|
||||
In enabled modes, the main declaration remains source-local while its protected
|
||||
application path is derived from the main input's destination. This distinction
|
||||
does not change default-mode path interpretation or weaken protection of
|
||||
remaining payload and synthetic operations.
|
||||
|
||||
Public option-field additions and the enabled-mode interpretation for mounted
|
||||
main inputs are compatibility considerations for separately authorized minor-
|
||||
release preparation. This decision does not select a release version or
|
||||
authorize release notes or publication.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Check only the final composed value.** Later sources can hide an earlier
|
||||
rejected declaration, so this does not validate every contributing document.
|
||||
- **Let consumers parse and check each source.** This duplicates acquisition,
|
||||
source attribution, safe diagnostics, and lifecycle coordination in every
|
||||
application.
|
||||
- **Require declarations to be equal or impose a universal version grammar.**
|
||||
Consumers already own the acceptance policy; ConfigKit need not invent an
|
||||
expected value or a domain-specific compatibility rule.
|
||||
- **Keep accepted non-main declarations in application payload.** A selected
|
||||
source-local declaration is metadata for this policy and cannot also remain
|
||||
an ordinary field at the same path without ambiguity.
|
||||
|
||||
## Consequences
|
||||
|
||||
Consumers can apply one existing version policy across contributing documents
|
||||
and select gradual or required adoption per load. Accepted non-main headers no
|
||||
longer participate in application composition; their inspectable evidence is
|
||||
kept separate and subject to sensitivity classification. Default callers retain
|
||||
current behavior. The implementation plan records the concrete API names,
|
||||
integration order, and validation evidence.
|
||||
81
docs/adr/0014-sequence-placement-and-composition.md
Normal file
81
docs/adr/0014-sequence-placement-and-composition.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# ADR 0014: Place and Compose Sequence Items Explicitly
|
||||
|
||||
## Status
|
||||
|
||||
Partially superseded by [ADR 0017](0017-collection-sequence-policy.md) for
|
||||
identity ownership and sequence combination. Explicit source placement remains
|
||||
accepted.
|
||||
|
||||
The [configuration contract](../config.md#strict-document-composition)
|
||||
defines implemented behavior, and the [consumer guide](../consumers/README.md#identified-prompt-sequences)
|
||||
shows the maintained example. The completed roadmap, implementation plan, and
|
||||
resolved remediation record are preserved in Git history at `008d2ba` under
|
||||
`docs/roadmap/`; see the [development guide](../development.md) for retrieval
|
||||
commands.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-16
|
||||
|
||||
## Context
|
||||
|
||||
ConfigKit strictly composes independent documents by recursively combining
|
||||
mappings and rejecting other overlapping values. This makes two independently
|
||||
authored sequence fragments conflict, even when their records are distinct.
|
||||
Precedence assembly has a different purpose: a higher-priority present sequence
|
||||
replaces the lower-priority sequence as one value. Those existing rules are
|
||||
predictable, but they do not express the separate operation of collecting
|
||||
independent records while rejecting duplicate identities.
|
||||
|
||||
Likewise, mapping destinations place a source beneath literal mapping keys, but
|
||||
there is no explicit way to state that a source mapping represents one item in a
|
||||
sequence. Inferring that intent from document shape or fields would make source
|
||||
meaning implicit and change existing placement behavior.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep mapping placement as the zero-value behavior and add an explicit item
|
||||
placement choice for a source whose mapping should become one element at a
|
||||
non-root sequence destination. Item placement provides structure only; it does
|
||||
not infer or synthesize identity values.
|
||||
|
||||
Allow a definition to register literal mapping paths and one literal identity
|
||||
field for sequences at those paths. Within each strict composition tier, a
|
||||
registered sequence accepts mapping records with a present string identity,
|
||||
appends distinct records in contribution order, and rejects repeated exact
|
||||
identity strings. Identical registrations coalesce; conflicting identity fields
|
||||
for the same path are invalid. Registration does not recursively select paths
|
||||
inside sequence records.
|
||||
|
||||
Keep precedence assembly unchanged. A present higher-priority sequence replaces
|
||||
the lower-priority value wholesale, including at a registered path. Registration
|
||||
changes strict composition only; it does not turn precedence into append or
|
||||
identity-based merging.
|
||||
|
||||
This decision preserves ConfigKit's distinction between independent
|
||||
same-priority contributions and intentional precedence. Explicit source
|
||||
placement and definition-owned composition policy make the two required choices
|
||||
visible while keeping the existing zero-value load behavior.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Infer item placement from a source mapping or identity field.** Shape-based
|
||||
inference would make ordinary mapping placement depend on document contents
|
||||
and would leave the intended destination ambiguous.
|
||||
- **Append all overlapping sequences during strict composition.** This would
|
||||
change existing conflict semantics at unregistered paths and would not detect
|
||||
duplicate records.
|
||||
- **Merge matching records recursively by identity.** This would add a second
|
||||
recursive merge rule with different conflict and precedence behavior from
|
||||
ConfigKit's existing mapping composition and overlay operations.
|
||||
- **Use identity to merge sequences across precedence tiers.** This would
|
||||
change established whole-value replacement semantics and conflate strict
|
||||
composition with precedence.
|
||||
|
||||
## Consequences
|
||||
|
||||
Consumers can explicitly place mappings as sequence items and opt selected
|
||||
sequences into duplicate-rejecting strict composition. Unregistered sequences
|
||||
retain their current strict-conflict behavior, and higher-priority sequences
|
||||
continue to replace lower-priority ones. The implementation plan owns the exact
|
||||
API, YAML syntax, diagnostics, and implementation status.
|
||||
68
docs/adr/0015-shared-dependency-ready-ordering.md
Normal file
68
docs/adr/0015-shared-dependency-ready-ordering.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ADR 0015: Share Dependency-Ready Ordering Across Imports and Inheritance
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Implemented behavior is described in the
|
||||
[configuration contract](../config.md#explicit-imports) and
|
||||
[internal import overview](../internal/imports.md).
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-19
|
||||
|
||||
## Context
|
||||
|
||||
Imports and sibling inheritance both resolve relationships between configuration
|
||||
contributions. Their existing resolvers use different traversal rules, making
|
||||
ordering depend on which mechanism expresses a dependency. Ordering affects
|
||||
sequence contributions, provenance, and diagnostics, so a consistent rule is
|
||||
part of ConfigKit's configuration model.
|
||||
|
||||
The resolvers also serve distinct purposes. Imports strictly compose sources;
|
||||
inheritance overlays a child on its resolved parent after source assembly.
|
||||
Item-local contributions and nested inheritance introduce scope boundaries that
|
||||
resolution must preserve.
|
||||
|
||||
## Decision
|
||||
|
||||
ConfigKit owns one shared dependency-ready scheduler for imports and sibling
|
||||
inheritance. Both resolvers use Kahn's algorithm with a stable ordering key,
|
||||
selecting the smallest currently eligible node after its dependencies have
|
||||
completed. The shared scheduler owns eligibility, deterministic tie-breaking,
|
||||
and dependency-cycle identification. Each resolver owns construction of its
|
||||
dependency relationships and application of its domain operations.
|
||||
|
||||
Apply the common scheduling rule within the scopes where dependencies are
|
||||
available. Complete item-local contributions before item placement and resolve
|
||||
enclosing inheritance before relationships in the resulting nested scopes.
|
||||
Source discovery supplies the import graph; the scheduler determines resolution
|
||||
order.
|
||||
|
||||
Strict composition, inheritance overlay, and precedence tiers retain their
|
||||
existing ownership and meanings under [ADR 0001](0001-separate-composition-and-overlay.md).
|
||||
The lifecycle and scope decisions in
|
||||
[ADR 0009](0009-sibling-inheritance.md) continue to govern inheritance. Import
|
||||
admission retains its cycle and repeated-source rules.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Deterministic depth-first postorder.** Keeps dependency branches together,
|
||||
but gives traversal branch selection an additional role in ordering.
|
||||
- **Dependency rounds.** Groups nodes by readiness rounds, but delays newly
|
||||
eligible nodes until the current round finishes.
|
||||
- **Descending longest-path depth.** Prioritizes the furthest dependencies,
|
||||
but adds distance from the root as an ordering policy.
|
||||
- **Separate resolver algorithms.** Allows local specialization, but leaves
|
||||
consumers and maintainers with different rules for analogous dependencies.
|
||||
|
||||
## Consequences
|
||||
|
||||
Both resolvers follow one deterministic ordering model and share responsibility
|
||||
for maintaining that model. Resolver-specific scopes, combination semantics,
|
||||
and source-admission rules remain explicit at their integration boundaries.
|
||||
|
||||
Independent branches may interleave as nodes become eligible. Adopting this
|
||||
rule changes existing traversal-dependent sequence positions and reporting
|
||||
order, requiring coordinated review of source assembly, provenance, and
|
||||
inheritance behavior. The configuration contract owns the implemented ordering
|
||||
details.
|
||||
83
docs/adr/0016-independent-import-controls.md
Normal file
83
docs/adr/0016-independent-import-controls.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# ADR 0016: Separate Import Selection, Traversal, and Presence Requirements
|
||||
|
||||
## Status
|
||||
|
||||
Accepted. Implemented behavior is described in the
|
||||
[configuration contract](../config.md#explicit-imports), the
|
||||
[public API declarations](../../lifecycle.go), and the
|
||||
[internal import overview](../internal/imports.md).
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-19
|
||||
|
||||
## Context
|
||||
|
||||
Consumers assemble configurations from named files and collections whose layout
|
||||
varies between installations. Simple imports should require little setup, while
|
||||
larger collections need control over discovery, recursion, filtering, and
|
||||
required contributions. Combining those choices implicitly makes declarations
|
||||
harder to interpret and pushes source-selection work into applications.
|
||||
|
||||
Discovery can also reach a candidate through overlapping searches or encounter
|
||||
different names for one canonical source. Selection and source admission need
|
||||
distinct identities to support useful search unions while preserving ConfigKit's
|
||||
existing source-integrity rules.
|
||||
|
||||
## Decision
|
||||
|
||||
ConfigKit owns import discovery and selection within its source-loading
|
||||
lifecycle. Support explicit files, directory searches, and path globs as
|
||||
distinct selection forms. Keep traversal, filename filtering, and presence
|
||||
requirements independently expressible. Discovery determines the selected
|
||||
contributions; the shared scheduler in
|
||||
[ADR 0015](0015-shared-dependency-ready-ordering.md) determines their resolution
|
||||
order.
|
||||
|
||||
Use optional-by-default imports with explicit requiredness controls. Selected
|
||||
sources remain subject to ConfigKit's validity, containment, resource, and
|
||||
composition contracts. This applies the library's
|
||||
[simplicity and defaults principles](../policy/architecture.md#simplicity-defaults-and-explicit-controls)
|
||||
to import absence while retaining meaningful errors for invalid contributions.
|
||||
|
||||
Applications own their selected import defaults. Authored declaration values
|
||||
take precedence over application defaults, which take precedence over library
|
||||
defaults. Each declaration resolves applicable controls independently,
|
||||
including declarations in imported sources. This gives applications a shared
|
||||
baseline while preserving explicit choices in individual declarations.
|
||||
|
||||
Keep declaration-local candidate identity separate from canonical source
|
||||
identity. Search unions coalesce repeated candidate paths within a source
|
||||
namespace before symlink resolution; filename filtering uses the selected or
|
||||
discovered name. Source admission continues to use canonical identity and the
|
||||
existing repeated-source rules. Distinct aliases retain their separate
|
||||
inclusion routes.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Use path globs for all discovery.** Offers one selection form, but makes
|
||||
collection traversal depend on path-pattern structure and complicates
|
||||
independently controlling recursion and filename filtering.
|
||||
- **Offer only explicit files and directory searches.** Reduces the selector
|
||||
vocabulary, but moves wildcard directory selection into repeated declarations
|
||||
or application-side discovery.
|
||||
- **Require every import by default.** Detects absent contributions immediately,
|
||||
but requires repeated opt-outs for installations with optional components.
|
||||
- **Make application settings override authored declarations.** Provides central
|
||||
enforcement, but prevents declarations from expressing local exceptions.
|
||||
- **Deduplicate selections globally by canonical source identity.** Coalesces
|
||||
aliases and separate inclusion routes, but changes the existing repeated-source
|
||||
contract and obscures independently authored contributions.
|
||||
|
||||
## Consequences
|
||||
|
||||
Consumers can express simple imports and more selective collection searches
|
||||
through the same loading lifecycle. Applications choose defaults and validate
|
||||
their required effective configuration; ConfigKit owns consistent selection,
|
||||
source admission, composition, and inspection.
|
||||
|
||||
Optional absence can conceal misspelled selectors, so requiredness and
|
||||
inspection provide explicit ways to detect and explain missing contributions.
|
||||
Declaration-local deduplication requires the loader to preserve candidate
|
||||
identity alongside canonical source identity. The configuration and public API
|
||||
contracts own the implemented behavior.
|
||||
143
docs/adr/0017-collection-sequence-policy.md
Normal file
143
docs/adr/0017-collection-sequence-policy.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# ADR 0017: Define Identity and Merge Rules for Each Collection
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
This supersedes the decisions about who declares identity and how
|
||||
sequences combine in [ADR 0014](0014-sequence-placement-and-composition.md),
|
||||
and the rule that inheritance always replaces sequences in
|
||||
[ADR 0009](0009-sibling-inheritance.md). It also extends that ADR's sibling
|
||||
scopes to identified sequences. Existing mapping-sibling scopes and source
|
||||
placement continue to apply. It supersedes the whole-sequence
|
||||
precedence replacement rule in the proposed
|
||||
[ADR 0007](0007-source-backed-underlays.md).
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-22
|
||||
|
||||
## Context
|
||||
|
||||
ConfigKit can load separate files as records in a sequence. Applications can
|
||||
register an identity key so records with different identities combine and
|
||||
duplicates fail. However, a higher configuration layer or an inheriting mapping
|
||||
currently replaces the whole sequence. Applications that need to extend a
|
||||
catalog across layers must write their own merging code and keep source and
|
||||
history information consistent with its results.
|
||||
|
||||
Configuration authors need to choose how records are identified and whether a
|
||||
layer adds records, updates records, or replaces the collection. Applications
|
||||
also need a way to require unique identities in the final loaded configuration.
|
||||
These rules must work consistently for imported records, inline sequences,
|
||||
overlays, and inheritance.
|
||||
|
||||
Records in a sequence may also share settings. Authors need to reuse those
|
||||
settings through inheritance while keeping each record's own identity.
|
||||
|
||||
## Decision
|
||||
|
||||
ConfigKit manages identity and merge rules for each collection during loading.
|
||||
Configuration files can declare those rules for imported and inline sequences,
|
||||
including sequences in selected overlays. Applications can require a particular
|
||||
identity definition, and configuration declarations must agree with it. Source
|
||||
placement determines where each contribution enters the configuration.
|
||||
|
||||
Keep the distinction between contributions at the same priority and overrides
|
||||
from a higher layer, as established in
|
||||
[ADR 0001](0001-separate-composition-and-overlay.md). Within one tier, records
|
||||
with distinct identities combine; duplicate identities and conflicting
|
||||
declarations fail. Determine the applicable rules before combining those
|
||||
contributions, including any identity established in lower tiers.
|
||||
|
||||
Across layers, support three operations: replace the whole sequence, append
|
||||
records while rejecting duplicate identities, or replace whole records by
|
||||
identity (`upsert`). Source layering and sibling inheritance use the same
|
||||
operations. For inheritance, the rules at the inheriting destination determine
|
||||
which operation applies.
|
||||
|
||||
An identified sequence is also a sibling scope for inheritance. A record may
|
||||
inherit from one other record in the same sequence, selected by its complete
|
||||
identity. Each record supplies its own identity before inheritance. The child
|
||||
keeps that identity and combines its settings with the parent's other fields,
|
||||
using the existing mapping and collection rules. Parents remain ordinary records
|
||||
subject to normal validation.
|
||||
|
||||
Resolve these relationships after source assembly, using the same dependency
|
||||
ordering as mapping-sibling inheritance. Resolve nested relationships at each
|
||||
inheriting destination. Preserve the assembled sequence order. Whole-record
|
||||
upsert keeps its existing meaning; inheritance then uses the records that remain.
|
||||
Typed defaults, CLI overrides, and application resolution follow the lifecycle
|
||||
in [ADR 0018](0018-cli-after-structural-resolution.md).
|
||||
|
||||
An explicitly selected operation remains in effect at its destination until a
|
||||
higher layer changes it. When no explicit operation applies, use upsert if an
|
||||
identity definition is available; otherwise replace the whole sequence. Authors
|
||||
can declare identity and an operation before supplying any records. Keep source
|
||||
acquisition order separate from layer priority and the dependency ordering
|
||||
established by [ADR 0015](0015-shared-dependency-ready-ordering.md).
|
||||
|
||||
Rules declared inside a record belong to that record. Retaining the record
|
||||
retains its local rules; replacing it discards them. Replacing a whole collection
|
||||
can discard its previous YAML identity definition, while application requirements
|
||||
still apply. Inheritance can copy collection identity, but the inheriting
|
||||
destination chooses its operation. Application registrations continue to govern
|
||||
the paths where they were registered. The same ownership rules apply to
|
||||
declarations made before a sequence exists.
|
||||
|
||||
Check YAML-declared identity when combining collections during source assembly
|
||||
and inheritance. Application registrations also require the final loaded
|
||||
sequences to satisfy their identity rules, including after typed defaults, CLI
|
||||
overrides, and application resolution. This guarantee checks sequences that
|
||||
exist; the application schema and validation decide whether a collection must
|
||||
exist and which value types are allowed. The guarantee applies when loading
|
||||
completes, rather than to every intermediate callback state or changes made by
|
||||
the caller afterward.
|
||||
|
||||
Preserve explicit presence, source attribution, history, sensitivity, and
|
||||
resource context through these operations, following
|
||||
[ADR 0003](0003-preserve-presence-and-resolution-history.md). Typed defaults,
|
||||
CLI assignments, and application resolution keep their own rules for changing
|
||||
values. The public API provides registration and inspection; the implementation
|
||||
chooses how to store and track collection rules internally.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Keep identity application-owned and always replace sequences across layers.**
|
||||
Keeps the current design small, but reusable catalogs require application
|
||||
changes or custom merging code.
|
||||
- **Put merge rules on each import.** Keeps the rules beside the imported source,
|
||||
but inline sequences and overlays need a separate way to declare them. It also
|
||||
makes rules for a collection spread across several imports harder to reconcile.
|
||||
- **Merge individual fields of records with matching identities.** Allows partial
|
||||
record updates, but adds rules for field conflicts and retained values.
|
||||
Replacing the whole record makes the update's scope clear.
|
||||
- **Keep inheritance limited to mapping siblings.** Keeps parent lookup simpler,
|
||||
but authors of sequence records must duplicate shared settings or applications
|
||||
must implement inheritance themselves. Collection identity provides a way to
|
||||
select a parent within the existing loading process.
|
||||
- **Check every YAML identity again at the end of loading.** Gives YAML declarations
|
||||
authority over values changed by defaults, CLI assignments, and application
|
||||
resolution. This extends the lifetime of those declarations; application
|
||||
registration provides that final guarantee when needed.
|
||||
|
||||
## Consequences
|
||||
|
||||
Consumers can build and customize catalogs in one load while retaining useful
|
||||
inspection information. Configuration authors choose how collections combine
|
||||
within application requirements. Duplicate identities among contributions at
|
||||
the same priority remain errors.
|
||||
|
||||
Existing registered collections use upsert across layers by default unless an
|
||||
explicit operation selects otherwise. Correct loading now requires tracking
|
||||
which declarations still apply as records are replaced, copied, or moved to new
|
||||
positions. Lower-tier identity must be available before combining higher-tier
|
||||
contributions.
|
||||
|
||||
Record inheritance also requires parent lookup by identity and checking for
|
||||
missing parents and cycles. Copied settings retain the same source, history,
|
||||
and sensitivity information as mapping-sibling inheritance.
|
||||
|
||||
The [configuration contract](../config.md#strict-document-composition) defines
|
||||
declaration syntax, identity comparison, omission and empty-value behavior, and
|
||||
diagnostics for the implemented feature.
|
||||
84
docs/adr/0018-cli-after-structural-resolution.md
Normal file
84
docs/adr/0018-cli-after-structural-resolution.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# ADR 0018: Apply CLI Overrides After Inheritance and Decoding
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
This supersedes the rule that CLI overrides apply before decoding in
|
||||
[ADR 0005](0005-explicit-overrides-and-safe-explanation.md) and the
|
||||
rule that they apply before inheritance in [ADR 0009](0009-sibling-inheritance.md).
|
||||
Their remaining ownership, inheritance scope, and safe inspection decisions
|
||||
continue to apply.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-22
|
||||
|
||||
## Context
|
||||
|
||||
Explicit command-line values are the operator's highest-priority input. If they
|
||||
apply before inheritance, a sequence supplied as a replacement could gain
|
||||
inherited records afterward. Its final contents would then differ from the
|
||||
operator's assignment.
|
||||
|
||||
The application also needs these values when it fills missing settings,
|
||||
normalizes values, or computes derived settings. ConfigKit needs a clear loading
|
||||
order that preserves both CLI precedence and the application's role in resolving
|
||||
the final configuration.
|
||||
|
||||
## Decision
|
||||
|
||||
ConfigKit uses this loading order:
|
||||
|
||||
1. Combine sources within tiers and apply precedence between layers.
|
||||
2. Resolve sibling inheritance, apply typed defaults, and decode the configuration
|
||||
into the application's types.
|
||||
3. Apply explicit CLI overrides and check them against the application schema.
|
||||
4. Run application resolution using the overridden configuration.
|
||||
5. Look up effective secret references, validate the final configuration, and
|
||||
return it.
|
||||
|
||||
CLI overrides are the highest-priority input to application resolution. A CLI
|
||||
sequence replaces the whole sequence; mappings combine recursively. Collection
|
||||
merge rules apply during the earlier source and inheritance steps. Inheritance
|
||||
and typed defaulting do not run again after CLI overrides.
|
||||
|
||||
Keep track of which values were explicitly supplied and which CLI input supplied
|
||||
them. Applications continue to parse command-line arguments; ConfigKit applies
|
||||
and validates the overrides and makes their effects inspectable. Reject
|
||||
structural directives in override paths and values, so instructions such as
|
||||
inheritance relationships remain part of source processing.
|
||||
|
||||
Application resolution keeps its existing rules for defaults and recorded
|
||||
changes under [ADR 0003](0003-preserve-presence-and-resolution-history.md).
|
||||
It receives CLI values together with their presence and source information.
|
||||
Defaults that only fill missing settings preserve explicit CLI values.
|
||||
Application normalization and derived-value calculations can still change them,
|
||||
with those changes recorded. Secret lookup and final validation use the resulting
|
||||
configuration.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Apply CLI overrides before inheritance.** Preserves existing ordering, but
|
||||
later merging needs special handling to preserve CLI replacement intent while
|
||||
still combining ordinary source contributions.
|
||||
- **Apply CLI overrides after inheritance but before typed decoding.** Allows an
|
||||
override to fix a source value that would otherwise fail decoding, but leaves
|
||||
typed defaulting after the operator's assignment. Completing defaulting and
|
||||
decoding first gives CLI overrides an already resolved configuration to modify.
|
||||
- **Apply CLI overrides after application resolution.** Applies assignments after
|
||||
the application's changes, but derived settings still reflect earlier values
|
||||
unless resolution runs again.
|
||||
|
||||
## Consequences
|
||||
|
||||
CLI overrides change a configuration whose inheritance and typed defaults are
|
||||
already resolved. The application can then derive settings from the operator's
|
||||
values. The final values, source information, history, and sensitivity handling
|
||||
must remain consistent throughout loading.
|
||||
|
||||
CLI input can no longer add or cancel inheritance relationships or bypass an
|
||||
earlier error in source assembly or inheritance. It also cannot fix a value that
|
||||
fails the earlier decoding step. Semantic validation still runs after overrides
|
||||
and application resolution. The [configuration contract](../config.md#explicit-cli-overrides)
|
||||
and [consumer guidance](../consumers/README.md) describe this behavior.
|
||||
68
docs/adr/0019-explicit-configuration-discovery.md
Normal file
68
docs/adr/0019-explicit-configuration-discovery.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ADR 0019: Keep Configuration Discovery Explicit and Inside Loading
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Date
|
||||
|
||||
2026-10-24
|
||||
|
||||
## Context
|
||||
|
||||
Applications often need a small ordered convention for choosing a main
|
||||
configuration file. Keeping that choice entirely in each application repeats
|
||||
path selection and file-loading plumbing. Moving the convention into ConfigKit
|
||||
without an explicit declaration would make ordinary loads search paths that a
|
||||
consumer did not choose, obscuring both precedence and filesystem access.
|
||||
|
||||
ConfigKit already owns synchronous source acquisition, parsing, composition,
|
||||
decoding, validation, and inspection. A selected host file can therefore enter
|
||||
that lifecycle as the existing file source, preserving its source identity,
|
||||
resource context, and diagnostics. Optional exhaustion can use the same absent
|
||||
main lifecycle as an explicitly absent source.
|
||||
|
||||
## Decision
|
||||
|
||||
Consumers choose and order the main-file conventions they want to use. They may
|
||||
describe literal paths, directory filename groups, and supported directory
|
||||
presets, and may mark the complete search as required. Search descriptions are
|
||||
explicit inputs to loading; no preset or convention is activated implicitly.
|
||||
|
||||
During synchronous loading, ConfigKit resolves the description to one real host
|
||||
file or to absence before parsing. The selected file follows the existing source
|
||||
acquisition and processing rules. A selected-file access, parse, version,
|
||||
composition, decode, or validation failure is terminal and does not resume the
|
||||
search. Optional exhaustion shares the absent-main lifecycle; required
|
||||
exhaustion reports a missing-source diagnostic. No candidate-attempt history is
|
||||
added to public metadata.
|
||||
|
||||
This keeps convention choice with applications while moving repeated selection
|
||||
and loading plumbing into the library's established lifecycle. It preserves
|
||||
explicit control, ordinary source behavior, and complete-on-return ownership.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Keep all search and fallback loading in each application.** This preserves
|
||||
consumer ownership but repeats the file-selection and load coordination that
|
||||
ConfigKit already owns, and risks divergent handling of selected-file errors.
|
||||
- **Search conventional locations implicitly during every load.** This reduces
|
||||
setup, but activates filesystem access and precedence the consumer did not
|
||||
select and makes ordinary source behavior less predictable.
|
||||
- **Return candidate paths for the application to load one by one.** This keeps
|
||||
selection outside the load lifecycle and encourages repeated complete loads,
|
||||
potentially rerunning defaults, callbacks, and validation during fallback.
|
||||
|
||||
## Consequences
|
||||
|
||||
The public search description must be immutable and root-owned, with private
|
||||
engine conversion and selection. The contract documents candidate ordering,
|
||||
selection, exhaustion, context, and provenance; the API declaration documents
|
||||
the public symbols. Dynamic bases and file availability are observed when each
|
||||
load consumes the description. Applications remain responsible for selecting
|
||||
conventions and for their schema and domain requirements.
|
||||
|
||||
The [configuration contract](../config.md) owns implemented behavior, and the
|
||||
[component overview](../internal/overview.md) describes implementation ownership.
|
||||
The completed implementation plan and remediation record are preserved in Git
|
||||
history at `0369111` under `docs/roadmap/`.
|
||||
61
docs/adr/0020-document-root-controls.md
Normal file
61
docs/adr/0020-document-root-controls.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# ADR 0020: Scope Configuration Controls to Document Roots
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
## Date
|
||||
|
||||
2026-11-01
|
||||
|
||||
## Context
|
||||
|
||||
ConfigKit interprets configuration controls during source acquisition, before
|
||||
placing and composing source payloads. Reserving the control name throughout
|
||||
application data restricts paths unrelated to source-document controls. The
|
||||
source-local position of a control and the effective application path of a
|
||||
value are different coordinates, even when their path text is identical.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `config` as the single control mapping name at the root of each acquired
|
||||
source document. Interpret each source's controls before placing its payload,
|
||||
using the existing context-specific grammar and import opt-in. A source retains
|
||||
its own document root regardless of its eventual destination, including when
|
||||
mounted below another value or as a sequence record.
|
||||
|
||||
Below a source root, `config` is ordinary application data, including in
|
||||
sequence records and selected overlays. Control-looking children in such data
|
||||
do not activate controls. Application-root `config` data produced by placement,
|
||||
defaults, CLI assignment, or resolution is also ordinary data. Placement and
|
||||
later lifecycle operations do not create another source-document parsing pass.
|
||||
|
||||
The former spellings `configkit` and `_configkit` are ordinary application keys
|
||||
at any depth, subject to normal schema and structural rules; they are not
|
||||
control aliases. Source-version checks protect actual source-local controls,
|
||||
not an application path merely because its spelling matches. Existing `extends`
|
||||
inheritance semantics and restrictions remain in force.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
- **Reserve the control name throughout application data.** This conflates
|
||||
source-document syntax with application paths and prevents ordinary fields
|
||||
unrelated to control interpretation.
|
||||
- **Recognize controls again after placement or later operations.** This makes
|
||||
application data acquire control semantics according to its destination and
|
||||
introduces a second interpretation pass.
|
||||
- **Support multiple or configurable control spellings.** This makes control
|
||||
interpretation ambiguous and expands the configuration contract.
|
||||
|
||||
## Consequences
|
||||
|
||||
Control evidence uses source-local coordinates while assembled values use
|
||||
application coordinates. Existing source identities and contexts distinguish
|
||||
these uses without a new inspection-record format. Consumers must migrate
|
||||
genuine old control blocks and account for source-root application fields
|
||||
named `config`; ordinary nested and assembled root fields remain available.
|
||||
|
||||
The completed feature roadmap and implementation plan are preserved in Git
|
||||
history at `c63f4ff` under `docs/roadmap/`, for example
|
||||
`git show c63f4ff:docs/roadmap/implementation.md`.
|
||||
The [configuration contract](../config.md) owns implemented behavior.
|
||||
1544
docs/config.md
1544
docs/config.md
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,157 @@
|
||||
# Consumer Adoption Guide
|
||||
|
||||
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and
|
||||
selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from
|
||||
applications. Consumers still own their configuration structs, defaults, semantic
|
||||
validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
||||
resource checks, remote acquisition, and service initialization.
|
||||
ConfigKit loads configuration into your application's Go types. You provide
|
||||
those types, defaults, and application rules. ConfigKit reads and combines the
|
||||
selected inputs, checks their types, and returns values with source information.
|
||||
Your application still owns CLI parsing, resource reads, remote acquisition, and
|
||||
runtime initialization.
|
||||
|
||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||
sanitized shapes using only ConfigKit and the Go standard library:
|
||||
Import `"go.maximumdirect.net/configkit"`; the package name is `configkit`.
|
||||
|
||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||
| --- | --- | --- |
|
||||
| Narratio-like | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
|
||||
| Notarius-like | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
||||
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
Document controls now use root `config` in every source. Rename genuine old
|
||||
`configkit` or `_configkit` control blocks and update stored declaration paths
|
||||
from `/configkit/...` to `/config/...`. Old blocks become ordinary data and a
|
||||
permissive schema can accept them without executing imports. Move, rename, or
|
||||
separately mount application fields previously authored as source-root `config`.
|
||||
Nested and assembled root `config` values remain ordinary application data; the
|
||||
[mounted-root example](../../examples/consumers/config_controls_test.go) shows
|
||||
the boundary. See the [configuration contract](../config.md#declaration-and-selection-rules)
|
||||
for the complete rules.
|
||||
Start with one file, then choose the tasks your application needs:
|
||||
|
||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||
executable examples and ordinary tests. It contains no private application data,
|
||||
credentials, network calls, or dependency on a sibling checkout.
|
||||
| Task | Start here |
|
||||
| --- | --- |
|
||||
| Load one file | [Happy path](#happy-path) and the [README example](../../README.md#load-a-configuration-file) |
|
||||
| Supply fixed defaults and check application rules | [Defaults and application rules](#defaults-and-application-rules) |
|
||||
| Add bundled defaults beneath application settings | [Native filesystem and layered input](#native-filesystem-and-layered-input) |
|
||||
| Select files or an overlay | [Discovery](#explicit-main-file-discovery), [import defaults](#import-defaults-and-selection-inspection), and the [overlay reference](../config.md#selected-overlays) |
|
||||
| Reuse settings or combine record lists | [Inheritance](#named-sibling-inheritance) and [identified sequences](#identified-prompt-sequences) |
|
||||
| Handle credentials | [Typed-secret reference](../config.md#typed-secret-references) and the [startup example](../../examples/consumers/consumers_test.go) |
|
||||
| Inspect sources, earlier values, or changes | [Inspect a completed load](#inspect-a-completed-load) and [diffs](#effective-configuration-diffs) |
|
||||
| Upgrade an existing consumer | [Migration guidance](#intentional-migration-changes) |
|
||||
|
||||
The Notarius-shaped fixtures deliberately load both `version: 4` and
|
||||
`version: "4"` into the same string-backed application field. Its version checker
|
||||
still owns the supported identifier; fractional numeric input and unsupported
|
||||
integer identifiers remain negative examples.
|
||||
## Choosing a loading entry point
|
||||
|
||||
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
|
||||
[`SearchConfig`](../../discovery.go) is also a source; pass it to `Load` or use
|
||||
it as the main input to [`LoadLayered`](../../layered.go).
|
||||
- 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).
|
||||
- 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
|
||||
on imports or discovery by themselves. See the
|
||||
[configuration contract](../config.md#layered-loading) and
|
||||
[discovery contract](../config.md#configuration-discovery) for exact rules.
|
||||
|
||||
The root [README example](../../README.md) shows the small `Load` path.
|
||||
[`layered.go`](../../examples/consumers/layered.go) demonstrates the bundled
|
||||
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).
|
||||
|
||||
## Happy path
|
||||
|
||||
An application defines ordinary types and passes one explicit source. Optional
|
||||
features appear only when the application needs them:
|
||||
Define the Go configuration type, choose one source, and call `Load`. Handle a
|
||||
load error before starting the application with `result.Config`. The
|
||||
[README example](../../README.md#load-a-configuration-file) shows this complete
|
||||
minimal loading function. An explicitly missing file is an error; use `NoSource`
|
||||
when you deliberately want defaults without a main document.
|
||||
|
||||
### Defaults and application rules
|
||||
|
||||
Using the README's `Config` type, an application can add reusable rules:
|
||||
|
||||
```go
|
||||
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{
|
||||
Definition: rules,
|
||||
Imports: useDeclaredImports,
|
||||
Overlay: selectedConfigOverlay,
|
||||
Overrides: explicitlySetFlags,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
definition := configkit.Definition[Config]{
|
||||
Defaults: func() Config { return Config{Enabled: true, Workers: 2} },
|
||||
Validate: func(value *Config, validation *configkit.Validation) {
|
||||
if value.Workers < 0 {
|
||||
validation.Error(configkit.Keys("workers"), "must be nonnegative")
|
||||
}
|
||||
},
|
||||
}
|
||||
return start(result.Config)
|
||||
```
|
||||
|
||||
Simple consumers use neither imports nor selected overlays. A complex consumer can opt into
|
||||
both without redefining their syntax. ConfigKit overlays are configuration overlays;
|
||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
||||
execution profile concepts.
|
||||
Pass this definition in `Options[Config]{Definition: definition}` when calling
|
||||
`Load`. Supplied `enabled: false` or `workers: 0` replaces those defaults.
|
||||
Use `Resolve` when a value must be derived or normalized before validation, and
|
||||
record that change with the provided helpers. The
|
||||
[callback reference](../config.md#recorded-application-resolution) compares the
|
||||
helpers; [executable examples](../../example_test.go) show full definitions.
|
||||
|
||||
### Inspect a completed load
|
||||
|
||||
Keep the result when you need to explain configuration. Use
|
||||
`result.Metadata().Lookup(configkit.Keys("workers"))` to inspect whether input
|
||||
supplied the value and which source or application step supplied it. Follow an
|
||||
origin's source ID with `result.Source(id)` when you need its source record.
|
||||
|
||||
Use `Explain` for earlier values, `WriteExplanation` for a readable explanation,
|
||||
and `WriteEffective` for safe YAML describing the final saved state. These
|
||||
operations describe the completed load even if your application later changes
|
||||
`result.Config`. The [inspection reference](../config.md#layered-metadata)
|
||||
explains returned values, copies, and hidden information.
|
||||
|
||||
Add imports, a selected overlay, or explicitly set CLI values through options
|
||||
only when needed. An overlay changes configuration; choosing an LLM, report, or
|
||||
execution profile remains application logic.
|
||||
|
||||
## Explicit main-file discovery
|
||||
|
||||
Applications choose the search conventions and their precedence, then pass the
|
||||
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry
|
||||
point. For example, the maintained sequence catalog puts a caller-selected
|
||||
file before a custom directory fallback while keeping its underlay and overlay
|
||||
choices independent. See
|
||||
[`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go) and its
|
||||
[behavior tests](../../examples/consumers/sequence_catalog_test.go), plus the
|
||||
[working-directory executable
|
||||
example](../../examples/consumers/discovery_test.go). Together, the [discovery
|
||||
API](../../discovery.go) and [configuration discovery
|
||||
contract](../config.md#configuration-discovery) explain the order of attempted
|
||||
files, what happens when no file is found, and why a selected-file failure
|
||||
stops the search. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
|
||||
records why discovery is explicitly selected.
|
||||
|
||||
## Import defaults and selection inspection
|
||||
|
||||
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` apply one
|
||||
load-wide policy to enabled main, underlay, and transitive imports. Each field
|
||||
is resolved independently: a declaration's explicit value wins, then the
|
||||
application default, then the library default. `Required`, `FollowSymlinks`,
|
||||
`IncludeHidden`, and `Recursive` use nil pointers for unspecified values.
|
||||
`Pattern` applies to
|
||||
directory searches: nil leaves filenames unrestricted, while a non-nil empty
|
||||
slice selects no files. A declaration's own `pattern` replaces the application
|
||||
default. The defaults do not enable imports or change selected-overlay behavior.
|
||||
The Go fields `FollowSymlinks` and `IncludeHidden` correspond to the unchanged
|
||||
YAML keys `follow` and `hidden`.
|
||||
|
||||
`Result.Sources`, `LoadError.Sources`, and `Resolution.Source` expose each
|
||||
declaring source's `EmptyImportSelections`. `Result.Source(id)` and
|
||||
`LoadError.Source(id)` retrieve one retained record directly when its load-local
|
||||
ID is known. These records identify the source-local declaration path, its
|
||||
authored location and selector;
|
||||
they do not include candidate names or a guessed reason for absence. Returned
|
||||
records are detached snapshots. The [configuration contract](../config.md#explicit-imports)
|
||||
owns selector applicability and exact absence semantics.
|
||||
|
||||
## Shared source-version checks
|
||||
|
||||
[`LoadVersionedService`](../../examples/consumers/source_version.go) demonstrates
|
||||
if-present and required modes with one `Definition.Version` rule across a mounted
|
||||
main input and an underlay. The example reads only sanitized
|
||||
`SourceInfo.VersionEvidence` fields; its executable test is in
|
||||
[`layered_test.go`](../../examples/consumers/layered_test.go). The
|
||||
[configuration contract](../config.md#shared-source-version-checks) defines
|
||||
participation, source-local lookup and diagnostics, non-main header consumption,
|
||||
main-path protection, default behavior, and evidence disclosure. Accepted
|
||||
declarations do not replace final assembled decoding or application validation.
|
||||
|
||||
## Effective configuration diffs
|
||||
|
||||
@@ -64,13 +164,14 @@ priority values remain present. Its resolver records a derived endpoint URL afte
|
||||
overlay application, and its nested typed secret reference is classified before
|
||||
the resulting records are presented.
|
||||
|
||||
The maintained [behavior tests and executable examples](../../examples/consumers/diff_test.go)
|
||||
use temporary files and disposable secret values. Presenters should use
|
||||
`ValueSnapshot.Present` for absence, record membership for change, and
|
||||
`HiddenChanged` for a withheld change indicator. Do not parse the redaction marker
|
||||
or add separately rendered full contexts that lack combined sensitivity. The
|
||||
[configuration contract](../config.md#effective-configuration-diffs) owns shared
|
||||
equality, granularity, and disclosure semantics.
|
||||
The maintained [behavior tests and executable
|
||||
examples](../../examples/consumers/diff_test.go) use temporary files and
|
||||
disposable secret values. Presenters should use `ValueSnapshot.Present` for
|
||||
absence, record membership for change, and `HiddenChanged` for a withheld
|
||||
change indicator. Do not parse the redaction marker or add separately rendered
|
||||
full contexts that lack combined sensitivity. The [configuration
|
||||
contract](../config.md#effective-configuration-diffs) owns shared equality,
|
||||
granularity, and disclosure semantics.
|
||||
|
||||
## Named sibling inheritance
|
||||
|
||||
@@ -109,12 +210,21 @@ and may fail independently of the completed configuration load. The
|
||||
[configuration contract](../config.md#source-resource-contexts) owns exact
|
||||
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
|
||||
|
||||
Main and underlay inputs use the same destination field; imported application
|
||||
contributions inherit the declaring input's destination. A destination is an
|
||||
application mapping path, independent of filesystem context. The configuration
|
||||
contract owns the complete behavior and validation rules.
|
||||
Main and underlay inputs use the same destination field. An import without a
|
||||
`destination` inherits its declaring document's effective destination; an
|
||||
explicit sequence of key strings appends a relative suffix. Nested imports
|
||||
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)
|
||||
mounts one mapping source beneath literal collection/prompt keys and loads a
|
||||
@@ -123,12 +233,13 @@ default. Consumers pass paths, not filenames or namespace strings for ConfigKit
|
||||
to interpret.
|
||||
|
||||
[`LoadCollectionBundle`](../../examples/consumers/destinations.go) assembles an
|
||||
embedded collection manifest with a source-root import, separate prompt and
|
||||
profile peers, and a higher-priority input that changes only one prompt's
|
||||
ordinary `version` field. The main document alone supplies the application
|
||||
schema version. A prompt's local `extends` relationship resolves after all
|
||||
tiers, so `custom` inherits the final `brief` content and fields while retaining
|
||||
its own profile selection.
|
||||
embedded collection manifest mounted beneath a literal collection path. The
|
||||
entry's YAML places nested metadata imports at the collection root and prompt
|
||||
and profile peers beneath their own keys. A higher-priority input changes only
|
||||
one prompt's ordinary `version` field. The main document alone supplies the
|
||||
application schema version. A prompt's local `extends` relationship resolves
|
||||
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
|
||||
source context. This also works for `custom`'s inherited resource. The
|
||||
@@ -140,6 +251,38 @@ relative resources, and strict peer conflicts. The
|
||||
[configuration contract](../config.md#input-destinations) owns shared placement,
|
||||
import, source-location, and ownership behavior.
|
||||
|
||||
## Identified prompt sequences
|
||||
|
||||
[`LoadPromptCatalog`](../../examples/consumers/prompt_catalog.go) shows the
|
||||
smaller import-focused adoption path: a registered `prompts.id` identity,
|
||||
directory and glob selection, a helper import that supplies an item's ID, and
|
||||
resource lookup through the effective source context. Its executable examples
|
||||
and tests are in
|
||||
[`prompt_catalog_test.go`](../../examples/consumers/prompt_catalog_test.go), with
|
||||
complete [catalog](../../examples/consumers/testdata/prompt-catalog/config.yml)
|
||||
and [required-file](../../examples/consumers/testdata/prompt-catalog/simple.yml)
|
||||
fixtures.
|
||||
|
||||
[`LoadSequenceCatalog`](../../examples/consumers/sequence_catalog.go) integrates
|
||||
an underlay declaration, imported main fragments, identified-record inheritance,
|
||||
selected overlay `append`, `upsert`, and `replace`, a typed CLI override, and
|
||||
recorded application resolution. Its tests and runnable example demonstrate
|
||||
stable output order, whole-record upsert, provenance/history inspection,
|
||||
resource lookup for inherited records, and secret-safe explanation in
|
||||
[`sequence_catalog_test.go`](../../examples/consumers/sequence_catalog_test.go).
|
||||
[`LoadNamespacedMapCatalog`](../../examples/consumers/sequence_catalog.go) adds
|
||||
an exact root registration and a wildcard namespace registration, showing that
|
||||
each concrete sequence has an independent identity scope. The same tests show
|
||||
that a YAML identity declaration without an application registration does not
|
||||
create final-result identity enforcement.
|
||||
|
||||
The examples keep prompt meaning and domain checks in the consuming application.
|
||||
The [configuration contract](../config.md#strict-document-composition) owns
|
||||
declaration, identity, operation, ownership, and final-validation semantics;
|
||||
[sibling inheritance](../config.md#sibling-inheritance) defines record
|
||||
relationships, and the [CLI contract](../config.md#explicit-cli-overrides) owns
|
||||
override ordering.
|
||||
|
||||
Known module catalogs should use canonical object entries with a module identifier
|
||||
and typed options. A small union struct is sufficient when the available option
|
||||
fields are known; validation selects which fields apply to each module. Runtime-
|
||||
@@ -151,14 +294,13 @@ conversion API.
|
||||
## Source-aware application path policy
|
||||
|
||||
[`LoadNotariusPaths`](../../examples/consumers/consumers.go) is the maintained
|
||||
path-bearing example. Its application resolver uses `Resolution.InputInfo` and
|
||||
path-bearing example. Its application resolver uses `Resolution.EntryInfo` and
|
||||
`Resolution.Source` before normalizing: relative CLI input uses an explicitly
|
||||
supplied invocation directory, retained defaults use the canonical main source,
|
||||
and a file-authored scalar uses its one declaring source. Absolute paths remain
|
||||
unchanged. Its executable test covers the matching effective YAML and explanation
|
||||
history, source-file ownership, and a symlink opened through an alias; canonical
|
||||
target-file directories, rather than the alias directory or process working
|
||||
directory, are the selected bases.
|
||||
unchanged. Its executable test checks the resulting YAML and explanation
|
||||
history. When the configuration filename is a symlink, this example uses the
|
||||
actual target file's directory as the base for file-authored paths.
|
||||
|
||||
The selected example policy leaves a relative value unchanged and emits a warning
|
||||
when a CLI base is absent or file provenance is missing or composite. That is an
|
||||
@@ -168,7 +310,15 @@ existence checks nor confinement checks.
|
||||
|
||||
## Intentional migration changes
|
||||
|
||||
The examples preserve domain meaning while adopting one configuration convention:
|
||||
When upgrading, start with the changes relevant to your dependency and inputs:
|
||||
|
||||
1. Update the module requirement and Go imports together as described below.
|
||||
2. Replace old public names using the old/new table.
|
||||
3. Update any layer or diagnostic-code strings stored or compared by your code.
|
||||
4. Review YAML and startup changes that apply to your application, then rebuild
|
||||
and run its configuration tests.
|
||||
|
||||
The maintained examples illustrate these configuration changes:
|
||||
|
||||
- Explicit empty strings, lists, zeroes, false values, and nulls remain intentional.
|
||||
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
|
||||
@@ -188,10 +338,70 @@ The examples preserve domain meaning while adopting one configuration convention
|
||||
parsers translate only explicitly set configuration flags into `Override` records.
|
||||
Separate invocation state remains outside configuration.
|
||||
- Credential fields migrate to environment-name `Secret` references. Applications
|
||||
explicitly call `LoadSecretsDir` before `Load` when they choose file bootstrap;
|
||||
explicitly call `LoadSecretsIntoEnvironment` before `Load` when they choose
|
||||
file bootstrap;
|
||||
blocked empty variables remain blocked, and ordinary `Load` never changes the
|
||||
process environment.
|
||||
|
||||
### Vanity module path migration
|
||||
|
||||
ConfigKit's module identifier changed from
|
||||
`gitea.maximumdirect.net/eric/ConfigKit` to `go.maximumdirect.net/configkit` before
|
||||
v1.0.0. Update the dependency requirement in your application's `go.mod` and its
|
||||
Go imports together, selecting a revision or release that declares the new module
|
||||
path. The package name remains `configkit`; the configuration model and public
|
||||
symbol names are unchanged by this identity migration.
|
||||
|
||||
Go treats the old and new import paths as different packages, with different
|
||||
named types. For example, if another library returns a ConfigKit `Path` or
|
||||
`Secret` using the old import, your application cannot use it as the new
|
||||
package's type just by changing a module requirement. Coordinate migration
|
||||
with dependencies that pass ConfigKit values to or from your application. The
|
||||
maintained examples use the new path, but other repositories must manage their
|
||||
own migrations.
|
||||
|
||||
Historical tags keep their original module declarations and contents. Older
|
||||
releases such as v0.10.2 still use the old path and cannot be installed under the
|
||||
vanity identifier simply because discovery can find their repository. A matching
|
||||
migrated revision is needed; no old-path compatibility module is supplied here.
|
||||
|
||||
### Pre-v1 public API cleanup
|
||||
|
||||
The pre-v1 API cleanup replaces these names outright; it does not retain
|
||||
compatibility aliases:
|
||||
|
||||
| Previous API | Current API |
|
||||
| --- | --- |
|
||||
| `Resolution.InputInfo(path)` | `Resolution.EntryInfo(path)` |
|
||||
| `Definition.Secret` | `Definition.Sensitive` |
|
||||
| `SourceVersionModeDefault` | `SourceVersionModeMainOnly` |
|
||||
| `SourceVersionCheck` | `SourceVersionEvidence` |
|
||||
| `SourceInfo.VersionCheck` | `SourceInfo.VersionEvidence` |
|
||||
| `ImportSelection` | `EmptyImportSelection` |
|
||||
| `SourceInfo.ImportSelections` | `SourceInfo.EmptyImportSelections` |
|
||||
| `SourceInfo.DestinationUnresolved` | `SourceInfo.DestinationIsFallback` |
|
||||
| `ImportDefaults.Follow` / `.Hidden` | `FollowSymlinks` / `IncludeHidden` |
|
||||
| `Paths(paths...)` | `SearchPaths(paths...)` |
|
||||
| `LoadSecretsDir(directory)` | `LoadSecretsIntoEnvironment(directory)` |
|
||||
| `LayerBase` | `LayerMain` |
|
||||
| `CodeExplainLimitExceeded` | `CodeHistoryLimitExceeded` |
|
||||
| `CodeResolveInvalidKind` / `CodeOptionsUnavailable` | Removed; no replacement |
|
||||
| `ImportSelection.Outcome`, `ImportSelectionOutcome`, `ImportSelectionNone` | Removed; empty-selection records no longer carry an outcome |
|
||||
| `Resolution.Record(path, ChangeDerivedDefault, reason, deps...)` | `Resolution.RecordDerivedDefault(path, reason, deps...)` |
|
||||
| `Resolution.Record(path, ChangeNormalization, reason, deps...)` | `Resolution.RecordNormalization(path, reason, deps...)` |
|
||||
| Enumerating `Result.Sources()` / `LoadError.Sources()` to find one ID | `Result.Source(id)` / `LoadError.Source(id)` |
|
||||
| Provenance string `"base"` | `"main"` |
|
||||
| History diagnostic string `"explain.limit_exceeded"` | `"history.limit_exceeded"` |
|
||||
|
||||
The YAML import keys `follow` and `hidden`, the `configkit:"secret"` tag, and
|
||||
typed `Secret` references are unchanged. If your application compares or
|
||||
stores layer names or diagnostic codes as strings, update those strings
|
||||
alongside Go references. This repository guidance does not claim that
|
||||
downstream applications have migrated and does not select a release version
|
||||
for this breaking pre-v1 change. `ChangeKind` and its inspection constants
|
||||
remain; use `Default` and `Normalize` when the helper should both assign and
|
||||
record a value.
|
||||
|
||||
### Selected-overlay migration
|
||||
|
||||
To adopt the current selected-overlay API, rename `Options.Profile`,
|
||||
@@ -202,10 +412,40 @@ The [configuration contract](../config.md#selected-overlays) is the canonical
|
||||
behavioral reference. Application-owned profile concepts, such as `model_profile`,
|
||||
are unchanged, and any CLI flag rename remains the application's decision.
|
||||
|
||||
These examples are migration evidence, not completed changes to Narratio, Notarius,
|
||||
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
|
||||
wiring, runtime catalogs, and operational documentation remains separate downstream
|
||||
work.
|
||||
### Background on the examples
|
||||
|
||||
These examples demonstrate migration shapes. They do not claim that Narratio,
|
||||
Notarius, WeatherReporter, or Seriatim has been migrated. Updating those
|
||||
repositories, their deployed YAML, CLI wiring, runtime catalogs, and
|
||||
operational documentation remains separate downstream work.
|
||||
|
||||
## Example background
|
||||
|
||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||
sanitized shapes using only ConfigKit and the Go standard library:
|
||||
|
||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||
| --- | --- | --- |
|
||||
| Artifact defaults and normalization (Narratio-like) | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
|
||||
| Versions and dynamic options (Notarius-like) | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
|
||||
| Module validation and credential startup (WeatherReporter-like) | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
|
||||
| Independent ordered rule files (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 |
|
||||
| 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 | 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 |
|
||||
| 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 |
|
||||
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
|
||||
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
|
||||
|
||||
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||
executable examples and ordinary tests. It contains no private application data,
|
||||
credentials, network calls, or dependency on a sibling checkout.
|
||||
|
||||
The Notarius-shaped fixtures deliberately load both `version: 4` and
|
||||
`version: "4"` into the same string-backed application field. Its version checker
|
||||
still owns the supported identifier; fractional numeric input and unsupported
|
||||
integer identifiers remain negative examples.
|
||||
|
||||
## Maintained API examples
|
||||
|
||||
@@ -217,7 +457,7 @@ focused behavioral owner:
|
||||
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
|
||||
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
|
||||
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
||||
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
||||
| `LoadSecretsIntoEnvironment`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
||||
| Native filesystem, grouped underlays, programmatic mapping, retained resource context | `Example_nativeFilesystem`, `Example_layeredCatalog`, and their focused consumer tests |
|
||||
| Literal input destinations, root-default compatibility, and mounted peer collections | `Example_inputDestination`, `LoadCollectionBundle`, and `destinations_test.go` |
|
||||
| Named sibling inheritance, presence, and derivation | `Example_inheritedPresets` and `TestPresetCatalogOneLoadAndDerivation` |
|
||||
|
||||
@@ -40,9 +40,10 @@ public behavior.
|
||||
| --- | --- |
|
||||
| Repository orientation or component responsibility | [Component overview](internal/overview.md) |
|
||||
| Input destinations | [Configuration contract](config.md#input-destinations), [ADR 0011](adr/0011-input-destinations.md), and the destination tests below |
|
||||
| Shared source-version checks | [Configuration contract](config.md#shared-source-version-checks), [ADR 0013](adr/0013-shared-source-version-checks.md), and the version/source-version tests below |
|
||||
| Documentation changes | The owning document |
|
||||
| Licensing, dependencies, attribution, or release-document changes | [Project grant](../COPYRIGHT) and [attribution](oss/attribution.md); release changes also follow the release procedure |
|
||||
| Architecture decisions or future work | [Deferred scope](roadmap/deferred.md) and relevant ADRs listed below |
|
||||
| Architecture decisions or future work | [Future features](roadmap/future.md) and relevant ADRs listed below |
|
||||
| Code and test changes | The owning contract and tests from the map below |
|
||||
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
|
||||
| Release preparation | [Release procedure](release.md) |
|
||||
@@ -50,6 +51,7 @@ public behavior.
|
||||
Accepted decisions:
|
||||
|
||||
- [Separate composition and overlay](adr/0001-separate-composition-and-overlay.md)
|
||||
- [Check source versions with one shared policy](adr/0013-shared-source-version-checks.md)
|
||||
- [Own YAML semantics behind a private parser boundary](adr/0002-own-yaml-semantics.md)
|
||||
- [Preserve presence and record application resolution](adr/0003-preserve-presence-and-resolution-history.md)
|
||||
- [Secret references and explicit bootstrap](adr/0004-secret-references-and-explicit-bootstrap.md)
|
||||
@@ -57,6 +59,12 @@ Accepted decisions:
|
||||
- [Keep the public facade root-owned](adr/0006-public-facade.md)
|
||||
- [Compare frozen effective configuration snapshots](adr/0010-effective-configuration-diff.md)
|
||||
- [Place inputs at literal application mapping paths](adr/0011-input-destinations.md)
|
||||
- [Share dependency-ready ordering](adr/0015-shared-dependency-ready-ordering.md)
|
||||
- [Separate import selection controls](adr/0016-independent-import-controls.md)
|
||||
- [Define identity and merge rules for each collection](adr/0017-collection-sequence-policy.md)
|
||||
- [Apply CLI overrides after inheritance and decoding](adr/0018-cli-after-structural-resolution.md)
|
||||
- [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)
|
||||
|
||||
For cross-cutting changes, follow every applicable row. Completed implementation
|
||||
and remediation plans are preserved in Git history at `cb5b435`; use current
|
||||
@@ -106,7 +114,7 @@ git show 8e91a2b:docs/roadmap/startup-benchmark.md
|
||||
```
|
||||
|
||||
Remaining optimization candidates belong to
|
||||
[deferred scope](roadmap/deferred.md#deferred-performance-experiments).
|
||||
[future performance experiments](roadmap/future.md#3-performance-experiments).
|
||||
|
||||
The completed effective-configuration diff roadmap, implementation plan, and
|
||||
resolved review findings are preserved in Git history at `1c2610c` under
|
||||
@@ -115,6 +123,110 @@ resolved review findings are preserved in Git history at `1c2610c` under
|
||||
comparison and disclosure behavior, and
|
||||
[ADR 0010](adr/0010-effective-configuration-diff.md) records the decision.
|
||||
|
||||
The completed input-destination, relative-import-destination, and shared
|
||||
source-version roadmaps, associated consumer request, implementation plan, and
|
||||
resolved review findings are preserved in Git history at `e4270c9` under
|
||||
`docs/roadmap/`. For example:
|
||||
|
||||
```sh
|
||||
git show e4270c9:docs/roadmap/implementation.md
|
||||
git show e4270c9:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
Current behavior belongs to the configuration contract's
|
||||
[input destinations](config.md#input-destinations),
|
||||
[imports](config.md#explicit-imports), and
|
||||
[shared source-version checks](config.md#shared-source-version-checks) sections.
|
||||
Explicit sequence-item placement, collection operations, identity registration,
|
||||
and final registered-identity validation are owned by the
|
||||
[strict composition](config.md#strict-document-composition) section;
|
||||
[imports](config.md#explicit-imports) owns acquisition and scheduling. The
|
||||
[maintained sequence catalog](consumers/README.md#identified-prompt-sequences)
|
||||
shows the consumer boundary, and [ADR 0017](adr/0017-collection-sequence-policy.md)
|
||||
records the collection decision.
|
||||
|
||||
The completed sequence-composition roadmap, implementation plan, and resolved
|
||||
remediation record are preserved in Git history at `008d2ba` under
|
||||
`docs/roadmap/`. For example:
|
||||
|
||||
```sh
|
||||
git show 008d2ba:docs/roadmap/sequence-composition.md
|
||||
git show 008d2ba:docs/roadmap/implementation.md
|
||||
git show 008d2ba:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
The completed import-discovery roadmap, implementation plan, and resolved
|
||||
remediation record are preserved in Git history at `5747ec4` under
|
||||
`docs/roadmap/`. For example:
|
||||
|
||||
```sh
|
||||
git show 5747ec4:docs/roadmap/imports.md
|
||||
git show 5747ec4:docs/roadmap/implementation.md
|
||||
git show 5747ec4:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
The subsequent import-integration audit and completed remediation record are
|
||||
preserved at `6903c9e:docs/roadmap/remediation.md`; retrieve them with
|
||||
`git show 6903c9e:docs/roadmap/remediation.md`.
|
||||
|
||||
The completed sequence-identity/collection-policy roadmap and implementation
|
||||
plan are preserved in Git history at `d0a605a`:
|
||||
|
||||
```sh
|
||||
git show d0a605a:docs/roadmap/sequences.md
|
||||
git show d0a605a:docs/roadmap/implementation.md
|
||||
```
|
||||
|
||||
Current behavior belongs to the configuration contract's
|
||||
[strict composition](config.md#strict-document-composition),
|
||||
[sibling inheritance](config.md#sibling-inheritance), and
|
||||
[CLI overrides](config.md#explicit-cli-overrides) sections.
|
||||
The completed remediation record, including the algorithm-audit findings, is
|
||||
preserved at `ea46001:docs/roadmap/remediation.md`.
|
||||
|
||||
The completed configuration-discovery roadmap, implementation plan, and
|
||||
remediation record are preserved in Git history at `0369111`, for example
|
||||
`git show 0369111:docs/roadmap/remediation.md`. Current behavior belongs to
|
||||
[configuration discovery](config.md#configuration-discovery); the public
|
||||
[discovery declarations](../discovery.go) own the Go API.
|
||||
|
||||
The completed consolidation proposal, implementation plan, and resolved audit
|
||||
findings are preserved in Git history at `34cbc0d` under `docs/roadmap/`. For
|
||||
example:
|
||||
|
||||
```sh
|
||||
git show 34cbc0d:docs/roadmap/implementation.md
|
||||
git show 34cbc0d:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
Potential enhancements are tracked in [future features](roadmap/future.md).
|
||||
The completed audit-remediation, internal-boundary simplification, shared-history,
|
||||
and public API cleanup roadmaps, reviews, and resolved findings are preserved at
|
||||
`68400ff` under `docs/roadmap/`. For example:
|
||||
|
||||
```sh
|
||||
git show 68400ff:docs/roadmap/api.md
|
||||
git show 68400ff:docs/roadmap/remediation.md
|
||||
```
|
||||
|
||||
Their earlier completed implementation plans are preserved at
|
||||
`bb5540b:docs/roadmap/implementation.md` (audit remediation),
|
||||
`f6d54a0:docs/roadmap/implementation.md` (internal-boundary simplification), and
|
||||
`07e4ae5:docs/roadmap/implementation.md` (shared history). The plan at
|
||||
`68400ff:docs/roadmap/implementation.md` records the public API cleanup.
|
||||
Current behavior and migration information belong to the
|
||||
[configuration contract](config.md), public declaration comments, and
|
||||
[consumer guide](consumers/README.md#pre-v1-public-api-cleanup).
|
||||
|
||||
The consolidated audit, including
|
||||
findings, evidence, and settled scope decisions, is preserved at
|
||||
`453da68:docs/roadmap/audit.md`; the original nine reports are preserved at
|
||||
`ecd5d5d` under `docs/roadmap/`. Retrieve the consolidated record with:
|
||||
|
||||
```sh
|
||||
git show 453da68:docs/roadmap/audit.md
|
||||
```
|
||||
|
||||
Install the Go version required by `go.mod` or a compatible newer toolchain.
|
||||
Acquire declared dependencies explicitly before entering an offline
|
||||
development or validation environment:
|
||||
@@ -161,24 +273,28 @@ code changes. Use the component overview to find the corresponding implementatio
|
||||
| Task | Behavioral test owners | Focused command |
|
||||
| --- | --- | --- |
|
||||
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` |
|
||||
| File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
||||
| File/reader access, imports, globs, containment | [Import internals](internal/imports.md), [source acquisition](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [import traversal](../internal/engine/imports_test.go), [import evidence](../internal/engine/import_evidence_test.go), [relative destinations](../internal/engine/destination_import_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
|
||||
| Explicit main-file discovery and absent-input loading | [Candidate selection](../internal/engine/discovery_test.go), [load integration](../internal/engine/discovery_load_test.go), [public facade](../facade_discovery_test.go), and maintained [catalog](../examples/consumers/sequence_catalog_test.go) and [preset](../examples/consumers/discovery_test.go) examples | `go test ./internal/engine -run 'Discovery\|Search'`, `go test . -run 'Search'`, and `go test ./examples/consumers -run 'WorkingDirectoryPreset\|SequenceCatalog'` |
|
||||
| Import defaults, empty-selection evidence, and dependency-ready ordering | [Defaults and facade evidence](../facade_import_defaults_test.go), [engine import evidence](../internal/engine/import_evidence_test.go), [scheduler](../internal/engine/dependency_order_test.go), [scheduling integrations](../internal/engine/imports_test.go) | `go test ./internal/engine -run 'Import\|ScheduleDependencies\|Inheritance'`, `go test . -run 'ImportDefaults\|FacadeImport'`, and `go test ./examples/consumers` |
|
||||
| Native filesystem and mapping sources | [Native acquisition](../internal/engine/native_filesystem_test.go), [filesystem facade](../facade_filesystem_test.go), [mapping facade](../facade_mapping_test.go) | `go test ./internal/engine -run 'NativeFilesystem'` and `go test . -run 'TestFacade'` |
|
||||
| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` |
|
||||
| Sibling inheritance, provenance, history, sensitivity | [Expansion](../internal/engine/inheritance_test.go), [destination scopes](../internal/engine/inheritance_walk_test.go), [origins](../internal/engine/inheritance_origin_test.go), [history](../internal/engine/inheritance_history_test.go), [secrets](../internal/engine/inheritance_secret_test.go), [public loading](../facade_inheritance_test.go) | `go test ./internal/engine -run 'Inheritance\|Inherited'` and `go test . -run '^TestFacadeInheritance\|^TestFacadeInheritedSecrets'` |
|
||||
| Sequence identity, declarations, and operations | [Identity](../internal/engine/sequence_identity_test.go), [registrations](../internal/engine/sequence_registration_test.go), [declarations](../internal/engine/sequence_declaration_test.go), [combination](../internal/engine/sequence_combine_test.go), [final facade validation](../facade_final_sequence_identity_test.go), and [consumer catalog](../examples/consumers/sequence_catalog_test.go) | `go test ./internal/engine -run 'Sequence\|Collection'`, `go test . -run 'SequenceIdentity\|SequenceRegistration'`, and `go test ./examples/consumers` |
|
||||
| Sibling inheritance, provenance, history, sensitivity | [Implementation flow](internal/overview.md#implementation-flow), [expansion](../internal/engine/inheritance_test.go), [destination scopes](../internal/engine/inheritance_walk_test.go), [origins](../internal/engine/inheritance_origin_test.go), [history](../internal/engine/inheritance_history_test.go), [secrets](../internal/engine/inheritance_secret_test.go), [public loading](../facade_inheritance_test.go) | `go test ./internal/engine -run 'Inheritance\|Inherited'` and `go test . -run '^TestFacadeInheritance\|^TestFacadeInheritedSecrets'` |
|
||||
| Layered loading and source equivalence | [Grouped lifecycle](../internal/engine/load_request_test.go), [layered facade](../facade_layered_test.go), [equivalence](../facade_equivalence_test.go) | `go test ./internal/engine -run '^TestGrouped'`, `go test . -run '^TestFacadeLayered'`, and `go test . -run '^TestEquivalentSourceForms'` |
|
||||
| Input destinations, mounted sources, and source ownership | [Destination engine tests](../internal/engine/destination_test.go), [tier and import tests](../internal/engine/destination_tiers_test.go), [integration and provenance tests](../internal/engine/destination_integration_test.go), [public facade tests](../facade_destination_test.go), and [consumer examples](../examples/consumers/destinations_test.go) | `go test ./internal/engine -run 'Destination|Mounted'`, `go test . -run 'Destination|FacadeMounted'`, and `go test ./examples/consumers -run 'CollectionBundle|CollectionPeer'` |
|
||||
| Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
||||
| Input and relative-import destinations, mounted sources, and source ownership | [Destination engine tests](../internal/engine/destination_test.go), [tier tests](../internal/engine/destination_tiers_test.go), [import traversal and version tests](../internal/engine/destination_import_test.go), [accounting tests](../internal/engine/destination_accounting_test.go), [lifecycle inspection tests](../internal/engine/destination_import_lifecycle_test.go), [public source/error tests](../facade_destination_test.go), [resource contexts](../facade_resource_context_test.go), and [consumer examples](../examples/consumers/destinations_test.go) | `go test ./internal/engine -run 'Destination\|Import\|Glob\|Mounted'`, `go test . -run 'Destination\|Import\|SourceContext\|FacadeMounted'`, and `go test ./examples/consumers` |
|
||||
| Shared source-version modes and evidence | [Engine source-version tests](../internal/engine/source_version_test.go), [public facade tests](../facade_source_version_mode_test.go), and [consumer example tests](../examples/consumers/layered_test.go) | `go test ./internal/engine -run 'SourceVersion'`, `go test . -run 'SourceVersion'`, and `go test ./examples/consumers` |
|
||||
| Schema, defaults, projection, cloning | [Implementation flow](internal/overview.md#implementation-flow), [schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection and typed cloning](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
|
||||
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
|
||||
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
|
||||
| Passive resource contexts | [Engine contexts](../internal/engine/resource_context_test.go), [public contexts](../facade_resource_context_test.go) | `go test ./internal/engine -run 'Resource'` and `go test . -run 'SourceContext'` |
|
||||
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
|
||||
| Effective configuration diff | [Engine comparison and masking tests](../internal/engine/diff_test.go), [engine operation tests](../internal/engine/diff_operation_test.go), [root facade tests](../facade_diff_test.go), and [consumer examples](../examples/consumers/diff_test.go) | `go test ./internal/engine -run 'Diff|Explain|History|Secret|Independent'`, `go test . -run 'Diff|FacadeInspection|PublicFacadeBoundary|DependencyImportBoundary'`, `go test ./examples/consumers`, and `go test . -run '^ExampleDiff$'` |
|
||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native and layered examples](../examples/consumers/layered_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
||||
| Effective configuration diff | [Engine comparison and masking tests](../internal/engine/diff_test.go), [engine operation tests](../internal/engine/diff_operation_test.go), [root facade tests](../facade_diff_test.go), and [consumer examples](../examples/consumers/diff_test.go) | `go test ./internal/engine -run 'Diff\|Explain\|History\|Secret\|Independent'`, `go test . -run 'Diff\|FacadeInspection\|PublicFacadeBoundary\|DependencyImportBoundary'`, `go test ./examples/consumers`, and `go test . -run '^ExampleDiff$'` |
|
||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native, layered, and ordinary relative-import examples](../examples/consumers/layered_test.go), [mounted collection examples](../examples/consumers/destinations_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
|
||||
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
|
||||
| Identified-sequence scaling evidence | [Mixed-upsert benchmark](../internal/engine/sequence_combine_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkSequenceMixedUpsert$' -benchmem` |
|
||||
| Inheritance scaling evidence | [Expansion benchmarks](../internal/engine/inheritance_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkInheritance' -benchmem` |
|
||||
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
|
||||
| Development check behavior | [Development checker test](../development_check_test.go) | `go test . -run '^TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure$'` |
|
||||
| Release profile wiring | [Release profile test](../releasekit_profile_test.go) | `go test . -run '^TestReleaseKitProfile$'` |
|
||||
|
||||
## Fuzz validation
|
||||
|
||||
|
||||
125
docs/internal/imports.md
Normal file
125
docs/internal/imports.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Import Discovery and Scheduling Internals
|
||||
|
||||
The [configuration contract](../config.md#explicit-imports) owns selector syntax,
|
||||
defaults, requiredness, placement, inspection evidence, and observable ordering.
|
||||
This note records implementation flow and private identities for maintainers.
|
||||
|
||||
## Discovery and acquisition
|
||||
|
||||
Each source's root directive is parsed into presence-aware declarations and
|
||||
resolved against load-wide defaults. `acquisition.go` owns shared acquisition
|
||||
policy, load-wide accounting, and tier-scoped repetition/namespace state;
|
||||
`import_acquisition.go` owns reading candidate bytes, declaration traversal,
|
||||
and import admission. Candidate
|
||||
selection in `import_selection.go` preserves a normalized
|
||||
namespace path separately from canonical access identity: overlapping roots
|
||||
coalesce the same candidate spelling in that namespace, while aliases reached
|
||||
through distinct spellings retain separate inclusion routes for admission.
|
||||
Directory searches walk each distinct normalized root once, apply all filename
|
||||
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.
|
||||
|
||||
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
|
||||
not mistaken for one. This is separate from import dependency-cycle detection.
|
||||
For host sources, discovery uses rooted `Lstat` and canonical containment.
|
||||
`filesystem_context.go` owns resolved access facts and live access setup; the
|
||||
acquisition operation closes its live handles before composition. Native sources
|
||||
use `fs.ReadLinkFS` inspection when supplied; otherwise traversal follows the
|
||||
capabilities and behavior of the provided `fs.FS`, without claiming a general
|
||||
sandbox guarantee. `source_context.go` derives passive resource contexts from
|
||||
resolved sources for later caller-owned reads; those records do not retain live
|
||||
acquisition handles.
|
||||
|
||||
Host and native link bindings share one iterative link-frame evaluator. It
|
||||
tracks active links for cycle detection and reuses successfully completed link
|
||||
targets while resolving the remaining path components; each binding retains its
|
||||
own namespace and target-containment rules. Directory reads distinguish an
|
||||
initial-open failure from failures after opening: only an initial missing
|
||||
directory can be treated as optional, while enumeration and close failures
|
||||
remain selection errors, including errors from the standard `fs.ReadDir`
|
||||
fallback.
|
||||
|
||||
Glob selection keeps traversal coordination in `selectGlobImportCandidates`:
|
||||
it advances literal and patterned segments, reads directories, unions and sorts
|
||||
candidates, validates final targets, then applies exclusions and requiredness.
|
||||
For each patterned directory entry, `prepareGlobEntry` handles matching and
|
||||
hidden filtering, containment, target and link inspection, intermediate-directory
|
||||
eligibility, host/native access-path selection, and candidate identity and label.
|
||||
It returns a candidate, an ordinary skip, or a diagnostic without mutating the
|
||||
selector's candidate set or traversal state. Directory entry preparation remains
|
||||
separate because its matching, absence, recursion, and traversal-state rules
|
||||
differ.
|
||||
|
||||
Acquisition remains authored: declarations are visited in source order and each
|
||||
selected candidate set in namespace-path lexical order. This preserves source
|
||||
records, IDs, source-local version checks, and callback ordering. The acquired
|
||||
inclusion graph is retained separately from composition scopes so cycles that
|
||||
cross item boundaries remain detectable. Source discovery does not itself choose
|
||||
sequence assembly order.
|
||||
|
||||
## Stable keys and scheduling
|
||||
|
||||
The shared scheduler compares immutable tuples component by component using Go's
|
||||
bytewise string order; if one tuple is a prefix of another, the shorter tuple
|
||||
sorts first. The import key is
|
||||
`(namespace-kind, namespace-anchor, resource-kind, resource-path)`:
|
||||
|
||||
- Namespace kind is `host` or `native`. Host resource paths are canonical
|
||||
absolute paths rendered with slash separators; native paths retain their
|
||||
namespace-relative identity.
|
||||
- Anchors come from explicit input positions, not discovery order: main is
|
||||
`(0,0)` and underlay/input positions are `(group+1,input+1)`, each encoded as
|
||||
fixed-width decimal. Host accesses in one scheduled tier share its host anchor.
|
||||
A native filesystem namespace uses its earliest explicit selecting position
|
||||
in that tier. Filesystem tokens distinguish namespace equality, not sort order,
|
||||
so separately constructed filesystems with the same label remain distinct.
|
||||
- Resource kind is `file` or `synthetic`. A synthetic root uses its explicit
|
||||
input anchor and an empty resource path rather than a display label or token
|
||||
address.
|
||||
|
||||
Inheritance keys are
|
||||
`("definition", scope-pointer, definition-pointer)`, where both pointers use
|
||||
canonical escaped semantic path spelling. Source IDs are not part of the key.
|
||||
|
||||
Both integrations use the same Kahn scheduler: it deduplicates dependency edges,
|
||||
keeps currently ready nodes in a min-heap, and selects the smallest ready key
|
||||
after every completion. Its scheduling cost is `O(E + V log V)` time and
|
||||
`O(V + E)` additional memory, excluding key construction, tuple copying, and
|
||||
tuple comparisons. On a stalled graph, deterministic DFS over the residual graph
|
||||
returns a closed cycle rotated to its smallest key; blocked dependents are not
|
||||
reported as cycle members.
|
||||
|
||||
Import edge preparation builds an importer-to-target adjacency index once for
|
||||
an acquisition's outer and item-local scheduling, and once for grouped
|
||||
scheduling. Each scope gathers edges from its participating identities and
|
||||
retains only edges with both endpoints in its local slots. The retained
|
||||
inclusion graph still owns cycle diagnostics and source relationships.
|
||||
|
||||
Import composition schedules a main tier or one underlay group as a single
|
||||
strict-scope graph. Applicable collection declarations are gathered for the
|
||||
scope before peer composition, including identity inherited from lower tiers.
|
||||
Identified peer sequences then combine distinct records in assembled source and
|
||||
list order; after the strict scope completes, layered assembly applies the
|
||||
resolved collection operation against the lower value, as defined by the
|
||||
[configuration contract](../config.md#strict-document-composition). Item
|
||||
contributions own nested local strict-mapping scopes; their helpers complete
|
||||
before the item is placed in its outer scope. Inheritance uses the same
|
||||
ready-node rule inside each sibling mapping scope, while preserving the authored
|
||||
mapping order in its output.
|
||||
These scopes do not flatten into one global graph, and independent precedence
|
||||
tiers remain separate.
|
||||
|
||||
The owners are [`acquisition.go`](../../internal/engine/acquisition.go),
|
||||
[`import_selection.go`](../../internal/engine/import_selection.go),
|
||||
[`import_acquisition.go`](../../internal/engine/import_acquisition.go),
|
||||
[`directory_walk.go`](../../internal/engine/directory_walk.go),
|
||||
[`import_graph.go`](../../internal/engine/import_graph.go),
|
||||
[`import_projection.go`](../../internal/engine/import_projection.go),
|
||||
[`dependency_order.go`](../../internal/engine/dependency_order.go), and
|
||||
[`inheritance_schedule.go`](../../internal/engine/inheritance_schedule.go).
|
||||
Their focused behavior is covered by import-selection, acquisition, scheduler,
|
||||
and scheduling integration tests under `internal/engine`; see the
|
||||
[development test map](../development.md#task-to-test-map).
|
||||
@@ -7,23 +7,41 @@ private state; public signatures and fields do not expose engine types.
|
||||
|
||||
Implemented component owners:
|
||||
|
||||
- `internal/engine` owns host and native-filesystem source acquisition,
|
||||
grouped strict composition, recursive precedence assembly, per-input mapping
|
||||
placement within that assembly, and destination-local sibling inheritance
|
||||
before typed decoding. It retains source placement alongside physical source
|
||||
- `internal/engine` owns host and native-filesystem source acquisition and
|
||||
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,
|
||||
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
|
||||
collection operation at precedence boundaries and owns dependency-ready,
|
||||
destination-local inheritance before typed decoding. CLI assignment follows
|
||||
inheritance and decoding, before application resolution. The engine also owns
|
||||
shared source-version checking with sanitized declaration evidence.
|
||||
Explicit import discovery supports declared paths, directory searches, and
|
||||
globs; root-owned defaults are converted to the engine's single defaults input
|
||||
and snapshotted during load preflight before acquisition, while source-local
|
||||
empty-selection evidence crosses the facade without changing acquisition
|
||||
order. The root constructs placement directly on engine options and inputs
|
||||
using explicit enum conversion; public types and fields remain root-owned.
|
||||
Import and inheritance resolution share dependency-ready scheduling within
|
||||
their composition scopes.
|
||||
It retains each source's effective placement alongside physical source
|
||||
identity and owns schema
|
||||
inspection, lifecycle coordination, callback auditing,
|
||||
provenance/history, passive source-context retention, secret resolution and
|
||||
bootstrap, safe inspection rendering, and effective-value comparison over
|
||||
bootstrap, operation-local secret failure-location attribution, safe
|
||||
inspection rendering, and effective-value comparison over
|
||||
frozen inspection snapshots. It never imports the root package.
|
||||
- Root `Source`, `Filesystem`, `Input`, `Underlay`, `Path`, `Secret`, records,
|
||||
lifecycle callbacks, results, and bootstrap reports are root-defined wrappers
|
||||
or records. Callback wrappers pass only live engine context during a callback;
|
||||
`Result.Config` is detached and its private inspection remains frozen
|
||||
independently.
|
||||
- Root conversion files explicitly cross the path, record, callback, secret, and
|
||||
error boundaries. There are no public aliases, anonymous engine embeddings, or
|
||||
public engine bridge methods.
|
||||
- Root conversion files explicitly cross the path, record, callback, secret,
|
||||
search-description, and error boundaries. There are no public aliases,
|
||||
anonymous engine embeddings, or public engine bridge methods.
|
||||
- Root `Diff` and `Difference` provide the public comparison boundary and
|
||||
convert detached engine records to root-owned paths and snapshots; consumer
|
||||
examples own presentation of the safe changed regions.
|
||||
@@ -43,8 +61,16 @@ these constraints and rejects exported aliases, fields, or signatures involving
|
||||
internal types.
|
||||
|
||||
The [configuration contract](../config.md) owns implemented YAML and lifecycle
|
||||
semantics, including [input destinations](../config.md#input-destinations) and
|
||||
semantics, including
|
||||
[configuration discovery](../config.md#configuration-discovery),
|
||||
[input destinations](../config.md#input-destinations),
|
||||
[shared source-version checks](../config.md#shared-source-version-checks), and
|
||||
[effective configuration diffs](../config.md#effective-configuration-diffs).
|
||||
The [strict composition contract](../config.md#strict-document-composition)
|
||||
owns sequence declarations, identity, operations, ownership, and final
|
||||
validation; [CLI overrides](../config.md#explicit-cli-overrides) own override
|
||||
ordering. The [consumer guide](../consumers/README.md#identified-prompt-sequences)
|
||||
links maintained workflow examples.
|
||||
The [consumer adoption guide](../consumers/README.md) owns application guidance.
|
||||
The [architecture policy](../policy/architecture.md) owns contributor
|
||||
boundaries; [ADR 0006](../adr/0006-public-facade.md) records the facade decision
|
||||
@@ -53,4 +79,111 @@ decision; [ADR 0011](../adr/0011-input-destinations.md) records the placement
|
||||
decision. See the [development guide](../development.md) for validation commands
|
||||
and [release procedure](../release.md) for source-candidate gates. [ADR
|
||||
0009](../adr/0009-sibling-inheritance.md) records why inheritance is a built-in
|
||||
post-assembly structural operation.
|
||||
post-assembly structural operation. [ADR 0012](../adr/0012-relative-import-destinations.md)
|
||||
records the relative-import extension; [ADR 0013](../adr/0013-shared-source-version-checks.md)
|
||||
records the source-version decision.
|
||||
[ADR 0014](../adr/0014-sequence-placement-and-composition.md) records explicit
|
||||
sequence placement and composition. [ADR 0015](../adr/0015-shared-dependency-ready-ordering.md)
|
||||
records the shared dependency-ready ordering decision; [ADR
|
||||
0016](../adr/0016-independent-import-controls.md) records the independent
|
||||
selection controls. [ADR 0017](../adr/0017-collection-sequence-policy.md) and
|
||||
[ADR 0018](../adr/0018-cli-after-structural-resolution.md) record collection
|
||||
operations and CLI ordering. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
|
||||
records explicit main-file discovery. The [import internals](imports.md)
|
||||
document candidate and namespace identities, discovery and scheduling, and the
|
||||
bounded algorithms.
|
||||
|
||||
## Implementation flow
|
||||
|
||||
The root facade converts caller-owned values into engine-owned requests. Its
|
||||
`ImportDefaults` conversion maps fields without taking ownership; engine load
|
||||
preflight snapshots the defaults once before source acquisition or callbacks.
|
||||
Ordinary and grouped loading then carry that read-only value through their
|
||||
normalized `loadRequest` and acquisition policy, while declaration resolution
|
||||
copies values needed by its independently owned effective policy. Placement is
|
||||
constructed directly on the engine's options and input records after explicit
|
||||
root-to-engine enum conversion. See
|
||||
[`lifecycle.go`](../../lifecycle.go), [`layered.go`](../../layered.go), and
|
||||
[`load_request.go`](../../internal/engine/load_request.go). Load-wide definition,
|
||||
version, default-import, and override policy stays distinct from each input's
|
||||
source, filesystem, destination, and import settings.
|
||||
|
||||
[`acquisition.go`](../../internal/engine/acquisition.go) defines acquisition
|
||||
policy, load-wide accounting, and tier-scoped repetition/namespace state. Each
|
||||
input is acquired under that policy. Source-root controls are extracted from
|
||||
each acquired tree before placement; assembled application data is not parsed
|
||||
again for controls. Grouped requests retain acquired underlay
|
||||
scopes until their tier is composed in
|
||||
[`load_request.go`](../../internal/engine/load_request.go). Completed tiers then
|
||||
share one assembly handoff that applies collection declarations and projects
|
||||
only that tier's source/version evidence; the caller keeps main-version
|
||||
protection and its projection explicit. Live
|
||||
filesystem access handles are confined to acquisition and closed there. Resolved
|
||||
filesystem facts used to authorize access are separate from passively retained
|
||||
resource contexts. Non-import validation retains those facts on `assembledInput`
|
||||
for passive-context construction and ordinary-overlay handling: see
|
||||
[`filesystem_context.go`](../../internal/engine/filesystem_context.go) and
|
||||
[`source_context.go`](../../internal/engine/source_context.go).
|
||||
|
||||
Import responsibilities are split across candidate selection and path
|
||||
normalization in [`import_selection.go`](../../internal/engine/import_selection.go),
|
||||
source reading and traversal admission in
|
||||
[`import_acquisition.go`](../../internal/engine/import_acquisition.go),
|
||||
directory walking in [`directory_walk.go`](../../internal/engine/directory_walk.go),
|
||||
dependency graph scheduling in [`import_graph.go`](../../internal/engine/import_graph.go),
|
||||
and projection of acquired evidence into final source coordinates in
|
||||
[`import_projection.go`](../../internal/engine/import_projection.go). The
|
||||
[import internals](imports.md) page describes the boundaries and identities.
|
||||
|
||||
After acquisition handles are closed, grouped composition and inheritance run on
|
||||
the retained ordered documents. `inheritance.go` coordinates traversal and
|
||||
[`inheritance_schedule.go`](../../internal/engine/inheritance_schedule.go) applies
|
||||
dependency-ready templates;
|
||||
[`inheritance_prepare.go`](../../internal/engine/inheritance_prepare.go)
|
||||
prepares child and parent inputs,
|
||||
[`inheritance_combine.go`](../../internal/engine/inheritance_combine.go)
|
||||
returns the combined value with its origin, policy, sequence, and diagnostic
|
||||
evidence, and [`inheritance_finalize.go`](../../internal/engine/inheritance_finalize.go)
|
||||
accounts for and finalizes the result. Projection and schema work have separate
|
||||
operation owners: [`value_operation.go`](../../internal/engine/value_operation.go)
|
||||
holds per-operation state, [`projection.go`](../../internal/engine/projection.go)
|
||||
builds projected values, [`schema.go`](../../internal/engine/schema.go) owns
|
||||
descriptor caching and type policy, and
|
||||
[`schema_fields.go`](../../internal/engine/schema_fields.go) owns ordinary and
|
||||
inline field assembly. [`decode.go`](../../internal/engine/decode.go) consumes
|
||||
those descriptors for typed decoding.
|
||||
|
||||
History and inspection use one private evidence owner. The
|
||||
[`history_model.go`](../../internal/engine/history_model.go) builder receives
|
||||
baseline, source-composition, inheritance, typed-checkpoint, and CLI evidence
|
||||
in [`preparePostDecodeOverrideContext`](../../internal/engine/override_context.go),
|
||||
before application resolution begins. The callback retains its separate fixed
|
||||
entry view; it does not read a builder that continues to change. After successful
|
||||
resolution auditing, [`resolveLoadInput`](../../internal/engine/load_result.go)
|
||||
adds the original resolution records and their net replay groups. At the existing
|
||||
pre-secret admission boundary, [`prepareInspectionHistory`](../../internal/engine/load_result.go)
|
||||
freezes that builder and counts the retained document roots before secret lookup
|
||||
and validation.
|
||||
|
||||
The frozen model is shared by `Metadata` and `Inspection`. Its shape-aware
|
||||
selection in [`history_select.go`](../../internal/engine/history_select.go)
|
||||
drives metadata history, explanation, and historical-path membership; the model
|
||||
also supplies historical diagnostic locations and sensitivity/dependency
|
||||
evidence. `Inspection` replays selected payloads to produce detached, safely
|
||||
rendered values, while metadata queries return detached attribution records.
|
||||
History and explanation behavior is covered by focused `history_*_test.go`
|
||||
tests alongside metadata, secret, inheritance, and facade integration tests.
|
||||
|
||||
Behavioral ownership follows the same handoffs: imports and grouped lifetimes are
|
||||
covered by `imports_test.go`, `load_request_test.go`, and focused source,
|
||||
discovery, and destination tests; inheritance by the inheritance test files;
|
||||
reflection behavior by `schema_test.go`, `projection_test.go`, and
|
||||
`decode_test.go`. The [development test map](../development.md#task-to-test-map)
|
||||
links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed
|
||||
copying, using the shared descriptors and per-operation traversal helpers.
|
||||
|
||||
Each schema operation builds its private reflection descriptor graph with
|
||||
operation-local caching; there is no global mutable descriptor cache. Decoding,
|
||||
projection, and cloning reuse these descriptors so field bindings agree. The
|
||||
public reference describes the resulting type, output, and ownership behavior;
|
||||
this descriptor storage is an implementation detail.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Base Policy Version:** 1.3
|
||||
|
||||
**Last Updated**: 2026-09-29
|
||||
**Last Updated**: 2026-10-22
|
||||
|
||||
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/architecture.md)
|
||||
|
||||
@@ -19,8 +19,8 @@ to ADRs for decision rationale.
|
||||
ConfigKit is an opinionated Go library for consistent YAML configuration loading
|
||||
and resolution across consuming applications. The [component overview](../internal/overview.md)
|
||||
owns implementation inventory, the [configuration contract](../config.md) owns
|
||||
implemented behavior, and the [deferred scope](../roadmap/deferred.md) preserves
|
||||
deferred scope.
|
||||
implemented behavior, and the [future feature list](../roadmap/future.md) preserves
|
||||
candidates for future consideration.
|
||||
|
||||
## Adopted Constraints And Implemented Structure
|
||||
|
||||
@@ -32,6 +32,12 @@ structure as implemented when its changes land.
|
||||
Constraints must reflect deliberately selected requirements and decisions.
|
||||
Do not promote incidental implementation choices into architectural invariants.
|
||||
|
||||
The implemented collection-policy and CLI-ordering constraints below reflect
|
||||
accepted [ADR 0017](../adr/0017-collection-sequence-policy.md) and
|
||||
[ADR 0018](../adr/0018-cli-after-structural-resolution.md). The configuration
|
||||
contract owns their current behavior; these architectural constraints describe
|
||||
the system boundaries and guarantees that guide changes to that behavior.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
ConfigKit prioritizes a coherent configuration model: consistency and
|
||||
@@ -91,24 +97,82 @@ boundaries, and keep simple consumers independent of optional composition
|
||||
features. The [consumer adoption guide](../consumers/README.md) demonstrates the
|
||||
implemented surface against representative simple and complex shapes.
|
||||
|
||||
## Simplicity, Defaults, And Explicit Controls
|
||||
|
||||
These principles apply throughout ConfigKit: to configuration syntax, the public
|
||||
Go API, defaults, and feature design. Evaluate their effects on both configuration
|
||||
authors and consuming applications.
|
||||
|
||||
**Keep straightforward workflows straightforward.** Common configuration
|
||||
workflows should require little configuration text and minimal API setup.
|
||||
Support more demanding workflows through explicit, composable options, without
|
||||
requiring simple consumers to understand or configure those options.
|
||||
|
||||
**Prefer simple, permissive defaults with explicit controls.** Choose predictable
|
||||
defaults that minimize required setup and accommodate valid configurations
|
||||
without unnecessary restrictions. Provide selectable options for workflows that
|
||||
need additional capabilities, narrower behavior, or stronger requirements.
|
||||
|
||||
Measure simplicity by what consumers must understand and maintain, including
|
||||
configuration text and application-side plumbing. Concise syntax is useful, but
|
||||
hidden inference and interacting meanings can make a shorter declaration harder
|
||||
to understand. Prefer explicitness where it makes behavior more predictable.
|
||||
A terse configuration format that requires substantial application-side
|
||||
infrastructure does not satisfy the simplicity principle.
|
||||
|
||||
Define what permissiveness means for each behavior. Allowing an optional
|
||||
contribution to be absent or accepting valid inputs without an unnecessary
|
||||
filename restriction can reduce required setup. Invalid declarations, ambiguous
|
||||
composition, and unreadable selected sources must still produce meaningful
|
||||
errors under the applicable contracts. Permissiveness does not authorize silent
|
||||
recovery from invalid supplied configuration or weaken configuration integrity.
|
||||
|
||||
Options should express coherent choices with understandable purposes and fit
|
||||
ConfigKit's shared configuration model. Support powerful controls when they
|
||||
serve concrete needs; these principles do not require a switch for every
|
||||
implementation behavior or conceivable consumer preference.
|
||||
|
||||
Defaults require judgment when simplicity and permissiveness pull in different
|
||||
directions. Broader discovery can accommodate more layouts while increasing the
|
||||
implicit search scope. Evaluate such tradeoffs explicitly; these principles do
|
||||
not mechanically select the broadest behavior for every option. Changes to
|
||||
existing defaults remain subject to compatibility review, and proposed behavior
|
||||
remains in the roadmap until implemented.
|
||||
|
||||
## Responsibility Boundaries
|
||||
|
||||
ConfigKit owns configuration input processing and the coordination of decoding,
|
||||
resolution, validation, and inspection. Applications own their configuration
|
||||
types, field meanings, defaults, normalization, semantic validation, and schema
|
||||
version policy. Applications also own main-file search conventions, CLI wiring,
|
||||
remote acquisition, runtime initialization, and resource checks.
|
||||
version policy. Applications choose and order their main-file search conventions;
|
||||
ConfigKit provides explicitly selected search descriptions that resolve during
|
||||
loading. Applications also own CLI wiring, remote acquisition, runtime
|
||||
initialization, and resource checks. See the [configuration discovery
|
||||
contract](../config.md#configuration-discovery) and [ADR
|
||||
0019](../adr/0019-explicit-configuration-discovery.md).
|
||||
|
||||
The engine applies each input's mapping destination within the existing source
|
||||
assembly and retains that placement with source identity and inspection records;
|
||||
assembly, applies immediate-parent-relative YAML destinations to imports, and
|
||||
retains each effective placement with source identity and inspection records;
|
||||
the root facade owns the public fields and conversions. The
|
||||
[configuration contract](../config.md#input-destinations) defines the observable
|
||||
placement behavior, and [ADR 0011](../adr/0011-input-destinations.md) records the
|
||||
accepted decision.
|
||||
placement behavior. [ADR 0011](../adr/0011-input-destinations.md) records input
|
||||
placement, and [ADR 0012](../adr/0012-relative-import-destinations.md) records
|
||||
the import extension.
|
||||
|
||||
Library code must not depend on consuming applications or initialize their
|
||||
runtime resources. It must not perform implicit network access, discover
|
||||
application-specific paths, or change process-global state during ordinary `Load`.
|
||||
runtime resources. It must not perform implicit network access, implicitly
|
||||
discover application-specific paths, or change process-global state during
|
||||
ordinary `Load`.
|
||||
Import discovery is available only through declarations in selected sources:
|
||||
explicit `path`, `dir`, and `glob` selectors may search their declared scope, but
|
||||
ConfigKit does not search conventional application paths unless the application
|
||||
explicitly supplies a discovery description, and it never discovers a main
|
||||
configuration file implicitly. Imports and sibling inheritance share
|
||||
dependency-ready scheduling within their existing scopes; acquisition order and
|
||||
precedence-tier boundaries remain distinct. The [configuration contract](../config.md#explicit-imports)
|
||||
owns observable behavior; [ADR 0015](../adr/0015-shared-dependency-ready-ordering.md)
|
||||
and [ADR 0016](../adr/0016-independent-import-controls.md) record the decisions.
|
||||
The explicit secrets-directory bootstrap is a narrow, opt-in exception: applications
|
||||
invoke it during startup, and it reports writes, skips and partial failures. It
|
||||
must never run implicitly as part of configuration loading.
|
||||
@@ -131,6 +195,31 @@ The [configuration contract](../config.md#completion-and-resource-lifetime) owns
|
||||
the precise completion and ownership behavior. The proposed decision record is
|
||||
[ADR 0008](../adr/0008-complete-on-return-loading.md).
|
||||
|
||||
### Accepted Processing Order
|
||||
|
||||
Complete source composition and precedence, including selected overlays, before
|
||||
sibling inheritance. Then apply typed defaults and decode the configuration.
|
||||
Apply explicit CLI overrides to that configuration and check their types before
|
||||
running application resolution. Effective secret lookup and final validation
|
||||
follow resolution, before publication. [ADR 0018](../adr/0018-cli-after-structural-resolution.md)
|
||||
records this ordering.
|
||||
|
||||
CLI overrides are the highest-priority input to application resolution. Supplied
|
||||
sequences replace whole sequences, and mappings combine recursively. Inheritance
|
||||
and typed defaulting do not run again after CLI application. CLI paths and values
|
||||
must not introduce inheritance controls; source-document controls are interpreted
|
||||
only at acquisition, before placement, and later application `config` data is
|
||||
not reinterpreted. Earlier source, inheritance, and
|
||||
decoding failures remain errors even if a CLI assignment would replace the value.
|
||||
Semantic validation assesses the final configuration after overrides and resolution.
|
||||
|
||||
Applications own flag parsing and domain resolution. ConfigKit owns override
|
||||
application, validation, and source attribution. Resolution receives CLI values
|
||||
with their explicit-presence and source information. Absence-based defaults
|
||||
preserve explicit values; recorded normalization and derivation can still change
|
||||
them. Typed defaults and application resolution retain their own assignment
|
||||
semantics, independently of collection operations during source assembly.
|
||||
|
||||
## Public API And Dependency Direction
|
||||
|
||||
Keep one small, coherent public surface for the supported lifecycle. Simple
|
||||
@@ -159,31 +248,44 @@ framework on this library.
|
||||
|
||||
## Configuration Integrity
|
||||
|
||||
Within a configuration tier, independent contributions use strict composition:
|
||||
shared mappings combine recursively, while overlapping non-mapping definitions
|
||||
are conflicts, including equal values. Between precedence tiers, intentional
|
||||
overlay combines mappings recursively, retains omitted lower-priority children,
|
||||
and replaces other present values according to the existing overlay rules.
|
||||
Tier precedence is explicit; traversal order within a tier establishes stable
|
||||
reporting, not permission to override. The distinction and its rationale are
|
||||
recorded in [ADR 0001](../adr/0001-separate-composition-and-overlay.md).
|
||||
Within a configuration tier, independent contributions use strict composition.
|
||||
Shared mappings combine recursively. Sequences with an applicable identity
|
||||
combine distinct records and reject duplicate identities, including equal
|
||||
records. Other overlapping non-mapping values remain conflicts, including equal
|
||||
values. Conflicting identity or operation declarations among peers also fail.
|
||||
Traversal order establishes stable reporting and record order; it does not grant
|
||||
precedence to peers. [ADR 0001](../adr/0001-separate-composition-and-overlay.md)
|
||||
records the distinction between strict composition and intentional overrides.
|
||||
|
||||
The main document and its imports form a strictly composed tier. Explicit
|
||||
underlay groups compose strictly within each group, then combine recursively
|
||||
from lowest to highest beneath the main tier. Source acquisition kind does not
|
||||
determine its precedence role. Input-specific constraints, including CLI path
|
||||
rules and opt-in version ownership, remain part of the configuration contract. Consistent combination
|
||||
semantics do not require identical acquisition or lifetime behavior from every
|
||||
source kind.
|
||||
underlay groups compose strictly within each group, then combine from lowest to
|
||||
highest beneath the main tier. Between tiers, mappings combine recursively and
|
||||
retain omitted lower-priority children. Sequence values follow collection policy;
|
||||
other present values retain the established replacement rules. Source acquisition
|
||||
kind does not determine precedence. Input-specific constraints, including version
|
||||
ownership and path protections, remain part of the configuration contract.
|
||||
Consistent combination semantics do not require identical acquisition or lifetime
|
||||
behavior from every source kind.
|
||||
|
||||
After source precedence is complete, built-in sibling `extends` relationships
|
||||
resolve structurally in the same parser-independent representation, using the
|
||||
existing recursive overlay rules. This adds neither an acquired source nor a
|
||||
consumer registration mechanism. Enclosing relationships precede nested
|
||||
destination-local resolution; typed defaults, application resolution, and
|
||||
secret lookup retain their existing lifecycle roles. The [configuration
|
||||
contract](../config.md#sibling-inheritance) owns the exact syntax and presence
|
||||
rules, and [ADR 0009](../adr/0009-sibling-inheritance.md) records the rationale.
|
||||
Built-in sibling `extends` relationships resolve after source precedence in the
|
||||
parser-independent document representation. Preserve existing mapping-sibling
|
||||
scopes and allow an identified sequence to provide a sibling scope for its
|
||||
records. A record may inherit from one parent in that sequence, selected by
|
||||
complete identity. The child supplies and keeps its own identity while inheriting
|
||||
the parent's other settings. Parents remain ordinary records subject to normal
|
||||
validation.
|
||||
|
||||
Use the shared dependency ordering and preserve assembled sequence order.
|
||||
Resolve enclosing relationships before nested destination-local relationships.
|
||||
Mappings combine recursively; sequences use the inheriting collection's policy.
|
||||
Whole-record upsert replaces records during combination; explicit inheritance
|
||||
uses the records that remain. Inheritance remains built-in and creates no
|
||||
acquired source.
|
||||
[ADR 0009](../adr/0009-sibling-inheritance.md) records the
|
||||
original scope and continuing ownership decisions, with sequence combination
|
||||
and identified-sequence inheritance defined by
|
||||
[ADR 0017](../adr/0017-collection-sequence-policy.md) and CLI ordering by
|
||||
[ADR 0018](../adr/0018-cli-after-structural-resolution.md).
|
||||
|
||||
ConfigKit must define supported YAML semantics explicitly rather than inherit
|
||||
parser defaults. Processing order and diagnostics must be deterministic for the
|
||||
@@ -192,13 +294,14 @@ configuration. Do not run application hooks on invalid typed configuration.
|
||||
|
||||
User-controlled precedence is application defaults, ordered underlays, composed
|
||||
main input, selected overlay, then explicitly supplied CLI overrides.
|
||||
Applications own flag parsing; ConfigKit owns overlay semantics and provenance.
|
||||
Domain resolution follows these
|
||||
inputs and must remain recorded, not become a hidden override channel.
|
||||
This priority order is distinct from processing order: typed defaults are applied
|
||||
at decoding, and application resolution follows CLI input under the lifecycle
|
||||
rules above.
|
||||
|
||||
Preserve node-based composition until the effective input is ready for typed
|
||||
decoding. Preserve presence and source information through processing. Ordinary Go zero
|
||||
values must not be used to infer whether input was absent. Final configuration,
|
||||
Preserve node-based composition through source assembly and inheritance until
|
||||
typed decoding. Preserve presence and source information through later CLI and
|
||||
application-resolution changes as well. Ordinary Go zero values must not be used
|
||||
to infer whether input was absent. Final configuration,
|
||||
metadata, and effective inspection must agree, including application resolution
|
||||
changes; see [ADR 0003](../adr/0003-preserve-presence-and-resolution-history.md).
|
||||
Safe historical values and an explanation renderer are part of the implemented contract,
|
||||
@@ -215,6 +318,72 @@ comparison and disclosure behavior, the [component overview](../internal/overvie
|
||||
owns implementation responsibility, and [ADR 0010](../adr/0010-effective-configuration-diff.md)
|
||||
records the accepted decision.
|
||||
|
||||
## Collection Identity And Combination
|
||||
|
||||
ConfigKit owns a common collection policy for imported records, inline sequences,
|
||||
selected overlays, and sibling inheritance. Configuration authors may declare
|
||||
identity and combination rules; applications may register authoritative identity
|
||||
constraints. Configuration declarations must agree with application constraints.
|
||||
Keep source placement explicit and separate from policy, retaining the placement
|
||||
decision in [ADR 0014](../adr/0014-sequence-placement-and-composition.md).
|
||||
[ADR 0017](../adr/0017-collection-sequence-policy.md) owns the rationale for
|
||||
collection policy and its relationship to application authority.
|
||||
|
||||
Gather declarations for the relevant destination and source or item-local scope
|
||||
before strict composition. Applicable identity from accumulated lower tiers must
|
||||
be available when composing higher-tier peers. Higher-tier declarations cannot
|
||||
repair invalid lower-tier composition. Preserve the distinction between source
|
||||
acquisition, dependency-ready scheduling within scopes, and precedence order.
|
||||
|
||||
Strictly compose each tier's contributions before applying one operation for the
|
||||
collection against its lower value. Support whole-sequence replacement,
|
||||
duplicate-rejecting append, and whole-record replacement by identity (`upsert`).
|
||||
Append and upsert require identity; do not infer it from record fields or position.
|
||||
Duplicate identities among peers fail under every mode. Source precedence and
|
||||
inheritance use the same operations and deterministic ordering rules. Whole-record
|
||||
upsert replaces the matching record's fields together; recursive record merging
|
||||
and unkeyed sequence concatenation are outside this operation model.
|
||||
|
||||
An explicit mode persists at its applicable destination until a higher explicit
|
||||
mode changes it. With no explicit mode, effective identity selects upsert and
|
||||
its absence selects whole-sequence replacement. Inferred defaults do not become
|
||||
persistent instructions. The inheriting destination chooses its operation; copying
|
||||
a parent collection does not automatically copy its mode to that destination.
|
||||
|
||||
Track YAML identity and explicit modes according to their separate lifetimes.
|
||||
Actual whole-value replacement discards lower YAML identity while preserving
|
||||
application constraints and any new identity declaration. Item-local policies
|
||||
belong to their records: retaining a record retains its rules, and discarding it
|
||||
discards those rules. Policies independently applicable at a destination remain
|
||||
in effect. Inheritance can copy YAML identity subject to agreement and replacement
|
||||
rules; application registrations continue to constrain their registered paths.
|
||||
Numeric positions alone do not establish policy ownership.
|
||||
|
||||
Allow declarations without accompanying records. A key declaration validates a
|
||||
retained sequence immediately or remains pending in its owning scope when no
|
||||
sequence exists. A mode-only declaration affects later combinations without
|
||||
recombining earlier tiers. Neither declaration alone clears a collection, and
|
||||
declaring replace alone does not authorize changing a retained collection's
|
||||
identity. These rules do not independently impose path-existence or shape
|
||||
requirements.
|
||||
|
||||
Enforce YAML identity during source composition, precedence, and identified
|
||||
inheritance operations. Application registrations additionally constrain final
|
||||
effective sequences after defaults, CLI overrides, and application resolution.
|
||||
The application schema and validation own existence, nullability, and allowed
|
||||
shape. The final identity guarantee covers actual sequences on successful load;
|
||||
it does not cover every intermediate callback state or later caller mutations.
|
||||
YAML-only declarations carry no such final guarantee through defaults, CLI
|
||||
assignments, or application resolution.
|
||||
|
||||
Preserve source attribution, presence, history, sensitivity, and resource context
|
||||
as records survive, move, or are replaced. Identity diagnostics use paths and
|
||||
available origin information without exposing identity values. The guarantee
|
||||
requires no new mutation-history system or exact reconstruction of causes for
|
||||
records supplied by defaults or callbacks. Exact identity equality, declaration
|
||||
syntax, presence cases, and operation tables belong in the
|
||||
[configuration contract](../config.md#strict-document-composition).
|
||||
|
||||
## Ownership And Confidentiality
|
||||
|
||||
Returned configuration and metadata must not share mutable state across loads.
|
||||
@@ -241,7 +410,7 @@ for safe messages supplied by their hooks.
|
||||
Do not introduce automatic directory discovery, network providers, arbitrary
|
||||
merge strategies, additional configuration languages, or application runtime
|
||||
management without concrete consumer evidence and an explicit design decision.
|
||||
The [deferred scope](../roadmap/deferred.md#explicit-non-goals-and-deferred-scope)
|
||||
owns the deferred-feature inventory. Significant boundary
|
||||
changes require an ADR and corresponding policy/contract updates; implementation
|
||||
convenience alone is not a reason to broaden the public API.
|
||||
Record significant decisions to exclude capabilities, and their rationale, in
|
||||
ADRs. Significant boundary changes require an ADR and corresponding
|
||||
policy/contract updates; implementation convenience alone is not a reason to
|
||||
broaden the public API.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Base Policy Version:** 1.11
|
||||
|
||||
**Last Updated**: 2026-09-28
|
||||
**Last Updated**: 2026-10-16
|
||||
|
||||
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/documentation.md)
|
||||
|
||||
@@ -38,9 +38,10 @@ roadmap owns implementation status and sequencing until the decision is
|
||||
implemented. Current architecture, user, operator, integration, and internal
|
||||
documentation are updated when the behavior lands.
|
||||
|
||||
`docs/roadmap/deferred.md` owns deliberate exclusions; task-specific plans
|
||||
own execution order and progress. Completed plans retire during release
|
||||
preparation.
|
||||
`docs/roadmap/future.md` owns potential enhancements and their open questions;
|
||||
task-specific plans own execution order and progress. Record significant
|
||||
architectural exclusions and their rationale in ADRs. Do not maintain a roadmap
|
||||
inventory of rejected proposals. Completed plans retire during release preparation.
|
||||
|
||||
### Audience And Detail
|
||||
|
||||
@@ -91,7 +92,7 @@ secret values.
|
||||
| Inventory of implemented internal packages | `docs/internal/overview.md` | Implemented packages and components, responsibilities, and links to focused internal docs. | Normative architecture, contributor reading policy, external contracts. |
|
||||
| Behavior and architecture of internal packages | Other files under `docs/internal/` | Implementation flow, collaborators, state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, configuration definitions and defaults, external schemas, operator procedures. |
|
||||
| Architectural decision records | `docs/adr/` | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, task sequencing. |
|
||||
| Deferred scope | `docs/roadmap/deferred.md` | Deliberate exclusions and conditions for reconsideration. | Task execution and current behavior. |
|
||||
| Potential future features | `docs/roadmap/future.md` | Candidate enhancements and open questions for future consideration. | Rejected proposals, architectural decisions, task execution, and current behavior. |
|
||||
| Future implementation plans | Task-specific files under `docs/roadmap/` | Unfinished work, sequencing, acceptance, and progress; completed plans retire during release preparation. | Implemented behavior and ADR rationale. |
|
||||
| Release-note history | `docs/releases/` | One checked-in summary per source release under the procedure; the note at its immutable tag is the release record. | Current commands, behavior, contracts, and compatibility definitions. |
|
||||
| Complete examples | `examples/` and executable Go examples | Maintained copyable artifacts and Go API examples. | Configuration reference, symbol contracts, and prose explanation. |
|
||||
@@ -156,8 +157,10 @@ Use sequentially numbered ADR filenames such as
|
||||
|
||||
Treat the decision content of an accepted ADR as immutable. When a decision
|
||||
changes, create a new ADR and update the earlier ADR's status to superseded.
|
||||
Rejected architectural alternatives belong in the ADR; rejected product ideas
|
||||
belong in the roadmap. Routine choices need no ADR.
|
||||
Rejected architectural alternatives and significant architectural exclusions
|
||||
belong in ADRs. Roadmaps preserve plausible future work, not rejected product
|
||||
ideas. Routine choices and rejected suggestions need no ADR unless they
|
||||
represent a significant architectural decision.
|
||||
|
||||
## Maintenance
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
ConfigKit's selected grant is LGPL-3.0-or-later, recorded in
|
||||
[COPYRIGHT](../../COPYRIGHT), with the complete LGPLv3 and incorporated GPLv3
|
||||
texts in [LICENSE](../../LICENSE). The [release procedure](../release.md)
|
||||
owns source-only Go module publication through Gitea. No separate hosted release
|
||||
object or license asset is required for this channel.
|
||||
owns source-only Go module publication and, beginning with v1.0.0, local delivery
|
||||
of a Gitea release page using the complete committed release note. That page
|
||||
retains the note's licensing footer; separate license assets are not required.
|
||||
|
||||
## Purpose And Scope
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ policy and link to it rather than introduce additional release requirements.
|
||||
|
||||
ConfigKit publishes a source-only Go module. Its declared deliverables are the
|
||||
immutable annotated source tag and version-matched checked-in release note.
|
||||
Standalone binaries, container images, hosted release objects, and separate
|
||||
registry publication are not required. Additional channels require a deliberate
|
||||
update to the policy and procedure.
|
||||
Beginning with v1.0.0, maintainers also deliver a Gitea release page containing
|
||||
that complete note after source publication. Standalone binaries, container
|
||||
images, and separate registry publication are not required. Further channels
|
||||
require a deliberate update to the policy and procedure.
|
||||
|
||||
The Go API, documented YAML and resolution semantics, and structured diagnostic
|
||||
contracts participate in compatibility review. A signature-preserving change
|
||||
@@ -80,9 +81,13 @@ the tagged source. The procedure must identify those deliverables and the
|
||||
responsibility of their automation. Published deliverables must be traceable
|
||||
to that source and identify the same version.
|
||||
|
||||
ConfigKit currently declares no additional deliverables. Confirmed publication
|
||||
of the intended source tag is the completion point. CI, macOS validation,
|
||||
hosted delivery, and consumer retrieval are outside the release gates.
|
||||
ConfigKit's hosted release page is delivered locally through ReleaseKit after
|
||||
confirmed source publication, using the tagged note and preserving its licensing
|
||||
footer. Hosted delivery has a separately reported outcome and may be retried
|
||||
against the same immutable tag without republishing it. Confirmed publication
|
||||
of the intended source tag remains the source-release completion point. CI,
|
||||
macOS validation, hosted-service availability, delivery credentials, and consumer
|
||||
retrieval are outside the source-publication gates.
|
||||
|
||||
## Versioning And Compatibility
|
||||
|
||||
@@ -176,8 +181,8 @@ ConfigKit uses the complete checked-in note as the annotated tag message.
|
||||
Include the release footer defined by the [OSS policy](oss.md#discoverability)
|
||||
in each new release document. The release procedure defines the concrete URLs
|
||||
and publication mechanics. Prepare these links with the release note; they do
|
||||
not add a network check or approval gate. If a hosted download page is used in
|
||||
a future channel, preserve the footer there.
|
||||
not add a network check or approval gate. ConfigKit's hosted release page reuses
|
||||
the complete committed note and its footer.
|
||||
|
||||
Handle version selection, release-note content, and retirement of completed
|
||||
roadmaps during preparation. Preserve planned and deferred work and repair
|
||||
@@ -221,6 +226,11 @@ expanded platform matrices solely because they are possible. Retain such
|
||||
checks when their distinct protection justifies their cost. A one-time migration
|
||||
check must not silently become a permanent release requirement.
|
||||
|
||||
ConfigKit selects ReleaseKit's targeted forbidden-file check to keep the local
|
||||
delivery token path out of the committed candidate. The profile owns the path;
|
||||
the check does not inspect file contents or Git history. Hosted credential
|
||||
preflight belongs to delivery and adds no source-publication gate.
|
||||
|
||||
ReleaseKit runs its selected Go commands with readonly module mode and
|
||||
`GOPROXY=off`; acquire dependencies and build the tool before candidate
|
||||
validation. Formatting and whitespace remain ordinary development checks in
|
||||
|
||||
189
docs/release.md
189
docs/release.md
@@ -1,16 +1,25 @@
|
||||
# ConfigKit Release Procedure
|
||||
|
||||
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
|
||||
ConfigKit publishes a source-only Go module. The immutable annotated tag and
|
||||
matching checked-in release note are the deliverables; there is no binary,
|
||||
container image, generated package, or separate registry publication.
|
||||
A release publishes the Go source under a new Git tag, with a matching release
|
||||
note committed in the repository. The tag is annotated: it includes the release
|
||||
note text and points to the released commit. Published tags and contents are
|
||||
immutable. There is no binary, container image, generated package, or separate
|
||||
registry publication. Beginning with v1.0.0, maintainers also deliver a Gitea
|
||||
release page containing the complete checked-in note after source publication.
|
||||
Source publication and hosted delivery have separate outcomes.
|
||||
|
||||
A **candidate** is the committed revision you intend to release. The
|
||||
**authoritative remote** is the repository where ConfigKit publishes releases;
|
||||
its exact push URL is listed below. The **consumer minimum** is the oldest Go
|
||||
version supported for applications using ConfigKit.
|
||||
|
||||
## Release identity and support
|
||||
|
||||
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
|
||||
- Module: `go.maximumdirect.net/configkit`
|
||||
- Primary branch: `main`
|
||||
- Authoritative remote: `origin`
|
||||
- Authoritative push URL: `https://gitea.maximumdirect.net/eric/ConfigKit.git`
|
||||
- Authoritative push URL: `ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git`
|
||||
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
|
||||
- Minimum Go version: the first stable patch of the selected Go series, or a
|
||||
higher patch when required by a dependency or specific correctness fix, as
|
||||
@@ -20,10 +29,15 @@ container image, generated package, or separate registry publication.
|
||||
- Best-effort targets: macOS amd64 and arm64, under the
|
||||
[platform support policy](policy/release.md#platform-support)
|
||||
|
||||
Release validation runs one offline race-enabled Go suite, one full vet pass,
|
||||
and CGO-disabled package compilation for both Linux targets. macOS and CI are
|
||||
Release validation runs the offline Go test suite once with the race detector,
|
||||
then a full vet pass and package compilation for both Linux targets with cgo
|
||||
disabled (`CGO_ENABLED=0`). macOS and CI are
|
||||
not release gates; do not wait for or poll CI.
|
||||
|
||||
The vanity module path is the name applications put in Go imports. Source/tag
|
||||
browser links and public Go retrieval use the Forge HTTPS repository
|
||||
independently of SSH publication.
|
||||
|
||||
## 1. Select Go and prepare the tool
|
||||
|
||||
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
|
||||
@@ -33,10 +47,10 @@ For a patch release, retain the series recorded in `go.mod` and leave its consum
|
||||
minimum unchanged. Follow the
|
||||
[toolchain selection policy](policy/release.md#configkit-toolchain-selection).
|
||||
|
||||
Put the selected series' latest locally available compatible patch first on
|
||||
`PATH`, ensuring it meets `go.mod`'s minimum. The same `go` executable must build
|
||||
ReleaseKit and run its child validation commands. Set local toolchain selection
|
||||
and verify it:
|
||||
Choose the latest compatible patch installed locally in that Go series. Put
|
||||
its `go` executable first on `PATH` and ensure it meets the minimum in
|
||||
`go.mod`. The same `go` executable must build ReleaseKit and run its child
|
||||
validation commands. Set local toolchain selection and verify it:
|
||||
|
||||
```sh
|
||||
export GOTOOLCHAIN=local
|
||||
@@ -61,8 +75,17 @@ use that required version instead and document the reason. When the series is
|
||||
unchanged, leave the minimum unchanged unless a minor or major release
|
||||
deliberately changes it under the
|
||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||
For example, validating with Go 1.26.8 leaves `go 1.26.0` unchanged. There is no
|
||||
separate requirement to validate with Go 1.26.0.
|
||||
For example:
|
||||
|
||||
| Release situation | Go used for validation | Consumer minimum |
|
||||
| --- | --- | --- |
|
||||
| Patch release in the 1.26 series | Latest compatible patch installed locally, such as 1.26.8 | Keep the existing `go.mod` minimum unchanged. |
|
||||
| Minor/major release staying in 1.26 | Latest compatible patch installed locally | Normally retain the minimum; any deliberate change follows the linked policy. |
|
||||
| Minor/major release adopting a newer installed stable series | Latest compatible patch installed locally in that series | Normally declare that series' initial stable patch, or the higher patch required by a dependency or identified fix. |
|
||||
|
||||
Using Go 1.26.8 for validation can therefore leave `go 1.26.0` unchanged. Users
|
||||
need the declared minimum, while maintainers validate with their selected patch.
|
||||
There is no separate requirement to validate with Go 1.26.0.
|
||||
|
||||
Prepare dependencies whether or not the minimum changes:
|
||||
|
||||
@@ -70,28 +93,39 @@ Prepare dependencies whether or not the minimum changes:
|
||||
go mod download
|
||||
```
|
||||
|
||||
Build the tool selected by `go.mod` into temporary storage outside the checkout.
|
||||
Build the ReleaseKit version recorded in `go.mod` into temporary storage outside
|
||||
the checkout.
|
||||
Ensure `$TMPDIR` names a system temporary location, not the repository. These
|
||||
commands never install another ReleaseKit version:
|
||||
|
||||
```sh
|
||||
releasekit_tmp=$(mktemp -d "${TMPDIR:-/tmp}/configkit-releasekit.XXXXXX")
|
||||
releasekit_bin="$releasekit_tmp/releasekit"
|
||||
go build -o "$releasekit_bin" gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
|
||||
go list -m gitea.maximumdirect.net/eric/ReleaseKit
|
||||
go build -o "$releasekit_bin" go.maximumdirect.net/releasekit/cmd/releasekit
|
||||
go list -m go.maximumdirect.net/releasekit
|
||||
"$releasekit_bin" version
|
||||
```
|
||||
|
||||
The module-selected executable is ReleaseKit v0.4.1. Use the built executable
|
||||
for publication to preserve its exit status and publication-state diagnostics.
|
||||
This repository selects ReleaseKit v0.6.0. Use the compiled program above for
|
||||
publication so its exit status and publication report reach you unchanged.
|
||||
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
|
||||
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
|
||||
executable first on `PATH` through the publication command.
|
||||
|
||||
For hosted delivery, save the Gitea API token in the ignored local file
|
||||
`.releasekit/gitea-token` with owner-only permissions. The profile uses it when
|
||||
`RELEASEKIT_GITEA_TOKEN` is unset or blank; a nonblank environment value takes
|
||||
precedence. The file path resolves from the repository root, and ReleaseKit
|
||||
trims surrounding whitespace from its contents. Keep credentials out of release
|
||||
notes, diagnostic labels, command arguments, and commits. The selected
|
||||
`forbidden_files` check also rejects a committed token at that exact path;
|
||||
it does not scan other files or history. Source publication uses Git
|
||||
authentication and does not read or require this token.
|
||||
|
||||
Verify access to the authoritative remote without displaying credentials:
|
||||
|
||||
```sh
|
||||
git ls-remote https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD refs/heads/main
|
||||
git ls-remote ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD refs/heads/main
|
||||
```
|
||||
|
||||
Publication requires explicit authorization. Local implementation commits do
|
||||
@@ -99,10 +133,10 @@ not authorize a push or release.
|
||||
|
||||
## 2. Prepare and commit the candidate
|
||||
|
||||
Choose an unused stable version locally and on the authoritative remote. Compare
|
||||
the candidate with the previous tag and review exported Go signatures and
|
||||
ownership, YAML and resolution semantics, diagnostic contracts, minimum Go
|
||||
version, and platform support. Review maintained consumer examples and the
|
||||
Choose a stable version that has not been used locally or on the publication
|
||||
remote. Compare the candidate with the previous tag. Review public Go APIs and
|
||||
value ownership, YAML and resolution rules, diagnostic contracts, the minimum
|
||||
Go version, and platform support. Review maintained consumer examples and the
|
||||
migration guidance. Explain any consumer-minimum increase and required consumer
|
||||
action under the
|
||||
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||
@@ -111,7 +145,7 @@ Create `docs/releases/<version>.md`. Its first line must be exactly
|
||||
`# ConfigKit <version>`, and it must end with the license and attribution footer:
|
||||
|
||||
```markdown
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/VERSION/docs/oss/attribution.md)
|
||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/configkit/src/tag/VERSION/docs/oss/attribution.md)
|
||||
```
|
||||
|
||||
Replace `VERSION` with the full tag and confirm the three target files exist in
|
||||
@@ -127,7 +161,7 @@ Push only that committed candidate to the literal authoritative URL. This does
|
||||
not follow local tags:
|
||||
|
||||
```sh
|
||||
git push --no-follow-tags https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD:refs/heads/main
|
||||
git push --no-follow-tags ssh://git@forge.maximumdirect.net:22222/base-layer/configkit.git HEAD:refs/heads/main
|
||||
```
|
||||
|
||||
ReleaseKit requires authoritative `main` to equal this candidate before it
|
||||
@@ -139,46 +173,114 @@ candidate to `main`, and invoke validation again.
|
||||
|
||||
## 4. Publish the committed candidate
|
||||
|
||||
With the authorized candidate already on authoritative `main`, invoke the
|
||||
module-selected native executable exactly once:
|
||||
Once publication is authorized and the candidate is on the publication remote's
|
||||
`main`, run the compiled ReleaseKit program once to validate and publish it:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
Replace the placeholder with the selected version. ReleaseKit checks the
|
||||
effective push URL against `expected_push_url`, verifies the candidate on
|
||||
push URL Git will actually use, after URL rewrites, against `expected_push_url`.
|
||||
It verifies the candidate on
|
||||
`main`, and checks the release-note H1 for the product and version. It then
|
||||
runs the selected race-enabled standard suite once with test-time vet disabled,
|
||||
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
|
||||
arm64. It publishes only the annotated tag for that candidate. Confirmed
|
||||
publication of the intended tag completes the release. The tag points to the
|
||||
captured commit and uses the complete checked-in note as its annotation message.
|
||||
The optional `check`
|
||||
command is available for diagnosis but is not a separate required pass:
|
||||
Use the optional `check` command for diagnosis when useful. It validates without
|
||||
publishing and is not required before `publish`, which performs its own checks:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" check vMAJOR.MINOR.PATCH
|
||||
```
|
||||
|
||||
Status 0 confirms publication, status 1 reports a known publication failure or
|
||||
conflict, status 2 reports usage, profile, or version errors, and status 3 means
|
||||
the push outcome remains unknown. Preserve both the native exit status and its
|
||||
diagnostics.
|
||||
For `publish`, interpret the program's exit status as follows:
|
||||
|
||||
| Status | Meaning | Next action |
|
||||
| --- | --- | --- |
|
||||
| `0` | Publication succeeded. | The source release is complete. Continue with hosted delivery. |
|
||||
| `1` | A known publication failure or conflict was reported. | Read the report and correct the stated problem. Preserve any published tag. |
|
||||
| `2` | The command, profile, or version is invalid. | Correct the command or configuration before trying again. |
|
||||
| `3` | The tag push outcome is still unknown. | Follow the recovery procedure below before retrying or declaring success. |
|
||||
|
||||
Keep both the program's exit status and its diagnostic report. Using `go run`
|
||||
would lose the distinction between its nonzero exit statuses.
|
||||
|
||||
The command does not push or repair `main`, wait for CI, or publish binaries or
|
||||
a hosted release page. A successful intended-tag publication does not require
|
||||
remote read-back or consumer retrieval; those checks remain optional.
|
||||
|
||||
## 5. Recover from a failed or uncertain tag push
|
||||
## 5. Deliver the hosted release page
|
||||
|
||||
After source publication succeeds, run local hosted delivery from the repository
|
||||
root. Keep the worktree clean and `HEAD` at the published tag's commit. Reuse the
|
||||
compiled executable from step 1. For the first release using this workflow:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" deliver v1.0.0
|
||||
```
|
||||
|
||||
For subsequent releases, substitute the selected version. The profile selects
|
||||
Gitea at `https://forge.maximumdirect.net`, owner `base-layer`, repository
|
||||
`configkit`. With no build targets, delivery creates or accepts a stable,
|
||||
non-draft release page containing the complete committed note, including its
|
||||
licensing footer. It creates no binary assets, receipts, or checksum manifest.
|
||||
An existing page with matching content is accepted; conflicting content is not
|
||||
overwritten.
|
||||
|
||||
Delivery automatically performs an authenticated, read-only repository preflight
|
||||
before hosted writes. A separate preflight before source publication is not a
|
||||
release gate. Delivery does not rerun candidate validation, publish a tag, or
|
||||
wait for CI. Hosted-service availability and delivery credentials do not gate
|
||||
source publication. Report source-publication success and hosted-delivery status
|
||||
separately.
|
||||
|
||||
| Delivery status | Meaning and next action |
|
||||
| --- | --- |
|
||||
| `0` | Delivery is complete; report the hosted release URL. |
|
||||
| `1` | Preflight was unavailable, or delivery failed, conflicted, or made partial progress. Read the reported state and correct the cause; investigate conflicts before retrying. |
|
||||
| `2` | Usage, profile, credential loading, or preflight configuration/access failed. Correct the reported problem before retrying delivery. |
|
||||
| `3` | A hosted write has an unresolved outcome. Inspect the hosted release and resolve that state before retrying. |
|
||||
|
||||
A delivery problem leaves the successfully published source tag intact. Retry
|
||||
delivery with the same immutable version after correcting the cause and
|
||||
resolving any uncertain write. For a later retry, use a clean checkout of the
|
||||
original tag, fetching that tag if needed. Build the module-selected CLI from
|
||||
that checkout using step 1's tool-build commands, without changing the tagged
|
||||
module files, and make the token available there. For example, with the tag
|
||||
available locally:
|
||||
|
||||
```sh
|
||||
release_version=v1.0.0
|
||||
git switch --detach "$release_version"
|
||||
```
|
||||
|
||||
After preparing the tool in that tagged checkout:
|
||||
|
||||
```sh
|
||||
"$releasekit_bin" deliver "$release_version"
|
||||
```
|
||||
|
||||
Do not rerun `publish`, move a tag, or replace a hosted page to repair delivery.
|
||||
If published source or content needs correction, prepare a new appropriate
|
||||
version through the normal release procedure.
|
||||
|
||||
## 6. Recover from a failed or uncertain tag push
|
||||
|
||||
The executable reports the publication state, phase, version, candidate commit,
|
||||
tag-object ID, effective endpoint, and error on standard error. It performs one
|
||||
bounded authoritative reconciliation after a failed tag push. Exit status 3
|
||||
tag-object ID, effective endpoint, and error on standard error. After a failed
|
||||
tag push, it makes one bounded check of the exact publication remote to find out
|
||||
whether the tag arrived. Exit status 3
|
||||
means the tag outcome remains unknown; do not blindly retry or move a tag.
|
||||
|
||||
For an uncertain result, use the exact endpoint, candidate commit, version, and
|
||||
tag-object ID from the report. Copy the captured endpoint into the variable
|
||||
An annotated tag is a Git object containing the release note and a pointer to
|
||||
the source commit. Its tag-object ID identifies that complete object. Checking
|
||||
only the commit ID would not establish that the intended tag object arrived.
|
||||
|
||||
For an uncertain result, use the exact endpoint (remote URL), candidate commit,
|
||||
version, and tag-object ID from the report. Copy the captured endpoint into the variable
|
||||
below using normal shell quoting. Check that Git still resolves it to itself,
|
||||
then query the authoritative refs once:
|
||||
|
||||
@@ -195,7 +297,8 @@ cannot establish absence at the captured endpoint.
|
||||
|
||||
Compare the `main` object ID with the reported candidate commit and the tag
|
||||
object ID with the reported tag-object ID. `--refs` returns the annotated tag
|
||||
object itself, not merely its peeled commit. If the tag object matches, the
|
||||
object itself. A peeled commit is the source commit reached by following that
|
||||
tag; it is a different identity. If the tag object matches, the
|
||||
release is published. If the query fails, the result is conflicting, or the
|
||||
endpoint or candidate differs, stop and resolve the state before retrying. If
|
||||
the endpoint is reachable, the tag is confirmed absent, and `main` still equals
|
||||
@@ -214,7 +317,7 @@ back `main`. A conflicting published tag must never be moved or reused. A
|
||||
defect in published source requires a corrected commit, a new semantically
|
||||
appropriate version, and a matching release note.
|
||||
|
||||
## 6. Optional consumer verification
|
||||
## 7. Optional consumer verification
|
||||
|
||||
When useful or requested, read back the remote tag or retrieve the exact
|
||||
version into a new module without a local `replace` directive. These checks are
|
||||
@@ -228,8 +331,8 @@ consumer_dir=$(mktemp -d)
|
||||
(
|
||||
cd "$consumer_dir"
|
||||
go mod init example.invalid/configkit-release-check
|
||||
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
|
||||
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
|
||||
GOWORK=off GOPROXY=direct go get "go.maximumdirect.net/configkit@$release_version"
|
||||
GOWORK=off go list -m -f '{{.Path}} {{.Version}}' go.maximumdirect.net/configkit
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
77
docs/releases/v0.10.0.md
Normal file
77
docs/releases/v0.10.0.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# ConfigKit v0.10.0
|
||||
|
||||
This minor release adds explicit, composable discovery of a configuration file.
|
||||
Applications choose the search conventions and their order; ConfigKit selects one
|
||||
file during loading and returns the final configuration with its normal metadata.
|
||||
Optional searches that find no file share the expanded `NoSource()` lifecycle.
|
||||
|
||||
## Changes
|
||||
|
||||
- Add `SearchConfig`, immutable `Search` and `FilenameGroup` descriptions, and
|
||||
search-wide requiredness through `SearchOptions`. Compose explicit `Paths`,
|
||||
custom `SearchDirectory` descriptions, and built-in Unix system, user-config,
|
||||
home, or working-directory presets with `Searches`. See the
|
||||
[public declarations](../../discovery.go) and
|
||||
[discovery contract](../config.md#configuration-discovery).
|
||||
- Expand searches in supplied order, trying a directory's filename groups before
|
||||
moving to the next directory. Named and generic groups provide conventional
|
||||
YAML filenames; custom groups provide literal names. Directory searches
|
||||
distinguish omitted filename selection from an explicitly empty selection.
|
||||
Constructors perform no filesystem access, and each load observes current
|
||||
directory bases and file availability.
|
||||
- Select the first existing candidate. Access errors and failures while loading
|
||||
the selected file terminate the load rather than trying another configuration.
|
||||
Selected files retain physical source identity, placement, provenance, imports,
|
||||
and relative-resource context. Discovery works with ordinary loading and
|
||||
layered inputs, including underlays, without adding a precedence tier.
|
||||
- Permit enabled imports with an absent main input: there are no main import
|
||||
declarations to traverse, while actual underlay sources retain normal imports.
|
||||
Support ordinary selected overlays with an absent main and explicit filesystem
|
||||
context, including a root-only host context. Layered overlays retain their
|
||||
independent `OverlayFilesystem` requirement. See
|
||||
[single-source loading](../config.md#single-source-loading) and
|
||||
[layered loading](../config.md#layered-loading).
|
||||
- Preserve ordinary file-context behavior after discovery and include the
|
||||
failing candidate's file identity in access diagnostics. Use Go's
|
||||
platform-aware Windows component rules and native drive-root-relative path
|
||||
resolution. Isolate user-config tests according to the host Go conventions.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
Existing explicit source constructors and loading APIs remain available.
|
||||
`File(path)` remains an exclusive selection that fails for a missing file;
|
||||
discovery is activated only by explicitly supplying `SearchConfig`.
|
||||
|
||||
Searches are optional by default. Set `SearchOptions{Required: true}` when the
|
||||
application must find a file. An exhausted optional search contributes no main
|
||||
document or source record; application version checks, defaults, resolution,
|
||||
and validation still run under their existing rules and may reject incomplete
|
||||
configuration. A selected overlay is still required to exist.
|
||||
|
||||
The former `NoSource()` rejections of enabled imports and explicit filesystem
|
||||
context are relaxed. An active ordinary overlay with no main file now loads
|
||||
when explicit context is supplied. Applications that relied on those combinations
|
||||
being rejected should review their option handling. Selected host files derive
|
||||
their own base directory; do not supply an explicit `BaseDir` or native filesystem
|
||||
context for a search that selects a host file.
|
||||
|
||||
Applications may replace their own fallback-loading loops with the new search
|
||||
descriptions. The maintained [consumer examples](../consumers/README.md#explicit-main-file-discovery)
|
||||
show simple preset selection and custom-first discovery alongside existing
|
||||
configuration tiers. Composition, collection operations, inheritance, CLI
|
||||
precedence, and the complete-on-return ownership model retain their existing
|
||||
semantics.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.0
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. Release validation uses Go 1.26.10.
|
||||
The YAML dependency remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`, and the maintainer
|
||||
tool remains ReleaseKit v0.4.1. Linux amd64 and arm64 remain release-validated
|
||||
targets; macOS remains best-effort. Publication follows the existing
|
||||
[release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.0/docs/oss/attribution.md)
|
||||
42
docs/releases/v0.10.1.md
Normal file
42
docs/releases/v0.10.1.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# ConfigKit v0.10.1
|
||||
|
||||
This patch release consolidates ConfigKit's internal loading machinery and
|
||||
reduces repeated import bookkeeping. Public APIs and configuration semantics
|
||||
remain unchanged from v0.10.0.
|
||||
|
||||
## Changes
|
||||
|
||||
- Consolidate ordinary and layered input acquisition, including import requests
|
||||
and selected-overlay handling, while retaining their distinct context and
|
||||
precedence rules. Remove unused private reverse conversions.
|
||||
- Separate import acquisition, scoped composition, and evidence projection;
|
||||
clarify inheritance preparation, combination, and finalization; and simplify
|
||||
reflective projection and schema-field assembly responsibilities.
|
||||
- Retain resolved filesystem facts from non-import validation for later passive
|
||||
resource contexts and ordinary overlays. Remove duplicate absent-host context
|
||||
derivation while preserving validation timing and handle cleanup.
|
||||
- Record empty optional-import selections directly against their declaring
|
||||
source, avoiding repeated searches through acquired sources. Index import
|
||||
dependencies for scope-local edge preparation instead of scanning the whole
|
||||
inclusion graph for every item scope. Stable dependency-ready ordering,
|
||||
provenance, and source destinations are preserved.
|
||||
- Expand regression coverage for import evidence ownership, scope boundaries,
|
||||
and broad main/underlay item imports. Improve
|
||||
[consumer loading guidance](../consumers/README.md) and internal documentation.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
No consumer code or configuration migration is required. Composition, collection
|
||||
operations, inheritance, discovery, CLI precedence, source-version checks, and
|
||||
inspection retain their existing contracts.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.1
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. Dependencies and platform support are
|
||||
unchanged. Publication follows the existing [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.1/docs/oss/attribution.md)
|
||||
70
docs/releases/v0.10.2.md
Normal file
70
docs/releases/v0.10.2.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# ConfigKit v0.10.2
|
||||
|
||||
This patch release fixes edge-case loading failures, resource cleanup, and limit
|
||||
accounting, and simplifies ConfigKit's internal loading machinery. Public APIs
|
||||
and documented configuration semantics remain unchanged from v0.10.1.
|
||||
|
||||
## Fixes
|
||||
|
||||
- Terminate pointer-only recursive type traversal safely during decoding and
|
||||
schema-path resolution, preserving nullable values and supported recursive
|
||||
structures. Roll back failed dynamic descriptor construction so subsequent
|
||||
operations do not reuse incomplete state or panic.
|
||||
- Accept finite, non-cyclic slice views that share backing storage while
|
||||
retaining cycle detection and detached output.
|
||||
- Deactivate resolution, validation, and version-check contexts when callbacks
|
||||
panic, and close library-owned import roots during panic unwinding. The
|
||||
original application panic continues to propagate.
|
||||
- Report directory enumeration and close failures instead of treating them as
|
||||
optional absence, including failures reported as not-exist after a successful
|
||||
open. Genuinely missing optional directories retain their existing behavior.
|
||||
- Reuse completed symlink dependencies during host and native import discovery,
|
||||
avoiding exponential repeated expansion of finite link graphs while retaining
|
||||
cycle detection and containment checks.
|
||||
- Check existing projection budgets before bulk container allocation and account
|
||||
for the complete retained inspection-history graph, including replay and
|
||||
separately copied inheritance evidence.
|
||||
- Apply the documented append/upsert sequence requirements to non-sequence
|
||||
contributions at retained collection-policy destinations.
|
||||
|
||||
## Internal improvements
|
||||
|
||||
- Index collection-policy retirement and prepare sparse incoming policy changes
|
||||
instead of repeatedly scanning or copying unrelated retained policy.
|
||||
- Reuse successful dynamic-type eligibility checks within an operation and index
|
||||
tier-history lookups, compound identity restoration, and secret-failure source
|
||||
attribution to avoid repeated scans.
|
||||
- Remove unused snapshots, evidence, helpers, fields, and test-only production
|
||||
paths. Clarify acquisition handoffs and separate collection preparation,
|
||||
application, and finalization while sharing sequence-history construction.
|
||||
- Separate patterned glob entry preparation from traversal. Consolidate engine
|
||||
import defaults, snapshot them during load preflight, and construct placement
|
||||
directly across the facade while keeping public types root-owned.
|
||||
- Add regression coverage for the corrected failures, public historical paths,
|
||||
defaults snapshots, and resource ownership. Strengthen fuzz detachment checks
|
||||
and replace redundant or implementation-coupled assertions.
|
||||
- Update release tooling from ReleaseKit v0.4.1 to v0.4.2.
|
||||
|
||||
These changes remove redundant work and improve scaling in the identified paths;
|
||||
no end-to-end performance improvement has been quantified.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
No consumer API or configuration-format migration is required. The
|
||||
[configuration contract](../config.md) remains authoritative.
|
||||
|
||||
Loads that previously succeeded despite directory acquisition failures, invalid
|
||||
append/upsert contributions, or incomplete retained-history accounting may now
|
||||
return the documented structured errors. These corrections enforce existing
|
||||
requirements and limits rather than introducing new ones.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.10.2
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. The YAML dependency and platform support
|
||||
are unchanged. Publication follows the existing [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.10.2/docs/oss/attribution.md)
|
||||
@@ -30,7 +30,7 @@ adopts ReleaseKit for source publication, and raises the consumer Go minimum to
|
||||
successful-tag-push completion point remain unchanged; CI is not a gate.
|
||||
- Adopt release policy 1.6, distinguishing the consumer Go minimum from the
|
||||
validation toolchain. Retire completed planning and measurement records into
|
||||
Git history while preserving [deferred work](../roadmap/deferred.md).
|
||||
Git history while preserving [performance candidates](../roadmap/future.md#3-performance-experiments).
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
|
||||
70
docs/releases/v0.8.0.md
Normal file
70
docs/releases/v0.8.0.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# ConfigKit v0.8.0
|
||||
|
||||
This minor release adds explicit input placement, relative import destinations,
|
||||
shared source-version checking, and opt-in composition of identified sequence
|
||||
records. Applications can assemble independently authored configuration fragments
|
||||
into one final configuration while retaining source locations, provenance,
|
||||
history, and sensitivity information.
|
||||
|
||||
## Changes
|
||||
|
||||
- Add `Destination` to ordinary load options and layered inputs. A source mapping
|
||||
can contribute beneath literal application keys without being rewritten by the
|
||||
consumer. Imports inherit that destination or append a relative YAML
|
||||
`destination`. Physical import paths and resource contexts remain relative to
|
||||
their declaring sources. See [input destinations](../config.md#input-destinations),
|
||||
[import syntax](../config.md#explicit-imports), and the
|
||||
[mounted collection example](../consumers/README.md#input-destinations-and-mounted-collections).
|
||||
- Add `SourceVersionModeIfPresent` and `SourceVersionModeRequired` to apply one
|
||||
application-owned version checker across main, underlay, imported, and selected
|
||||
overlay documents. Enabled modes check source-local declarations and consume
|
||||
accepted non-main headers before composition. Detached, sensitivity-aware
|
||||
evidence is available through `SourceInfo.VersionCheck`. Acceptance remains
|
||||
application policy; ConfigKit does not require declarations to equal the main
|
||||
version. See [shared source-version checks](../config.md#shared-source-version-checks).
|
||||
- Add explicit `PlacementModeSequenceItem` placement for ordinary and layered
|
||||
inputs, and `placement: sequence-item` for path and glob imports. A source
|
||||
mapping becomes one item at its destination; ordinary helper imports compose
|
||||
within that item and can supply its identity. See
|
||||
[imports](../config.md#explicit-imports).
|
||||
- Add `Definition.SequenceCompositions` registrations for strict composition at
|
||||
selected literal paths using one string identity field. Distinct records append
|
||||
in contribution order, while duplicate identities conflict. Higher-priority
|
||||
sequences still replace lower-priority sequences wholesale. Registration does
|
||||
not add identity-based overlays or final identity validation after defaults and
|
||||
application resolution. See the [composition contract](../config.md#strict-document-composition)
|
||||
and [maintained prompt catalog](../consumers/README.md#identified-prompt-sequences).
|
||||
- Extend source inspection with destination and placement evidence, including
|
||||
whether a nested destination is unresolved in an intermediate or failed-load
|
||||
snapshot. Preserve source-specific history, consumed-header sensitivity, and
|
||||
explanations of descendants removed by sequence replacement as records are
|
||||
assembled. Source locations continue to identify authored coordinates.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
The new behavior is opt-in: zero-valued destinations retain root placement,
|
||||
mapping placement remains the default, unregistered sequences retain strict
|
||||
conflict behavior, and the default source-version mode preserves the existing
|
||||
main-only version policy. Recursive mapping precedence, whole-sequence
|
||||
replacement between tiers, and application-owned final validation remain intact.
|
||||
|
||||
Public option, definition, input, and source-information structs gain fields.
|
||||
Consumers using unkeyed literals for affected structs must switch to keyed
|
||||
literals or update them. `SourceInfo` now contains a `Path` and is no longer
|
||||
comparable with `==` or usable directly as a map key; compare the relevant fields
|
||||
or select an application-appropriate key instead. Existing keyed literals need
|
||||
no changes to retain the previous loading behavior.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.8.0
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. The YAML dependency remains
|
||||
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, and the maintainer tool dependency remains
|
||||
ReleaseKit v0.4.1. Linux amd64 and arm64 remain release-validated targets;
|
||||
macOS remains best-effort. Publication follows the existing
|
||||
[release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.8.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.8.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.8.0/docs/oss/attribution.md)
|
||||
111
docs/releases/v0.9.0.md
Normal file
111
docs/releases/v0.9.0.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# ConfigKit v0.9.0
|
||||
|
||||
This minor release expands import selection, adds author-declared collection
|
||||
identity and operations, and supports inheritance between identified records.
|
||||
It also moves CLI assignments after inheritance and typed decoding. These changes
|
||||
include breaking API and configuration behavior changes from v0.8.0; review the
|
||||
migration guidance below before upgrading.
|
||||
|
||||
## Changes
|
||||
|
||||
- Extend imports with named-file and directory lists, directory filename patterns,
|
||||
exclusions, recursion and depth controls, and explicit hidden-file and symlink
|
||||
discovery controls. Add `ImportDefaults` to ordinary and layered options, with
|
||||
authored declarations taking precedence over application defaults. Imports are
|
||||
optional by default. `SourceInfo.ImportSelections` exposes completed optional
|
||||
declarations that selected no sources. See the
|
||||
[import contract](../config.md#explicit-imports) and
|
||||
[consumer guidance](../consumers/README.md#import-defaults-and-selection-inspection).
|
||||
- Use shared dependency-ready ordering for import composition and sibling
|
||||
inheritance. Dependencies complete before dependents, with deterministic
|
||||
selection among currently eligible contributions. Source acquisition and
|
||||
callback order remain separate from assembly order. See the
|
||||
[ordering decision](../adr/0015-shared-dependency-ready-ordering.md).
|
||||
- Add root `configkit.sequences` declarations for collection identity and
|
||||
`replace`, `append`, or `upsert` operations, including in selected overlays.
|
||||
Strict peers combine distinct identities and reject duplicates; precedence
|
||||
and inheritance apply the destination's collection operation. Upsert replaces
|
||||
a whole matching record at its retained position. Identity supports compound
|
||||
field sets and scalar strings, numbers, booleans, and null. See the
|
||||
[collection contract](../config.md#strict-document-composition).
|
||||
- Add `Definition.SequenceIdentities`, exact `SequenceIdentityAt` registrations,
|
||||
and dynamic `SequenceIdentityMatching` selectors. Application registrations
|
||||
govern structural collection operations and check existing matching final
|
||||
typed sequences, including values from defaults, CLI assignments, and
|
||||
application resolution. YAML-only declarations govern structural operations
|
||||
without adding that final-result check.
|
||||
- Support record inheritance within an identified sequence using an `extends`
|
||||
mapping containing the parent's complete identity. The child retains its own
|
||||
identity, relationships use surviving assembled records, and output sequence
|
||||
order is preserved. See [sibling inheritance](../config.md#sibling-inheritance)
|
||||
and the maintained [sequence catalog](../consumers/README.md#identified-prompt-sequences).
|
||||
- Apply explicit CLI overrides to the decoded configuration before application
|
||||
resolution. Mappings retain omitted fields and their presence metadata;
|
||||
sequences replace whole lists independently of collection operations. See the
|
||||
[CLI contract](../config.md#explicit-cli-overrides).
|
||||
- Correct native import alias and contained-symlink handling, source-version
|
||||
path projection, and provenance/history through collection movement,
|
||||
replacement, inheritance, and CLI assignment. Reduce repeated scans,
|
||||
accumulated policy copying, and sparse-projection storage during construction.
|
||||
|
||||
## Compatibility and upgrade
|
||||
|
||||
This pre-v1 minor release intentionally changes the v0.8.0 contracts:
|
||||
|
||||
1. **Migrate import requiredness.** The YAML `optional` field is removed and is
|
||||
rejected. Replace `optional: true` with `required: false`; omission is equivalent
|
||||
when the application does not default imports to required. Replace
|
||||
`optional: false` with `required: true`. Imports that previously relied on
|
||||
implicit requiredness now need `required: true`, or an application
|
||||
`ImportDefaults.Required` default set to true. Review discovery filters:
|
||||
`follow` and `hidden` default to true, and directory searches select all
|
||||
filenames unless a pattern is supplied.
|
||||
2. **Replace sequence registrations.** `SequenceComposition` and
|
||||
`Definition.SequenceCompositions` are removed without compatibility aliases.
|
||||
For a previous registration at `prompts` using `id`, use:
|
||||
|
||||
```go
|
||||
rules.SequenceIdentities = []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
}
|
||||
```
|
||||
|
||||
These registrations now also enforce identity in final typed sequences. Fix
|
||||
missing or duplicate identities introduced by defaults, CLI assignments, or
|
||||
application resolution. Registration does not require a collection to exist
|
||||
or determine its allowed shape; schema and application validation retain
|
||||
those responsibilities.
|
||||
3. **Review collection precedence and ordering.** An identified collection now
|
||||
defaults to upsert across layers and inheritance when no explicit mode
|
||||
applies, rather than unconditional whole-sequence replacement. Declare
|
||||
`mode: replace` at the relevant destination where replacement is intended.
|
||||
With append or upsert, `[]` contributes no records and does not clear the
|
||||
collection; null is invalid. Use `mode: replace` with `[]` to clear it.
|
||||
An explicit mode persists at its destination until changed; a parent's mode
|
||||
does not transfer to a different inheriting destination. Unidentified
|
||||
sequences retain strict peer conflicts and whole-value precedence
|
||||
replacement. Dependency-ready assembly can change sequence positions and
|
||||
diagnostic or history order compared with the former traversal order.
|
||||
4. **Review CLI recovery and inheritance assumptions.** CLI overrides can no
|
||||
longer repair values that fail earlier decoding, bypass source or inheritance
|
||||
errors, or add or cancel inheritance relationships. Supply structural
|
||||
directives in source configuration. Application resolution still receives
|
||||
explicit CLI values and may record subsequent normalization or derivation.
|
||||
5. **Update affected Go literals and diagnostic handling.** Use keyed literals
|
||||
for expanded option and source-information structs. Collection validation adds
|
||||
`CodeSequenceInvalid` (`sequence.invalid`) and `CodeSequenceConflict`
|
||||
(`sequence.conflict`); update diagnostic handling where applicable.
|
||||
|
||||
Select this version and rebuild:
|
||||
|
||||
```sh
|
||||
go get gitea.maximumdirect.net/eric/ConfigKit@v0.9.0
|
||||
```
|
||||
|
||||
The consumer minimum remains Go 1.26.0. The YAML dependency remains
|
||||
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, and the maintainer tool dependency remains
|
||||
ReleaseKit v0.4.1. Linux amd64 and arm64 remain release-validated targets;
|
||||
macOS remains best-effort. Publication follows the existing
|
||||
[release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.9.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.9.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.9.0/docs/oss/attribution.md)
|
||||
144
docs/releases/v1.0.0.md
Normal file
144
docs/releases/v1.0.0.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# ConfigKit v1.0.0
|
||||
|
||||
ConfigKit's first stable release completes the public API cleanup, moves the Go
|
||||
module to `go.maximumdirect.net/configkit`, and scopes configuration controls to
|
||||
the root of each source document. It also corrects presence, history, and
|
||||
sensitivity reporting and adds direct source lookup for successful and failed
|
||||
loads.
|
||||
|
||||
This release includes intentional breaking changes from v0.10.2. The public Go
|
||||
API, documented configuration and resolution semantics, and structured diagnostic
|
||||
contracts now follow the [stable-release compatibility policy](../policy/release.md#versioning-and-compatibility).
|
||||
That policy includes the existing allowance for minor releases to raise the Go
|
||||
consumer minimum.
|
||||
|
||||
## Upgrade from v0.10.2
|
||||
|
||||
### Module path
|
||||
|
||||
Update the dependency requirement and Go imports from
|
||||
`gitea.maximumdirect.net/eric/ConfigKit` to `go.maximumdirect.net/configkit`,
|
||||
selecting v1.0.0. The package name remains `configkit`.
|
||||
|
||||
Go treats the two import paths as different packages with different named types.
|
||||
Coordinate the change with dependencies that exchange ConfigKit values with your
|
||||
application. Earlier tags retain their original module declarations; no old-path
|
||||
compatibility module is provided. See the
|
||||
[module migration guide](../consumers/README.md#vanity-module-path-migration).
|
||||
|
||||
### Source-document controls
|
||||
|
||||
Rename genuine source-root `configkit` control blocks to `config` in main files,
|
||||
imported fragments, underlays, selected overlays, and programmatic mapping inputs.
|
||||
The recognized controls remain `imports` and `sequences`, subject to their
|
||||
existing context-specific rules and import opt-in.
|
||||
|
||||
The control name is reserved only at the root of each acquired document. ConfigKit
|
||||
extracts controls before placing that document's payload. Nested `config` keys,
|
||||
including those in sequence records, are ordinary application data. Application-root
|
||||
`config` data produced by placement, defaults, CLI overrides, or resolution is
|
||||
also ordinary data and is not interpreted again as controls.
|
||||
|
||||
Review source-root application fields already named `config`: move, rename, or
|
||||
separately mount that data. The former names `configkit` and `_configkit` are now
|
||||
ordinary application keys at every depth, with no control aliases. A permissive
|
||||
schema can therefore accept an unmigrated control block as data without executing
|
||||
its imports. Stored source-local declaration paths change from `/configkit/...`
|
||||
to `/config/...`.
|
||||
|
||||
Source-version checks distinguish actual source-local controls from application
|
||||
paths with the same spelling. Existing inheritance rules remain in place. See
|
||||
[imports and control scope](../config.md#declaration-and-selection-rules) and
|
||||
[the directive migration guidance](../config.md#earlier-directive-spelling).
|
||||
|
||||
### Public Go API
|
||||
|
||||
The following names replace their pre-v1 counterparts outright, without
|
||||
compatibility aliases:
|
||||
|
||||
| Previous API | v1.0.0 API |
|
||||
| --- | --- |
|
||||
| `Resolution.InputInfo(path)` | `Resolution.EntryInfo(path)` |
|
||||
| `Definition.Secret` | `Definition.Sensitive` |
|
||||
| `SourceVersionModeDefault` | `SourceVersionModeMainOnly` |
|
||||
| `SourceVersionCheck` | `SourceVersionEvidence` |
|
||||
| `SourceInfo.VersionCheck` | `SourceInfo.VersionEvidence` |
|
||||
| `ImportSelection` | `EmptyImportSelection` |
|
||||
| `SourceInfo.ImportSelections` | `SourceInfo.EmptyImportSelections` |
|
||||
| `SourceInfo.DestinationUnresolved` | `SourceInfo.DestinationIsFallback` |
|
||||
| `ImportDefaults.Follow` / `.Hidden` | `FollowSymlinks` / `IncludeHidden` |
|
||||
| `Paths(paths...)` | `SearchPaths(paths...)` |
|
||||
| `LoadSecretsDir(directory)` | `LoadSecretsIntoEnvironment(directory)` |
|
||||
| `LayerBase` | `LayerMain` |
|
||||
| `CodeExplainLimitExceeded` | `CodeHistoryLimitExceeded` |
|
||||
| `Resolution.Record(path, ChangeDerivedDefault, reason, deps...)` | `Resolution.RecordDerivedDefault(path, reason, deps...)` |
|
||||
| `Resolution.Record(path, ChangeNormalization, reason, deps...)` | `Resolution.RecordNormalization(path, reason, deps...)` |
|
||||
|
||||
The recording methods describe mutations the application has already performed;
|
||||
they do not assign values. `Default` and `Normalize` remain the helpers that both
|
||||
assign and record. `EntryInfo` names the fixed resolution-entry metadata view,
|
||||
including defaults and CLI assignments. Version evidence records what was
|
||||
observed, not proof of checker execution or acceptance.
|
||||
|
||||
The redundant `ImportSelection.Outcome`, `ImportSelectionOutcome`, and
|
||||
`ImportSelectionNone` are removed. Empty-selection records retain their selector,
|
||||
path, and location. The obsolete diagnostic constants `CodeResolveInvalidKind`
|
||||
and `CodeOptionsUnavailable` are also removed without replacements.
|
||||
|
||||
Update stored or compared strings as well as Go symbols:
|
||||
|
||||
- The main provenance layer changes from `"base"` to `"main"`.
|
||||
- The retained-history limit diagnostic changes from
|
||||
`"explain.limit_exceeded"` to `"history.limit_exceeded"`.
|
||||
|
||||
The YAML import options `follow` and `hidden`, the `configkit:"secret"` Go tag,
|
||||
and typed `Secret` references retain their existing spelling and behavior. See
|
||||
the [API migration guide](../consumers/README.md#pre-v1-public-api-cleanup) for
|
||||
the consolidated mapping and usage guidance. Rebuild and run your application's
|
||||
configuration tests after migrating its imports, API calls, and source controls.
|
||||
|
||||
## Source inspection
|
||||
|
||||
`Result.Source(id)` and `LoadError.Source(id)` return a detached `SourceInfo`
|
||||
record and a found flag for an already acquired source. They complement
|
||||
`Sources()` and the existing callback-scoped `Resolution.Source(id)`. Empty or
|
||||
unknown IDs return false. Lookup does not reopen inputs or resolve credentials;
|
||||
failed loads retain the same partial, conservatively redacted evidence as their
|
||||
source inventory. See [results and failures](../config.md#results-and-failures).
|
||||
|
||||
## Correctness and internal improvements
|
||||
|
||||
- Resolution presence queries distinguish numeric mapping keys from sequence
|
||||
indexes using the configuration's entry shape. A later shape change no longer
|
||||
transfers input explicitness to a different semantic path.
|
||||
- Metadata history and value explanations now use one shared internal history
|
||||
model. This corrects attribution across shape changes, preserves container
|
||||
origins, and avoids duplicate intermediate source-history records.
|
||||
- Final dynamically typed secret fields retain their sensitivity classification
|
||||
in inspection output. Parsed dependency paths are deduplicated without losing
|
||||
distinctions needed for sensitivity propagation.
|
||||
- Composition-conflict diagnostics preserve claimant order through structured
|
||||
reporting and rendering, consistent with scoped assembly order.
|
||||
- History selection and explanation use indexes and reuse prepared selections
|
||||
and sensitivity dependencies to reduce repeated traversal. Retained-history
|
||||
accounting follows the shared model, and secret resolution and validation share
|
||||
traversal machinery.
|
||||
|
||||
These changes preserve the documented loading and inspection rules apart from
|
||||
the explicit migrations above. No end-to-end performance improvement is claimed.
|
||||
The [configuration contract](../config.md) and public Go declaration comments
|
||||
remain the behavior references.
|
||||
|
||||
## Requirements and release delivery
|
||||
|
||||
The consumer minimum rises from Go 1.26.0 to Go 1.27.0. Consumers must upgrade
|
||||
their Go toolchain before adopting this release. The YAML dependency is unchanged.
|
||||
Linux amd64 and arm64 remain the release-validated targets; macOS support remains
|
||||
best-effort under the [platform support policy](../policy/release.md#platform-support).
|
||||
|
||||
Release tooling moves to `go.maximumdirect.net/releasekit` v0.6.0. Beginning with
|
||||
this release, the source tag and checked-in note are accompanied by a Gitea release
|
||||
page containing the complete note and licensing footer. Source publication and
|
||||
hosted delivery remain separate outcomes under the [release procedure](../release.md).
|
||||
|
||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/configkit/src/tag/v1.0.0/docs/oss/attribution.md)
|
||||
@@ -1,57 +0,0 @@
|
||||
# Deferred Scope
|
||||
|
||||
This document owns intentionally excluded features, not a committed delivery plan.
|
||||
The [architecture policy](../policy/architecture.md) governs changes to these
|
||||
boundaries. Implemented behavior belongs in the [configuration contract](../config.md)
|
||||
and consumer adoption in the [consumer guide](../consumers/README.md).
|
||||
|
||||
## Explicit non-goals and deferred scope
|
||||
|
||||
General environment-variable configuration overlays are not supported. Typed secret
|
||||
references and explicit directory bootstrap are the environment convention. Literal
|
||||
credential configuration and implicit secret-directory discovery are unsupported.
|
||||
|
||||
The following remain deliberately excluded:
|
||||
|
||||
- automatic `conf.d` discovery;
|
||||
- remote imports, arbitrary providers, and additional configuration formats;
|
||||
- custom merge strategies, sequence merging, reset operators, multiple-parent
|
||||
inheritance, and cross-scope references;
|
||||
- named overlay inheritance;
|
||||
- parser-specific or custom structural-decoding extension APIs;
|
||||
- generic dynamic-subtree conversion and module-registry APIs;
|
||||
- automatic schema migrations;
|
||||
- hot reload, runtime resource replacement, and remote resource validation;
|
||||
- a bundled CLI framework or application commands; and
|
||||
- configuration fingerprints.
|
||||
|
||||
Applications may build their own validate, effective-value, source, bootstrap, and
|
||||
explanation commands around the library. Broadening these boundaries requires
|
||||
concrete consumer evidence, compatibility review, and an explicit design change.
|
||||
|
||||
Effective configuration diffs are implemented over frozen successful-load
|
||||
inspections; their current comparison and disclosure contract is in the
|
||||
[configuration contract](../config.md#effective-configuration-diffs), with
|
||||
consumer workflows in the [adoption guide](../consumers/README.md#effective-configuration-diffs).
|
||||
Fingerprints remain deferred.
|
||||
|
||||
Built-in sibling inheritance using `extends` is implemented as described in the
|
||||
[configuration contract](../config.md#sibling-inheritance). It is distinct from
|
||||
inheritance between selected overlay files, which remains excluded above.
|
||||
|
||||
## Deferred performance experiments
|
||||
|
||||
The completed performance audit left these internal optimization candidates for
|
||||
separate investigation:
|
||||
|
||||
- branch-local explanation replay instead of the current whole-tree replay;
|
||||
- redesigning the private metadata-copy handoff while preserving detached public
|
||||
records;
|
||||
- caching schema descriptors across loads; and
|
||||
- indexing complex contributor-trace membership while preserving its equality
|
||||
rules and compact linked traces.
|
||||
|
||||
These are not committed implementation work. Reconsider them with evidence that
|
||||
the benefit justifies the ownership, retained-memory, or equivalence risks. The
|
||||
completed audit and implementation evidence are preserved in Git history at
|
||||
`8e91a2b` under `docs/roadmap/`.
|
||||
104
docs/roadmap/future.md
Normal file
104
docs/roadmap/future.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Potential Future Work
|
||||
|
||||
This document lists potential enhancements and maintenance work for consideration
|
||||
in future ConfigKit releases. Entries are not accepted contracts, implementation
|
||||
plans, or release commitments. Adopting a candidate requires an explicit design
|
||||
decision and a separate plan. Significant architectural exclusions and their rationale belong
|
||||
in ADRs; this document does not maintain a list of rejected proposals.
|
||||
|
||||
## 1. Recursive body merging for identified records
|
||||
|
||||
Status: deferred. The implemented
|
||||
[collection policy](../config.md#strict-document-composition) uses `upsert` to
|
||||
replace a complete matched record. Recursive merging of fields inside matching
|
||||
records remains a separate candidate and would revisit the whole-record update
|
||||
boundary selected by [ADR 0017](../adr/0017-collection-sequence-policy.md).
|
||||
|
||||
Consider this only in response to a concrete consumer need. A proposal would need
|
||||
to define conflicts for overlapping body fields, removal of omitted fields,
|
||||
presence and ordering, and how provenance, history, sensitivity, and resource
|
||||
context follow partially retained records. Any change requires a new decision and
|
||||
implementation plan; this entry does not alter current upsert behavior.
|
||||
|
||||
## 2. Required version declarations for selected sources
|
||||
|
||||
Status: deferred. PromptKit requested a way to require version declarations on
|
||||
a selected subset of source files while permitting other fragments to omit them.
|
||||
|
||||
### Motivation and proposed behavior
|
||||
|
||||
PromptKit could require each collection's root file (for example,
|
||||
`collection.yml` or `meta.yml`, with naming still undecided) to declare an
|
||||
accepted specification version. Other files would use the existing
|
||||
[check-when-present behavior](../config.md#shared-source-version-checks).
|
||||
This would enforce a rule that otherwise remains a documentation convention.
|
||||
|
||||
All declarations would use the same application-owned `Definition.Version`
|
||||
path and acceptance policy. The proposal concerns where declarations are
|
||||
required; it does not introduce independently versioned collections or subtrees.
|
||||
A root declaration would not prove that headerless descendants declare a
|
||||
compatible version or replace final configuration validation.
|
||||
|
||||
### Questions to resolve before implementation
|
||||
|
||||
- **Selection API:** should callers mark explicit inputs, select sources by
|
||||
path or predicate, or annotate imports? An input-level requirement is simpler,
|
||||
but cannot by itself select a root reached only through an import.
|
||||
- **Scope:** does a requirement apply only to the selected document or also to
|
||||
its imports? Applying it only to that document best fits PromptKit's request;
|
||||
imported fragments could still declare versions checked by the shared policy.
|
||||
- **Mode interaction:** how should selection interact with disabled source
|
||||
checks? Require an enabled mode, or activate checking for selected sources?
|
||||
Selection would strengthen check-when-present behavior and add no requirement
|
||||
beyond the load-wide required mode.
|
||||
- **Source handling and evidence:** how should selection work across file,
|
||||
reader, byte, and mapping inputs, glob matches, and repeated imports? Define
|
||||
behavior for absent optional inputs and preserve source-local diagnostics,
|
||||
inspection evidence, and existing sensitivity handling.
|
||||
|
||||
## 3. Performance experiments
|
||||
|
||||
Status: deferred pending evidence. The completed performance audit identified
|
||||
four internal optimization candidates for separate investigation:
|
||||
|
||||
- Branch-local explanation replay instead of the current whole-tree replay.
|
||||
- Redesigning the private metadata-copy handoff while preserving detached public
|
||||
records.
|
||||
- Caching schema descriptors across loads.
|
||||
- Indexing complex contributor-trace membership while preserving its equality
|
||||
rules and compact linked traces.
|
||||
|
||||
These are experiments, not committed implementation work or promised speedups.
|
||||
Reconsider them with evidence that the benefit justifies the ownership,
|
||||
retained-memory, or equivalence risks. The completed audit and implementation
|
||||
evidence are preserved in Git history at `8e91a2b` under `docs/roadmap/`.
|
||||
|
||||
### Questions to resolve before implementation
|
||||
|
||||
- Which representative workloads show enough remaining cost to justify each
|
||||
optimization, and what measurements would establish a useful improvement?
|
||||
- Can narrower replay and membership indexes preserve explanation results,
|
||||
ordering, provenance, and sensitivity behavior?
|
||||
- Can metadata handoff changes preserve detached public records and ownership
|
||||
across loads without introducing aliasing?
|
||||
- What cache lifetime and concurrency design would make schema reuse worthwhile
|
||||
without excessive retained memory?
|
||||
|
||||
## 4. Public path comparison
|
||||
|
||||
Status: deferred pending a concrete consumer need. This preserves A2 from the
|
||||
completed API review at `68400ff:docs/roadmap/review.md`.
|
||||
|
||||
`Path.String()` is a JSON Pointer spelling, not a complete semantic identity:
|
||||
an explicit numeric mapping key and a sequence index can share it. The public
|
||||
type is opaque and currently non-comparable.
|
||||
|
||||
If consumers need comparison, consider a small exact-representation operation
|
||||
rather than exposing mutable segments or introducing a pattern language. A
|
||||
proposal must distinguish exact segment representation, including parsed versus
|
||||
explicit segments and invalid state, from resolution against a document. Pointer
|
||||
string equality must not be presented as semantic equality.
|
||||
|
||||
Keeping `Path` solely as an opaque query/building value remains an alternative.
|
||||
No current contract changes or implementation commitment follow from this entry;
|
||||
a comparison operation can be added compatibly later.
|
||||
@@ -1,580 +0,0 @@
|
||||
# Implement configuration input destinations
|
||||
|
||||
Status: complete. Stages 1–12 are implemented and audited. The
|
||||
[feature roadmap](input-destinations.md) owns the approved behavior and rationale; the
|
||||
[consumer request](promptkit-feature-request.md#public-acceptance-scenarios)
|
||||
supplies the public acceptance scenarios. This document owns execution and
|
||||
completion evidence. Planning baseline: `6353598`.
|
||||
|
||||
## Working instructions
|
||||
|
||||
- Start each stage with `AGENTS.md`, [development.md](../development.md), the
|
||||
applicable policies, and working-tree inspection. Use graph-first code
|
||||
discovery. Preserve unrelated changes and previously completed stages.
|
||||
- Implement only that stage and its supporting tests/documentation. Mark it
|
||||
complete here with actual commands and evidence, then commit the nonempty
|
||||
validated unit with a short plain-English message. Subsequent stages remain
|
||||
planned. A stage is not complete solely because its narrow tests pass.
|
||||
- Keep early mechanics private until the complete engine behavior is ready.
|
||||
Root destination fields land in Stage 10. Ordinary root loading must remain
|
||||
working throughout. Do not publish a partially supported public placement API.
|
||||
- Follow the existing engine/document/YAML boundaries. Reuse strict composition,
|
||||
recursive overlay, sibling inheritance, paths, history, and sensitivity.
|
||||
No dependencies, toolchain changes, alternate mergers, mount registry,
|
||||
per-fragment lifecycle, import-destination syntax, resource discovery, or new
|
||||
release gates are needed. These are scope boundaries, not new prohibitions
|
||||
on separately approved future work.
|
||||
- Update current-behavior documentation when public behavior lands; keep future
|
||||
claims here or in the roadmap until then. Add the ADR in Stage 1 under the
|
||||
documentation policy's accepted-decision exception.
|
||||
- Use focused offline tests while iterating; run `./scripts/check.sh` before
|
||||
committing production changes. Documentation-only stages need whitespace and
|
||||
local-link checks. Stage 12 adds full ownership/race validation. Tests protect
|
||||
observable behavior, not wrapper allocation or helper-call counts. Reuse
|
||||
coverage rather than repeat an entire loading matrix at every layer.
|
||||
- No benchmark campaign or public complexity guarantee is required. Prefer
|
||||
straightforward bounded walks and wrapping each acquired contribution once;
|
||||
avoid rescanning all preceding contributions for every new input.
|
||||
- Do not push, tag, publish, update downstream dependencies, or retire unfinished
|
||||
records. Release compatibility review is separate from publication.
|
||||
|
||||
## Resolved implementation decisions
|
||||
|
||||
1. Add root-owned `Input.Destination Path` and `SourceInfo.Destination Path`,
|
||||
mirrored privately in engine input/source records. Extend `loadInput` and
|
||||
explicit root/engine conversion and cloning. No new loading entry point,
|
||||
public wrapper type, option, or origin field is needed. Ordinary `Load`
|
||||
continues to supply root placement.
|
||||
2. The zero path means root. Validate `Path.valid()` and reject every explicit
|
||||
index segment, including a negative index. Normalize parsed tokens to literal
|
||||
mapping-key segments: `/0` means the key `0`, not sequence element zero.
|
||||
Empty, dot, dot-dot, slash-containing, tilde-containing, and numeric keys keep
|
||||
existing semantic-path meaning. Do not split dots, interpret filesystem
|
||||
traversal, enforce identifier grammar, or add namespace uniqueness.
|
||||
3. Reject destination segments equal to `configkit` or `extends`: placement
|
||||
would introduce a reserved mapping key with a mapping value. Substrings such
|
||||
as `myextends` are ordinary keys. This preserves the approved structural
|
||||
rules rather than adding an application naming policy. Source-root import
|
||||
directives are still processed before placement.
|
||||
4. Validate destination shape/reservations for all declared inputs during
|
||||
grouped option preflight, before reading any source. Reuse
|
||||
`CodeOptionsInvalid` for invalid destination declarations; include the
|
||||
offending semantic path when representable, with no fabricated physical
|
||||
location. An invalid source still follows existing source-option validation.
|
||||
A valid destination on `NoSource` contributes no nodes, source record, or
|
||||
budget charges. No extra error type or exact message contract is introduced.
|
||||
5. Separate acquired source syntax from application placement. Retain original
|
||||
parsed source metadata for filesystem/directive handling. Strip source-root
|
||||
directives, then wrap each application body at the normalized destination
|
||||
before fragment version checks and strict composition. Imports inherit the
|
||||
same destination transitively. Do not wrap both a source application and its
|
||||
recomposed result; every retained application must already use effective
|
||||
coordinates. Content roots remain mappings under existing source contracts.
|
||||
6. Wrapping a body at `Keys("a", "b")` creates `{a: {b: body}}` as private
|
||||
document mappings. Actual body nodes and entries keep their physical source
|
||||
coordinates. Introduced mappings/entries use the contributing document's root
|
||||
location as a documented source fallback, not purported authored coordinates.
|
||||
They have the same contributing source ID and create no synthetic source.
|
||||
Retained history trees remain immutable and separate from assembly mutation.
|
||||
7. A present empty body creates explicit destination mappings, including an
|
||||
import-only source whose application body is empty. It still cannot clear
|
||||
lower mappings. A missing optional import and `NoSource` remain absent. Empty
|
||||
source text remains the existing invalid document, not an empty mapping.
|
||||
8. `VersionRule.Path` stays an effective application path. Check main once after
|
||||
its own imports compose and before underlays/overlay/CLI, at the existing
|
||||
phase. Imported and underlay claims are checked against placed application
|
||||
content, including scalar/null/sequence ancestors replacing a protected path.
|
||||
A mapping ancestor with no protected descendant is not a version claim.
|
||||
Mounted domain `version` fields are ordinary data. Existing inheritance,
|
||||
selected-overlay, CLI protection and exact-path numeric conversion remain;
|
||||
no nested blanket conversion or additional fragment checker is introduced.
|
||||
9. Placement does not alter physical source identity, filesystem namespace,
|
||||
source IDs, acquisition order, imported-from locations, or resource contexts.
|
||||
Repeated physical sources in one strict group still fail even at different
|
||||
destinations. The same physical source in separate tiers retains the existing
|
||||
independent source applications/records and each record's own destination.
|
||||
Overlay and CLI source records have root `Destination`; they are not mounted
|
||||
merely because main is mounted. Active layered overlays retain their explicit
|
||||
independent filesystem selection and application-root meaning.
|
||||
10. Value-level paths use effective coordinates. Keep genuine source-local
|
||||
import-directive diagnostics and parse/read failures at their original
|
||||
locations; do not invent a logical child path when parsing cannot identify
|
||||
one. Prefix existing application-relative diagnostic paths exactly once
|
||||
where available. Preserve structured codes, related physical locations,
|
||||
deterministic ordering, and value-free messages.
|
||||
11. Use current depth/node limits, not a new destination cap. With root depth
|
||||
zero, each destination segment adds one level and, under `countInputNodes`,
|
||||
one mapping node plus one key. Retain current raw input accounting (including
|
||||
directives); charge only the additional `2 * segmentCount` synthetic nodes
|
||||
per acquired body, once, to the shared input budget. Placement adds no bytes,
|
||||
acquired sources, import nesting, or glob visits. Validate placed application
|
||||
depth and per-document node bounds before composition; share accounting
|
||||
across inputs and imports. Precheck wrapper depth/count before allocating
|
||||
wrappers. Use existing source/import limit diagnostics in their acquisition
|
||||
contexts; retain projection, expansion, and history bounds independently.
|
||||
Root placement adds no charge or new rejection. NoSource adds none either.
|
||||
12. Cloned source records include independently owned destination paths at every
|
||||
existing publication boundary: results, load errors, metadata/inspection,
|
||||
callback input information, and root conversion. Passive resource contexts
|
||||
remain keyed by the real source identity; inspection must work without an
|
||||
open configuration handle after load. This does not extend caller-owned
|
||||
filesystem or callback resource lifetimes.
|
||||
|
||||
## Stage 1 — Record the decision and capture the baseline
|
||||
|
||||
Status: complete. Scope: decision record and evidence, no feature code.
|
||||
|
||||
1. Read the roadmap, request, relevant policies, and ADRs 0001, 0003, 0006,
|
||||
0008, and 0009. Inspect the owners identified in the roadmap and the function
|
||||
map below. Confirm the baseline and worktree before starting.
|
||||
2. Run `./scripts/check.sh`; record the commit, Go version, and results here.
|
||||
Locate existing coverage for root loading, strict underlays, version ownership,
|
||||
repeated sources, history, resource contexts, and detached records.
|
||||
3. Write `docs/adr/0011-input-destinations.md` after confirming the number is
|
||||
unused. Record the approved input-placement decision as Accepted, with the
|
||||
roadmap owning implementation status. Follow the repository ADR format.
|
||||
Focus on ConfigKit, put rejected options only in Alternatives Considered,
|
||||
and avoid negative parallelism elsewhere. No PromptKit-specific semantics.
|
||||
4. Record the compatibility assessment under the release policy: adding fields
|
||||
preserves zero-value/keyed-literal use, but callers using unkeyed literals
|
||||
for `Input` or `SourceInfo` must migrate to keyed literals. Treat the feature
|
||||
as minor-release work; select a version and write its note only during
|
||||
separately authorized release preparation.
|
||||
|
||||
Validated: `./scripts/check.sh` (go1.26.8 linux/amd64) passed on candidate
|
||||
`d3d468a8b47d780787fca329ff65b107d98ca59f`; this includes formatting,
|
||||
whitespace, local documentation links, tests, vet, and build. The planning
|
||||
baseline `6353598bd6cce2eb78a5fe990e144cc0cadf1280` is present in history. The
|
||||
worktree was clean before this stage.
|
||||
|
||||
Coverage located for later destination work:
|
||||
|
||||
- Root and equivalent source loading: `TestEquivalentSourceFormsPreserveInputSemantics`
|
||||
(`facade_equivalence_test.go`) and
|
||||
`TestFacadeLayeredZeroUnderlaysMatchesOrdinaryLoad` (`facade_layered_test.go`).
|
||||
- Strict underlays and group budgets: `TestGroupedLoadStrictConflictsRepetitionAndBudgets`
|
||||
and `TestGroupedLoadPrecedenceSourceOrderAndSingleLifecycle`
|
||||
(`internal/engine/load_request_test.go`).
|
||||
- Version ownership and imported claims: `TestLoadRejectsImportConflictsAndFragmentVersions`
|
||||
(`internal/engine/imports_test.go`) and version-path coverage in
|
||||
`internal/engine/load_test.go`.
|
||||
- Repeated physical sources: `TestRepeatedImportsAcrossTiersAndReaderConsumption`
|
||||
(`facade_equivalence_test.go`).
|
||||
- History and source contexts: `TestUnderlayHistoryRetainsLaterMappingContributors`
|
||||
(`facade_underlay_origins_test.go`) and
|
||||
`TestFacadeSourceContextsFollowOriginsWithoutQueryIO`
|
||||
(`facade_resource_context_test.go`).
|
||||
- Detached records: `TestLoadMetadataReflectsDefaultsInputAndDetachedRecords`
|
||||
(`internal/engine/load_test.go`) and
|
||||
`TestMetadataQueriesReturnDetachedRecords` (`internal/engine/metadata_test.go`).
|
||||
|
||||
The decision is recorded in [ADR 0011](../adr/0011-input-destinations.md).
|
||||
Compatibility assessment: adding the two public fields breaks callers using
|
||||
unkeyed literals for `Input` or `SourceInfo`; keyed and zero-value use remains
|
||||
compatible. The release policy permits the intentional pre-1.0 contract change
|
||||
in a minor release when the release note states impact and migration. Version
|
||||
selection and release-note preparation remain out of scope until separately
|
||||
authorized. No feature code or current behavior contract changed.
|
||||
|
||||
## Stage 2 — Add private destination normalization and wrapping
|
||||
|
||||
Status: complete. Scope: pure private mechanics with bounded unit tests.
|
||||
|
||||
1. Add a small engine helper file for normalized mapping-only destinations and
|
||||
constructing placed document bodies. Follow decisions 2–7; keep helpers
|
||||
private and avoid introducing a second path abstraction.
|
||||
2. Validate invalid paths, explicit indices, and exact reserved tokens. Normalize
|
||||
parsed numeric tokens into keys, preserving literal text and pointer escaping.
|
||||
3. Build wrappers without mutating the supplied tree. Preserve body locations;
|
||||
give introduced entries/mappings the source-root fallback. Implement depth and
|
||||
node measurement/prechecks usable with existing limits, including the extra
|
||||
synthetic-node count; aggregate charging is wired in Stage 7.
|
||||
4. Cover root identity/equivalence, nested empty mapping, literal/escaped/numeric
|
||||
keys, invalid index/path, exact versus substring reserved keys, owned paths,
|
||||
unchanged body coordinates, and bounded wrapper checks. Do not count helper
|
||||
invocations or fix an allocation strategy in tests.
|
||||
|
||||
Validated with `go test ./internal/engine -run 'Destination|PlaceDocument'`
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); both passed. The private
|
||||
helpers normalize mapping-only destinations, preserve authored nodes and source
|
||||
coordinates, and check per-document depth/node bounds before allocating wrappers.
|
||||
No loader accepts a new public field yet.
|
||||
|
||||
## Stage 3 — Integrate private non-import input placement
|
||||
|
||||
Status: complete. Scope: engine request plumbing and ordinary acquired bodies.
|
||||
|
||||
1. Add destination fields to engine `Input`, `loadInput`, and `SourceInfo`.
|
||||
Preserve ownership through engine `LoadLayered` request construction,
|
||||
`cloneSourceInfo`, and source-record copies. Keep the root facade unchanged.
|
||||
2. Extend grouped preflight to validate/normalize destinations before source
|
||||
reads. Use the existing diagnostic accumulator and options-invalid category.
|
||||
Ordinary `Load` supplies root placement through its existing request adapter.
|
||||
3. Thread placement into `assembleBaseInputTracked` without confusing raw
|
||||
`parsedSource` with effective `assembledInput.document`. Preserve the root
|
||||
path through existing wrappers/callers. For imports-disabled acquisition,
|
||||
validate disabled directives on raw content, place the body, and retain the
|
||||
destination on the acquired source record. NoSource still short-circuits.
|
||||
4. Use engine loads to cover mounted main and underlay bodies, omitted versus
|
||||
explicit root destinations, empty mapping versus NoSource, and preflight
|
||||
rejection without consuming a test-owned reader. Reuse current source-kind
|
||||
acquisition tests; full source equivalence is verified publicly in Stage 10.
|
||||
|
||||
Validated with `go test ./internal/engine -run 'Destination|PlaceDocument|Grouped'`
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); both passed. Private non-import
|
||||
placement reaches assembly, with root behavior unchanged. Imports are the next
|
||||
private integration step.
|
||||
|
||||
## Stage 4 — Carry placement through import acquisition
|
||||
|
||||
Status: complete. Scope: source-local directives and placed imported applications.
|
||||
|
||||
1. Thread a normalized destination through `loadImportedDocumentsTracked`,
|
||||
`loadImportedWithAccessTracked`, and `importTraversal`, retaining root defaults
|
||||
for current private callers. Place the selected body's application content
|
||||
after `splitImportDirectives`; do the same for every imported body.
|
||||
2. Set placement on selected/imported source records, including error records
|
||||
once a source has been acquired. Keep `ImportedFrom` at the physical directive.
|
||||
Run imported `fragmentVersionClaim` against placed bodies and retain placed
|
||||
bodies in `state.documents` before `composeDocuments` builds applications.
|
||||
3. Leave filesystem joins, containment, glob traversal, optional missing inputs,
|
||||
active cycles, visited identities, and acquisition IDs/order unchanged.
|
||||
Destination must not become part of the repeated-source identity key.
|
||||
4. Test native and host transitive imports, nested source-root directives,
|
||||
import-only bodies, and mounted imported protected versus ordinary version
|
||||
paths. Verify a repeated file at two destinations in one group still fails,
|
||||
including an import/direct-input alias. Reuse cycle/containment regressions.
|
||||
|
||||
Validated with `go test ./internal/engine -run 'Destination|PlaceDocument|Import|Grouped'`
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); both passed. Retained imported
|
||||
contributions have effective placement and physical source identity, with no
|
||||
double wrapping or application-level directive leakage.
|
||||
|
||||
## Stage 5 — Complete strict tiers and main version ownership
|
||||
|
||||
Status: complete. Scope: assembly and version interactions, not a new merger.
|
||||
|
||||
1. Review `acquireUnderlayGroup`: version checks must use placed content, and
|
||||
documents taken from `acquired.composition.applications` must already be
|
||||
placed. Recompose strict peers with ordinary `composeDocuments`; preserve
|
||||
per-source applications, origins, UnderlayIndex, and input accounting.
|
||||
2. In `executeLoadRequest`, main's version check uses its placed main/import
|
||||
composition at the existing point. `assembleLayeredInput` recursively overlays
|
||||
completed underlay groups beneath main, selected overlay, and CLI as before.
|
||||
Do not mount the final assembly or prepend main's destination to upper tiers.
|
||||
3. Test disjoint same-destination peers, different destinations, equal and unequal
|
||||
conflicts, ancestor/descendant destinations, and a strict conflict despite an
|
||||
upper override. Verify recursive mapping overlays, atomic sequences, null,
|
||||
explicit false/zero, and version-only domain overrides retaining lower fields.
|
||||
4. Exercise mounted main with a nested VersionRule, absent root declarations,
|
||||
imported/underlay direct and ancestor claims, and nonclaiming mapping ancestors.
|
||||
Check the hook runs once and does not inspect each fragment independently.
|
||||
Preserve exact configured-path integer conversion and ordinary nested typing.
|
||||
|
||||
Validate: destination/composition/overlay/version/grouped tests and everyday
|
||||
check. Validated with `go test ./internal/engine -run 'LayeredDestination'`,
|
||||
`go test ./internal/engine -run 'Destination|Composition|Overlay|Version|Grouped'`,
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); all passed. The new mounted
|
||||
cases verify strict same-tier conflicts and disjoint composition, independent
|
||||
destinations and nested paths, recursive/atomic overlay behavior, explicit
|
||||
null/false/zero, and main-only nested version checking after imports compose.
|
||||
Done: placed and authored equivalents agree on values and conflicts; main
|
||||
ownership and lifecycle timing are preserved.
|
||||
|
||||
## Stage 6 — Reconcile diagnostic coordinate systems
|
||||
|
||||
Status: complete. Scope: useful paths without fictional source locations.
|
||||
|
||||
1. Audit early acquisition, import, fragment-version, and composition failures.
|
||||
Use one narrow path-prefix translation where diagnostics genuinely identify
|
||||
application content; do not blanket-prefix every diagnostic or double-prefix
|
||||
diagnostics already produced by placed trees.
|
||||
2. Preserve syntax/read/directive locations and related import sites. Value
|
||||
conflicts and later typed/resolver/validation failures use effective paths
|
||||
but original content coordinates. Invalid introduced structure identifies
|
||||
the destination declaration rather than a nonexistent source wrapper line.
|
||||
3. Add focused assertions for a mounted conflict with two real sources, a mounted
|
||||
decode or validation error, an imported version error, invalid destination,
|
||||
and malformed source-local directives. Test stable codes/paths/locations and
|
||||
nil results, not whole messages or callback choreography.
|
||||
|
||||
Validate: destination/diagnostic/import/version/decode tests and everyday check.
|
||||
Validated with `go test ./internal/engine -run 'LayeredDestinationDiagnostics'`,
|
||||
`go test ./internal/engine -run 'Destination|Diagnostic|Import|Version|Decode'`,
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); all passed. The mounted
|
||||
conflict, decode, and imported-version assertions use effective application
|
||||
paths with authored source locations; invalid destinations have no physical
|
||||
location; malformed source-root directives retain their authored path and
|
||||
location. Later value diagnostics are produced from placed trees, while directive
|
||||
diagnostics retain their raw source coordinates. Post-review remediation also
|
||||
rebases known Mapping value-builder failure paths, which are produced before
|
||||
placement. `TestFacadeDestinationMappingFailurePaths` covers mounted main and
|
||||
underlay failures, escaped keys and sequence-index intent, cycles, unchanged
|
||||
root paths, and unchanged source-constructor failures. Its mounted value cases
|
||||
failed before the fix and pass afterward.
|
||||
Done: diagnostics consistently distinguish application paths from source syntax.
|
||||
|
||||
## Stage 7 — Complete shared placement accounting
|
||||
|
||||
Status: complete. Scope: resource bounds with no new public limit system.
|
||||
|
||||
1. Wire Stage 2's prechecks and synthetic-node charges into both tracked ordinary
|
||||
acquisition and import traversal, using `importAccounting` and current limit
|
||||
structures. Charge each source body once, including empty import bodies.
|
||||
2. Preserve existing raw byte/source/node accounting and imported directive
|
||||
inspection. Additional wrappers count in per-body depth/node checks and
|
||||
shared aggregate nodes; recomposition must not charge them again. Ensure
|
||||
selected overlays and CLI start from totals including placement charges.
|
||||
3. Test below/at/above relevant bounds using small private limit fixtures where
|
||||
existing seams permit; add a narrow private seam if necessary. Cover deep
|
||||
body plus destination, many shallow mounted inputs, imported wrappers, and
|
||||
empty mappings. Verify root placement and NoSource do not add charges.
|
||||
Preserve history/expansion limits; do not turn fixture values into defaults.
|
||||
|
||||
Validated with `go test ./internal/engine -run 'Destination|Import|Grouped|Overlay|Override'`
|
||||
and `./scripts/check.sh` (go1.26.8 linux/amd64); both passed. New coverage in
|
||||
`TestDestinationPlacementCountsAgainstSharedNodeBudget`,
|
||||
`TestMultipleMountedInputsSharePlacementCharges`,
|
||||
`TestImportedEmptyBodiesChargeEachDestinationWrapper`,
|
||||
`TestSelectedOverlayAndCLIUsePlacementNodeCharges`, and
|
||||
`TestNoSourceAndRootPlacementAddNoWrapperCharges` checks totals below, at, and
|
||||
above the boundary, multiple shallow inputs, imported empty bodies, and charging
|
||||
of upper tiers from mounted inputs. `TestPlaceDocumentRootAndBounds` continues
|
||||
to cover per-document depth/node boundaries; existing grouped accounting tests
|
||||
cover unchanged raw source/byte charging. Done: placement cannot bypass current
|
||||
budgets and introduces no unrelated caps.
|
||||
|
||||
## Stage 8 — Preserve provenance, history, and source resource contexts
|
||||
|
||||
Status: complete. Scope: engine inspection and retained evidence.
|
||||
|
||||
1. Audit `compositionOrigins`, `completeCompositionOrigins`, application replay,
|
||||
metadata construction/finalization, and source-record cloning against placed
|
||||
applications. Prefer feeding placed trees into existing processing rather
|
||||
than adding another path-offset index or rewriting final metadata afterward.
|
||||
2. Verify explicit destination containers, actual leaves, composite origins,
|
||||
superseded/removed descendants, and root/source fallbacks. Historical and
|
||||
effective indexes keep independently owned paths and existing shape semantics.
|
||||
3. Preserve source-ID resource contexts through imports, surviving lower-tier
|
||||
values, and inheritance. Test completed-result and resolution-time context
|
||||
queries against real relative files, without making ConfigKit open the
|
||||
referenced application resource or retain loading handles.
|
||||
4. Exercise the same physical source in separate tiers with different placement,
|
||||
inspect each source record, and verify independent destination paths. Repeat
|
||||
inspection after comparison and independent loads to detect retained mutation.
|
||||
|
||||
Validated with `go test ./internal/engine -run 'Destination|Metadata|Provenance|History|Resource|SourceContext'`,
|
||||
`go test -race ./internal/engine`, and `./scripts/check.sh` (go1.26.8
|
||||
linux/amd64); all passed. `TestLayeredDestinationsRetainOriginsHistoryAndDetachedRecords`
|
||||
checks synthetic-container fallback locations, authored leaf coordinates,
|
||||
composite origins, removed-descendant history, and the same host file retained
|
||||
at distinct destinations in separate tiers before and after comparison and an
|
||||
independent load. `TestLayeredImportedInheritedValuesRetainResourceContexts`
|
||||
uses real temporary files to verify resolution-time and completed-result
|
||||
relative-resource lookup through an imported surviving parent and its inherited
|
||||
child. Existing metadata and resource-context tests continue to cover detached
|
||||
records and loading-handle closure. Done: inspection explains effective
|
||||
placement and physical origin without consumer reconstruction.
|
||||
|
||||
## Stage 9 — Verify final inheritance, sensitivity, and lifecycle integration
|
||||
|
||||
Status: complete. Scope: complete private engine behavior before facade exposure.
|
||||
|
||||
1. Resolve sibling inheritance only after full source precedence using the
|
||||
existing expansion path. Cover a child and parent supplied by separate peers,
|
||||
nested destination-local siblings, cancellation, and an inherited relative
|
||||
resource retaining its declaring context. Cross-collection search is unchanged.
|
||||
2. Reuse existing inheritance-version, overlay, and CLI guards; add targeted
|
||||
mounted cases for actual protected paths and their ancestors. Do not reject
|
||||
unrelated inheritance merely because it shares a mapping ancestor.
|
||||
3. Test effective-path secret predicates, field tags and recorded dependencies,
|
||||
superseded classified values, safe effective output/history/explanation, and
|
||||
bilateral diff redaction using disposable canaries. Compare mounted versus
|
||||
explicitly wrapped loads for equal effective values and no differences.
|
||||
4. Verify defaults, decoding, resolution, secret lookup, and validation execute
|
||||
once on the final shape; failures return no usable result. Reuse existing
|
||||
ordinary-load lifetime/ownership coverage and add mounted protection where
|
||||
placement creates a distinct risk.
|
||||
|
||||
Validate: destination/inheritance/secret/sensitivity/explain/diff/lifecycle tests
|
||||
and everyday check. Validated with
|
||||
`go test ./internal/engine -run 'Destination|Inheritance|Secret|Sensitivity|Explain|Diff|Load|Version|Grouped' -count=1`,
|
||||
`go test -race ./internal/engine`, and `./scripts/check.sh` (go1.26.8
|
||||
linux/amd64); all passed.
|
||||
|
||||
`TestLayeredDestinationInheritanceUsesFinalSiblingScopes` verifies parent and
|
||||
child contributions from separate peers, a higher-tier parent update, nested
|
||||
destination-local siblings, and cancellation. `TestLayeredDestinationInheritanceDoesNotSearchOtherCollections`
|
||||
keeps sibling lookup local. `TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers`
|
||||
protects inherited values, scalar/null ancestors, consumed directives, selected
|
||||
overlays, and CLI overrides at mounted paths while allowing nonclaiming mapping
|
||||
ancestors and unrelated inheritance. The coverage exposed an ancestor gap in
|
||||
the existing inheritance-version check; it now applies the shared fragment claim
|
||||
logic to the transferred parent subtree.
|
||||
|
||||
`TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape` checks one
|
||||
defaults/resolution/secret-lookup/validation lifecycle, final mounted decoding,
|
||||
field tags and effective-path predicates, superseded history, and safe effective
|
||||
output/explanation against canaries; it compares mounted and explicitly wrapped
|
||||
loads for equal configuration and no diff. `TestLayeredDestinationDiffUsesBilateralSensitivity`
|
||||
checks redaction from either side and recorded dependencies, and
|
||||
`TestLayeredDestinationDecodeFailureReturnsNoResultOrLaterCallbacks` checks a
|
||||
mounted decode failure returns no result. Stage 8's
|
||||
`TestLayeredImportedInheritedValuesRetainResourceContexts` continues to verify
|
||||
the declaring context for inherited relative resources. Done: approved engine
|
||||
semantics integrate without per-fragment loading or a post-return processing
|
||||
pass.
|
||||
|
||||
## Stage 10 — Expose the root-owned facade and public acceptance tests
|
||||
|
||||
Status: complete. Scope: the two public fields and complete consumer behavior.
|
||||
|
||||
1. Add `Input.Destination Path` and `SourceInfo.Destination Path` to root types.
|
||||
Extend `toEngineInput`, `fromEngineSourceInfo`, `toEngineSourceInfo`, and all
|
||||
source publication paths with owned conversion. Run existing public/import
|
||||
boundary tests; expose no engine nodes, aliases, or bridge methods.
|
||||
2. Document zero/root behavior, mapping-only paths, main/underlay parity,
|
||||
inherited import placement, source-coordinate fallback, and source ownership
|
||||
beside declarations. Update the configuration contract for implemented input
|
||||
destinations, preserving canonical ownership of shared semantics.
|
||||
3. Add external-package public-load coverage for equivalent host file, native
|
||||
fs.FS file, byte, reader, and Mapping sources. Cover default/root destinations,
|
||||
literal escaped/numeric keys, empty mapping, NoSource, and invalid declarations.
|
||||
Include one public mounted main case, not just underlays.
|
||||
4. Add representative public tests tying final configuration to metadata,
|
||||
source placement, history, resources, callbacks, and safe diffs. Mutate returned
|
||||
record copies and repeat queries/loads to confirm detachment. Reuse engine
|
||||
matrices; public tests demonstrate supported integration rather than every
|
||||
private branch. Confirm failed loads expose detached source destinations.
|
||||
|
||||
Validated with `go test . -run 'Destination|FacadeLayered|PublicFacadeBoundary|DependencyImportBoundary' -count=1`,
|
||||
`go test -race ./...`, and `./scripts/check.sh` (go1.26.8 linux/amd64); all
|
||||
passed.
|
||||
|
||||
`TestFacadeInputDestinationsAcrossSourceKinds` exercises host, native
|
||||
filesystem, bytes, reader, and mapping inputs with literal escaped and
|
||||
numeric-looking mapping keys. `TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths`
|
||||
covers omitted/empty root placement, empty mapping presence, absent `NoSource`,
|
||||
invalid index/reserved destinations, and reader preflight. `TestFacadeImportedSourcesRetainInputDestination`
|
||||
checks public transitive import placement. `TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff`
|
||||
ties mounted main/underlay values to metadata, both source destinations, callback
|
||||
source context, relative-resource access during and after loading, history, safe
|
||||
effective/explanation rendering, and bilateral diff redaction; it also mutates
|
||||
returned records and confirms later queries and comparisons remain unchanged.
|
||||
`TestFacadeFailedLoadExposesDetachedSourceDestination` verifies failure records,
|
||||
and `TestSourceInfoDestinationConversionsAndCopies` covers root/engine source
|
||||
record conversion. Existing public-boundary tests pass with the new root-owned
|
||||
fields. Done: destination selection and inspection are available through the
|
||||
supported ConfigKit import path.
|
||||
|
||||
## Stage 11 — Add maintained simple and collection-style consumer examples
|
||||
|
||||
Status: complete. Scope: executable adoption examples with consumer-owned types.
|
||||
|
||||
1. Add a minimal mounted-fragment example using `Input.Destination` in an
|
||||
ordinary layered load. Show literal keys and root-default compatibility
|
||||
without introducing consumer parsing or generic merge helpers.
|
||||
2. Add one representative collection assembly under `examples/consumers`: peer
|
||||
prompt/profile fragments, collection metadata with ordinary `version`, a
|
||||
source-root import, local sibling inheritance, and an intentional higher-tier
|
||||
version-only override retaining the body. Keep the application VersionRule
|
||||
main-owned. Use a controlled native filesystem or temporary files.
|
||||
3. Demonstrate effective-path inspection plus source-relative resource lookup
|
||||
during resolution and from the result, including an inherited/surviving field.
|
||||
Application code owns opening/interpreting the resource and any qualified IDs.
|
||||
4. Add tests for final values, presence, actual source placement/coordinates,
|
||||
resource context, and expected conflict behavior. Link maintained examples
|
||||
from the consumer guide; add a small executable root example if useful for
|
||||
discovery without duplicating the complete scenario.
|
||||
|
||||
Validated with `go test ./examples/consumers -count=1`,
|
||||
`go test . -run '^Example' -count=1`, and `./scripts/check.sh` (go1.26.8
|
||||
linux/amd64); all passed, including local documentation links.
|
||||
|
||||
`Example_inputDestination` demonstrates a literal mounted mapping path and an
|
||||
ordinary root input without consumer-side parsing or merging.
|
||||
`LoadCollectionBundle` and `TestCollectionBundleAssemblesMountedPeersAndResources`
|
||||
exercise prompt/profile peer sources, imported collection metadata with an
|
||||
ordinary version label, main-owned schema-version checking, local sibling
|
||||
inheritance, and a higher-tier prompt version-only change that retains the rest
|
||||
of its body. The resolver and `ReadCollectionPromptContent` follow direct and
|
||||
inherited `content_file` provenance to the declaring native-filesystem context.
|
||||
The test checks final values, explicit presence, source destinations and authored
|
||||
coordinates, relative resources, and the inherited field's origin.
|
||||
`TestCollectionPeerPromptConflictsRemainStrict` preserves expected conflict
|
||||
behavior. The examples are linked from the consumer adoption guide. Done: simple
|
||||
and representative consumers substantiate the same model without a compensating
|
||||
structural pass.
|
||||
|
||||
## Stage 12 — Reconcile documentation and audit the complete implementation
|
||||
|
||||
Status: complete. Scope: final integration audit and evidence, not release work.
|
||||
|
||||
1. Update README navigation, consumer guidance, the component overview, and
|
||||
development reading/test maps. Add ADR 0011 to navigation. Update architecture
|
||||
policy only as needed to recognize placement within existing assembly and
|
||||
source ownership, linking precise behavior to the configuration contract.
|
||||
Do not change unrelated policies, deferred boundaries, or published notes.
|
||||
2. Audit every resolved decision, roadmap behavior, and request acceptance row
|
||||
against implementation and tests. Record the corresponding test names here;
|
||||
for reused coverage explain the behavior it proves. Do not mark complete
|
||||
based on green tests that do not exercise mounting.
|
||||
3. Review the complete baseline-to-candidate diff for accidental strengthening:
|
||||
extra naming rules, namespace uniqueness, main-only restrictions, changed
|
||||
merge/version semantics, invented source-read ordering guarantees, new limits,
|
||||
or expanded secret policies. Remove temporary helpers/unsupported branches
|
||||
left by staged integration. Confirm original source coordinates remain real.
|
||||
4. Run `./scripts/check.sh` and
|
||||
`GOTOOLCHAIN=local GOPROXY=off go test -race ./...`. Review source-kind and
|
||||
placement ownership coverage, error disclosure, complete-on-return handling,
|
||||
and root/private boundaries. Record commit, Go version, results, and any
|
||||
limitations. No release command, CI waiting, benchmark, or downstream upgrade.
|
||||
5. Mark this plan and the roadmap implemented only when code, public examples,
|
||||
and owning contracts agree. Retain completed planning evidence until separately
|
||||
authorized release preparation.
|
||||
|
||||
Audit evidence for the approved decisions and request acceptance surface:
|
||||
|
||||
| Behavior reviewed | Evidence and what it proves |
|
||||
| --- | --- |
|
||||
| Root-owned `Input.Destination` and `SourceInfo.Destination`; detached conversions and publication | `TestSourceInfoDestinationConversionsAndCopies`, `TestFacadeFailedLoadExposesDetachedSourceDestination`, and `TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff` verify root/engine conversion, error records, and returned record detachment. `TestPublicFacadeBoundary` and `TestDependencyImportBoundary` verify no engine types cross the facade. |
|
||||
| Literal mapping keys, parsed numeric keys, reserved structural tokens, and invalid-path preflight | `TestNormalizeDestinationUsesLiteralMappingKeys`, `TestNormalizeDestinationRejectsIndicesAndReservedKeys`, and `TestLayeredLoadPreflightsDestinationsBeforeReadingSources` cover mapping-only normalization and rejection before reader consumption; `TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths` verifies the public error boundary. |
|
||||
| Placement of acquired bodies, empty mappings versus absence, imports, all source kinds, physical identity, and authored coordinates | `TestFacadeInputDestinationsAcrossSourceKinds` covers host/native/bytes/reader/mapping sources; `TestFacadeSameFragmentMountsAtSeparateCollectionDestinations` proves independent public collections can retain the same local key. `TestLayeredImportOnlyInputMaterializesDestination`, `TestLayeredHostImportsInheritInputDestinationTransitively`, `TestLayeredNativeFilesystemImportsInheritInputDestination`, and `TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity` cover inherited import placement and unchanged identity. `TestPlaceDocumentWrapsEmptyMappingAndPreservesSourceCoordinates`, `TestPlaceDocumentPreservesAuthoredEntryCoordinates`, and `TestLayeredDestinationDiagnosticsUseEffectivePathsAndAuthoredLocations` distinguish fallback wrapper locations from authored coordinates. |
|
||||
| Strict composition, explicit-wrapper equivalence, precedence, empty values, and domain `version` overrides | `TestLayeredDestinationStrictCompositionBoundaries`, `TestLayeredDestinationMatchesExplicitlyWrappedInput`, `TestEmptyMountedMappingRetainsLowerChildren`, and `TestLayeredDestinationOverlayRetainsOrdinaryValueSemantics` prove existing group and overlay semantics are retained; the latter includes ordinary field-only updates rather than whole-record replacement. |
|
||||
| Main-owned version checks and imported, underlay, inheritance, overlay, CLI, and ancestor protection | `TestLayeredDestinationChecksVersionOnceOnComposedMain`, `TestLayeredDestinationVersionClaimsUsePlacedApplicationPaths`, `TestLayeredImportedVersionClaimsUseEffectiveDestination`, and `TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers` cover effective paths, one main check, protected-path claims/ancestors, and nonclaiming mapping ancestors. `TestLoadVersionDestinationTypesKeepOrdinaryDecoding` preserves exact configured-path conversion and ordinary nested decoding. |
|
||||
| Final-shape sibling inheritance, no cross-collection lookup, one lifecycle, and failure without a usable partial result | `TestLayeredDestinationInheritanceUsesFinalSiblingScopes`, `TestLayeredDestinationInheritanceDoesNotSearchOtherCollections`, `TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape`, and `TestLayeredDestinationDecodeFailureReturnsNoResultOrLaterCallbacks` exercise these behaviors. Reused inheritance, lifecycle, and callback tests cover generic behavior that destination placement does not alter. |
|
||||
| Effective paths, presence, provenance/history, safe rendering, sensitivity, bilateral diff, and source-relative resources | `TestLayeredDestinationsRetainOriginsHistoryAndDetachedRecords`, `TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape`, `TestLayeredDestinationDiffUsesBilateralSensitivity`, `TestLayeredImportedInheritedValuesRetainResourceContexts`, and `TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff` connect mounted values to inspection. The imported-inheritance test specifically proves the inherited value keeps the declaring source's passive resource context. |
|
||||
| Shared depth/node limits and unchanged root/absence accounting | `TestPlaceDocumentRootAndBounds`, `TestDestinationPlacementCountsAgainstSharedNodeBudget`, `TestMultipleMountedInputsSharePlacementCharges`, `TestImportedEmptyBodiesChargeEachDestinationWrapper`, `TestSelectedOverlayAndCLIUsePlacementNodeCharges`, and `TestNoSourceAndRootPlacementAddNoWrapperCharges` cover per-document and aggregate limits without adding a new limit class. |
|
||||
| Maintained consumer usage and strict peer behavior | `Example_inputDestination`, `TestCollectionBundleAssemblesMountedPeersAndResources`, and `TestCollectionPeerPromptConflictsRemainStrict` verify the simple and representative collection examples against actual values, resources, source records, and conflict behavior. |
|
||||
|
||||
The wider baseline-to-candidate review covered all 38 changed files (`6353598..2d8df22`): production changes are limited to the root facade fields/conversions, private input/import assembly and accounting, source record ownership, and the inheritance version-claim correction described above. The contract and tests do not impose identifier grammar, namespace uniqueness, main-only placement, source-read ordering, new budgets, nested version conversion, or expanded secret policy. The implementation wraps only acquired application bodies after processing source-root directives; placement does not change physical source identity, authored node locations, filesystem resolution, or later-tier semantics. Stage evidence above reuses existing general composition, resource, inheritance, version, lifecycle, sensitivity, and boundary tests where those behaviors are unchanged; mounted-specific cases are named separately.
|
||||
|
||||
Completion record: audited source candidate `2d8df22dd1cb9fde8e60853da3e2260731acf2bb`. On `go1.26.8 linux/amd64`, `./scripts/check.sh` passed, including formatting, whitespace, documentation links, tests, vet, and build. `GOTOOLCHAIN=local GOPROXY=off go test -race ./...` also passed with dependency downloads disabled. No remaining implementation limitations were found; release preparation, CI, benchmark campaigns, and downstream upgrades remain separately scoped.
|
||||
|
||||
Done: one complete layered load supports input destinations with coherent values,
|
||||
diagnostics, inspection, limits, and source ownership across the approved surface.
|
||||
|
||||
## Implementation owner map
|
||||
|
||||
Use graph tools to locate these symbols rather than assume line numbers:
|
||||
|
||||
- Public input forwarding: `layered.go`, `records.go`, `conversion.go`
|
||||
(source conversions), and existing facade tests.
|
||||
- Private request and lifecycle: `internal/engine/layered.go`, `load_request.go`
|
||||
(`loadInput`, `acquireUnderlayGroup`, `assembleLayeredInput`), `load.go`
|
||||
(`executeLoadRequest`), and `load_input.go` (`assembleBaseInputTracked`).
|
||||
- Directives and imports: `directives.go` (`splitImportDirectives`,
|
||||
`fragmentVersionClaim`), and `imports.go` (`visitPath`).
|
||||
- Shared document semantics: `compose.go`, `document_walk.go`
|
||||
(`countInputNodes`), `path.go`, `parse.go`, and `value_operation.go` limits.
|
||||
- Inspection: `metadata.go`, `history.go`, `explain.go`, `resource_context.go`,
|
||||
`inheritance_origin.go`, `render.go`, and `diff.go`; source copies live in
|
||||
engine `records.go`. Confirm current filenames through the component overview.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None required for implementation. The four policy choices are approved; this
|
||||
plan selects API spelling, validation timing/error category, coordinate handling,
|
||||
and accounting within them. Surface a newly discovered incompatibility or scope
|
||||
change with evidence rather than resolving it by silently narrowing the feature
|
||||
or strengthening an unrelated contract.
|
||||
@@ -1,171 +0,0 @@
|
||||
# Configuration Input Destinations Roadmap
|
||||
|
||||
Status: implemented. This roadmap records the approved behavior and rationale
|
||||
for the [PromptKit feature request](promptkit-feature-request.md). The
|
||||
[implementation plan](implementation.md) records the completed sequence and
|
||||
audit evidence; the [configuration contract](../config.md#input-destinations)
|
||||
owns current behavior.
|
||||
|
||||
## Targeted end state
|
||||
|
||||
A consumer can assign a mapping destination to a configuration input and perform
|
||||
one ordinary `LoadLayered` call. The input's application content contributes
|
||||
beneath that destination while retaining normal composition, precedence,
|
||||
inheritance, typed loading, diagnostics, and inspection. PromptKit can assemble
|
||||
collections without parsing and rewriting sources, merging values, or rebuilding
|
||||
provenance itself.
|
||||
|
||||
For example, a fragment defining `summarize.alerts` can contribute beneath
|
||||
`collections`, `weatherreporter`, `prompts`. An independent source with the same
|
||||
local key can contribute beneath another collection without conflict. Collection
|
||||
and prompt names remain ordinary application keys, not ConfigKit namespaces.
|
||||
|
||||
The public surface adds an optional `Destination Path` field on `Input`, not a
|
||||
new loading entry point or an operation on completed results. Content-bearing
|
||||
main inputs and underlay inputs support
|
||||
the same destination model; root placement remains the default.
|
||||
|
||||
The following behavior defines the target:
|
||||
|
||||
- Omitted and empty destinations retain root placement. Destinations consist of
|
||||
literal mapping keys, including dots, slashes, and numeric-looking text.
|
||||
Explicit sequence-index destinations are outside the requested capability.
|
||||
- A valid destination on `NoSource` contributes nothing. An actual empty
|
||||
application mapping materializes its destination mappings as explicit input,
|
||||
matching authored wrappers, without clearing lower-tier children.
|
||||
- Existing structural reservations apply to introduced mappings. Destinations
|
||||
that materialize `configkit` or a mapping-valued `extends` contrary to existing
|
||||
contracts fail through the structured loading-error boundary. Keys merely
|
||||
containing those words remain ordinary keys.
|
||||
- Host files, native filesystem files, bytes, readers, and mapping sources all
|
||||
support placement under their existing acquisition and ownership contracts.
|
||||
Filesystem context, import opt-in, and precedence remain independent choices.
|
||||
- Mounted contributions have the same application values and conflict behavior
|
||||
as explicitly wrapped contributions, with accurate original source locations.
|
||||
Disjoint mappings combine; overlapping non-mapping values within a group
|
||||
conflict even when equal. A later tier cannot repair a strict-group conflict.
|
||||
- Different destinations can coexist in one group, and the same destination can
|
||||
occur in different tiers. Ancestor/descendant destinations follow ordinary
|
||||
composition and overlay rules without namespace uniqueness restrictions.
|
||||
Later tiers recursively overlay mappings and replace other values. Empty
|
||||
mappings do not clear lower values; sequences remain atomic.
|
||||
- A prompt's ordinary `version` override changes that field and retains omitted
|
||||
lower fields. It neither identifies a merge record nor replaces a whole prompt.
|
||||
- Source-root `configkit.imports` remains structural. Imported application data
|
||||
inherits the input destination transitively. Import paths still resolve from
|
||||
their declaring source; containment, optionality, cycles, and repeated-source
|
||||
rules remain applicable. Placement does not change physical source identity.
|
||||
- Sibling inheritance runs after assembly in the effective destination scope.
|
||||
Peer fragments can supply a child and its sibling parent. Qualified PromptKit
|
||||
references do not acquire cross-collection inheritance meaning.
|
||||
- Version ownership is enforced at effective application paths. A collection's
|
||||
mounted `version` is ordinary data, while underlay/import contributions to the
|
||||
actual protected path, or replacements of its ancestors, remain invalid.
|
||||
There is one main-owned version check and no blanket nested numeric conversion.
|
||||
- Defaults, decoding, resolution, secret lookup, and validation run once on the
|
||||
assembled application, not independently on fragments. Callbacks and value
|
||||
diagnostics use effective paths. Failures use structured load errors and yield
|
||||
no usable partial result.
|
||||
- Presence, provenance, history, explanations, effective rendering, and diffs
|
||||
agree with the final values. Sensitivity is evaluated at effective application
|
||||
paths under the existing historical and dependency rules.
|
||||
- Existing source records expose placement, including inherited import
|
||||
placement. Introduced ancestor mappings retain the contributing source's
|
||||
identity; any root-location fallback identifies that document, not an authored
|
||||
wrapper. Actual content retains its real source coordinates.
|
||||
- Original source locations and passive resource contexts survive placement,
|
||||
overrides, and inheritance. `Result.SourceContext` and
|
||||
`Resolution.SourceContext` still identify the declaring filesystem and base
|
||||
directory for a field such as `content_file: ./summary.md`.
|
||||
- Destinations and the structure they introduce participate in existing load
|
||||
limits; placement cannot bypass depth or node accounting. Source ownership,
|
||||
complete-on-return loading, and detached result behavior remain intact.
|
||||
|
||||
Import-destination syntax, independent collection specification headers, and
|
||||
cross-collection inheritance are separate possible follow-ups. This work does
|
||||
not add directory discovery, collection installation, remote acquisition,
|
||||
namespace ownership enforcement, dependency solving, live reload, filename-based
|
||||
identity, arbitrary transformations, or new merge strategies. PromptKit owns its
|
||||
collection inventory, qualified IDs, and execution semantics.
|
||||
|
||||
## Implementation baseline (v0.7.1)
|
||||
|
||||
The request reviewed v0.7.1. At that baseline, the relevant boundaries were:
|
||||
|
||||
- [Input](../../layered.go) holds a source, import opt-in, and filesystem context,
|
||||
but no destination. Root types delegate to the engine through the existing
|
||||
[facade boundary](../internal/overview.md).
|
||||
- [Layered loading](../config.md#layered-loading) already provides strict groups,
|
||||
ordered precedence, shared acquisition budgets, and one final lifecycle.
|
||||
[Underlay acquisition](../../internal/engine/load_request.go) gathers source
|
||||
applications and recomposes them into their strict group.
|
||||
- [Input acquisition](../../internal/engine/load_input.go) and
|
||||
[import traversal](../../internal/engine/imports.go) process source-root
|
||||
directives and source identity before final assembly. Imported version claims
|
||||
are checked during traversal; underlay claims are checked before group
|
||||
composition. These currently operate without a destination.
|
||||
- [Version claim checks](../../internal/engine/directives.go) inspect a document
|
||||
using the configured application path. Wrapping only an already acquired
|
||||
result would leave early fragment checks and diagnostic paths incorrect.
|
||||
- [Sibling inheritance](../adr/0009-sibling-inheritance.md) already runs after
|
||||
source precedence and preserves source-to-destination derivation. Placement
|
||||
must feed that existing mechanism rather than resolve fragments independently.
|
||||
- Existing source records identify acquisition and precedence but do not expose
|
||||
input placement. Existing history retains source applications, so moving only
|
||||
the final tree would be insufficient for coherent inspection.
|
||||
|
||||
The architectural fit is an extension of input assembly, not a parser extension
|
||||
or a second configuration model. The
|
||||
[architecture policy](../policy/architecture.md) supplies the ownership and
|
||||
consistency constraints; [deferred scope](deferred.md) continues to govern the
|
||||
unrequested features above.
|
||||
|
||||
## Selected Design Decisions
|
||||
|
||||
### Main-input destinations and absent inputs
|
||||
|
||||
Allow the same destination model for content-bearing main inputs and underlays,
|
||||
with `VersionRule.Path` remaining an effective application path and any
|
||||
specification declaration still owned by main. The checker retains its existing
|
||||
handling of an absent declaration and its existing lifecycle phase. `NoSource`
|
||||
contributes nothing even with a valid destination. This keeps `Input` coherent
|
||||
across its roles and preserves absence without per-fragment version checks.
|
||||
|
||||
### Public placement inspection and synthetic ancestors
|
||||
|
||||
Expose effective placement as `SourceInfo.Destination Path` on existing source
|
||||
records, including inherited import placement.
|
||||
Use original source identity for introduced ancestor mappings, documenting that
|
||||
any root-location fallback identifies the contributing document rather than a
|
||||
physically authored wrapper. Actual content keeps its real coordinates. This
|
||||
allows consumers to connect placement to sources without a new query system.
|
||||
|
||||
Preserve that relationship through inherited fields, composite origins, and the
|
||||
same source selected in separate tiers. Placement is per source application;
|
||||
consumers can inspect it without inferring it from final paths.
|
||||
|
||||
### Reserved structural keys in destinations
|
||||
|
||||
Use existing `Path` conventions for literal keys and parsed
|
||||
numeric tokens, reject explicit index segments, and preserve current structural
|
||||
reservations in the effective tree. Reject destination segments that would
|
||||
materialize `configkit` or a mapping-valued `extends` contrary to those contracts,
|
||||
with a clear structured destination error. Ordinary keys containing those words,
|
||||
dots, or slashes remain ordinary keys. Source-root directives are processed before
|
||||
placement and are not rejected simply because their application body is mounted.
|
||||
|
||||
Mounted and authored wrappers therefore retain the same structural meaning.
|
||||
Destination diagnostics identify placement as the cause of invalid introduced
|
||||
structure, using existing error categories where suitable.
|
||||
|
||||
### Empty mappings and presence of introduced containers
|
||||
|
||||
Mounting an actual empty application mapping materializes its
|
||||
destination mappings as explicit contributions, matching authored wrappers.
|
||||
This preserves the request's equivalence principle and lets consumers inspect
|
||||
an intentionally empty collection. Such mappings still do not clear lower-tier
|
||||
children. An absent source contributes nothing.
|
||||
|
||||
This applies equally to empty imported application bodies and inputs containing
|
||||
only import directives. Presence follows the contribution's meaning rather than
|
||||
whether an implementation happened to allocate wrapper nodes.
|
||||
@@ -1,319 +0,0 @@
|
||||
# PromptKit Feature Request: Configuration Input Destinations
|
||||
|
||||
Status: consumer feature request; not accepted or implemented.
|
||||
|
||||
## Requested Capability
|
||||
|
||||
Provide a public API through which a consumer can select a destination mapping
|
||||
path for each configuration input. The input's application content contributes
|
||||
beneath that path while retaining ConfigKit's normal composition, precedence,
|
||||
inheritance, validation, diagnostics, and inspection behavior.
|
||||
|
||||
PromptKit needs this capability for its proposal 3, **Collections With Mounted
|
||||
Configuration Fragments**, recorded in PromptKit's
|
||||
`docs/roadmap/configuration-collections-mounted-fragments.md`. This request
|
||||
describes the public API needs and observable acceptance criteria. It does not
|
||||
prescribe ConfigKit's internal design or an implementation plan.
|
||||
|
||||
The reviewed public baseline is ConfigKit v0.7.1. The current
|
||||
[layered-loading contract](../config.md#layered-loading) lets consumers choose
|
||||
sources, import policy, filesystem context, and precedence groups, but does not
|
||||
provide an input destination. Existing behavior remains authoritative until an
|
||||
extension is adopted.
|
||||
|
||||
## Consumer Problem
|
||||
|
||||
PromptKit applications assemble independently maintained collections of prompts,
|
||||
inference profiles, and backends. Collections can contain identical local names,
|
||||
such as `summarize.alerts`, without identifying the same resource. PromptKit wants
|
||||
an effective configuration organized by collection, while letting authors omit
|
||||
repeated collection and resource-container wrappers from individual files.
|
||||
|
||||
A file selected from a collection's `prompts` directory would contain:
|
||||
|
||||
```yaml
|
||||
summarize.alerts:
|
||||
version: "1"
|
||||
default_profile: provider/brief
|
||||
messages:
|
||||
- role: user
|
||||
content: Summarize the weather alerts.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
```
|
||||
|
||||
Given destination `[collections, weatherreporter, prompts]`, its contribution
|
||||
would appear in the final application configuration as:
|
||||
|
||||
```yaml
|
||||
collections:
|
||||
weatherreporter:
|
||||
prompts:
|
||||
summarize.alerts:
|
||||
version: "1"
|
||||
default_profile: provider/brief
|
||||
messages:
|
||||
- role: user
|
||||
content: Summarize the weather alerts.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
```
|
||||
|
||||
Another collection can contribute `summarize.alerts` beneath
|
||||
`[collections, homewatch, prompts]` without conflicting. After final handoff,
|
||||
PromptKit interprets these as `weatherreporter/summarize.alerts` and
|
||||
`homewatch/summarize.alerts`. The profile reference is illustrative and must be
|
||||
supplied separately by the application configuration.
|
||||
|
||||
PromptKit will support one installed definition per prompt. Its optional flat
|
||||
`version` is ordinary domain metadata, not a merge identity. This request does
|
||||
not need identified-record sequences, sequence-root documents, or a custom
|
||||
merge policy.
|
||||
|
||||
Authors could write the complete wrappers today, but that repeats assembly
|
||||
details in every fragment. Consumer-side parsing and wrapping would put generic
|
||||
source processing back into PromptKit and complicate source attribution.
|
||||
PromptKit instead wants to select sources and destinations, then receive one
|
||||
final configuration through ConfigKit's supported loading lifecycle.
|
||||
|
||||
## Minimum Public API
|
||||
|
||||
### Per-Input Destination
|
||||
|
||||
Allow a destination to be attached to each `Input` passed to `LoadLayered`.
|
||||
One possible public spelling is an optional `Destination Path` field on `Input`.
|
||||
The name and exact signature are suggestions; equivalent public API designs
|
||||
are welcome.
|
||||
|
||||
Illustrative consumer code using the proposed field, which does not exist in
|
||||
v0.7.1:
|
||||
|
||||
```go
|
||||
promptInput := configkit.Input{
|
||||
Source: configkit.File("/usr/local/share/collections/weatherreporter/prompts/alerts.yml"),
|
||||
Destination: configkit.Keys("collections", "weatherreporter", "prompts"),
|
||||
}
|
||||
|
||||
peers := configkit.Underlay{
|
||||
Inputs: []configkit.Input{promptInput},
|
||||
}
|
||||
```
|
||||
|
||||
The consumer would include `peers` in its existing `LayeredOptions` and invoke
|
||||
`LoadLayered` with its normal root main input and application `Definition`.
|
||||
This is a partial API sketch, not an executable example or a proposed new
|
||||
loading entry point. A single public layered load is sufficient for PromptKit;
|
||||
it does not require a separate mounting operation on completed results.
|
||||
|
||||
The API must let the consumer:
|
||||
|
||||
- Express a destination relative to the application configuration root using
|
||||
literal mapping-key segments. Dots, slashes, and numeric-looking text inside
|
||||
a key are not implicit path separators or array indexes. PromptKit's namespace
|
||||
grammar remains its own domain rule.
|
||||
- Omit the destination to retain existing root placement. An explicitly empty
|
||||
destination should have the same meaning. Mapping destinations are sufficient;
|
||||
sequence-index destinations are not needed by this request.
|
||||
- Assign different destinations to inputs in the same strict composition group,
|
||||
and assign the same destination to inputs in separate precedence groups.
|
||||
- Use the destination with existing content-bearing source kinds: host files,
|
||||
native filesystem files, bytes, readers, and mapping sources, subject to their
|
||||
existing acquisition and ownership contracts.
|
||||
- Retain the existing import opt-in and filesystem context independently of the
|
||||
destination. A configuration destination is not a filesystem root or base
|
||||
directory.
|
||||
|
||||
PromptKit can continue to enumerate an explicitly selected directory and attach
|
||||
the same destination to its returned inputs. ConfigKit does not need a new
|
||||
directory scanner, namespace registry, filename convention, or PromptKit schema.
|
||||
The namespace comes from the application's assembly declaration, not from an
|
||||
implicit interpretation of a directory basename.
|
||||
|
||||
### Representative Assembly
|
||||
|
||||
These are application choices, not ConfigKit-specific reserved paths:
|
||||
|
||||
| Selected inputs | Destination key segments |
|
||||
| --- | --- |
|
||||
| Application main document | Root |
|
||||
| `weatherreporter/collection.yml` | `collections`, `weatherreporter` |
|
||||
| YAML files under `weatherreporter/prompts` | `collections`, `weatherreporter`, `prompts` |
|
||||
| YAML files under `homewatch/prompts` | `collections`, `homewatch`, `prompts` |
|
||||
| YAML files under `provider/profiles` | `collections`, `provider`, `profiles` |
|
||||
| YAML files under `provider/backends` | `collections`, `provider`, `backends` |
|
||||
|
||||
Peer collection contributions belong to a single strict underlay group when the
|
||||
application wants conflict detection among them. Intentional operator overrides
|
||||
use later groups or main. Destination selection must not establish precedence
|
||||
or make source traversal order authorize replacement.
|
||||
|
||||
## Required Observable Behavior
|
||||
|
||||
### Composition And Precedence
|
||||
|
||||
Mounted and explicitly wrapped contributions must have equivalent application
|
||||
values and conflict behavior at their effective destinations. Actual source
|
||||
locations naturally differ and must remain accurate.
|
||||
|
||||
Retain the [strict composition](../config.md#strict-document-composition) and
|
||||
[layering](../config.md#layered-loading) rules:
|
||||
|
||||
- Disjoint mapping entries at the same destination combine.
|
||||
- Overlapping non-mapping values within a strict group conflict, including equal
|
||||
scalars, nulls, and sequences. Different destinations do not collide merely
|
||||
because the source documents share a local key.
|
||||
- Later precedence layers recursively override mappings and replace other
|
||||
values. A strict conflict cannot be repaired by an upper layer.
|
||||
- Empty mappings do not clear lower definitions; ordinary arrays remain atomic.
|
||||
In particular, `messages` does not acquire sequence-merging behavior.
|
||||
- Overlapping destination paths and contributions at their ancestors follow
|
||||
those same rules. No namespace-level uniqueness restriction is requested.
|
||||
|
||||
Changing only a prompt's `version` in a later layer changes that field while
|
||||
retaining omitted lower-layer fields. ConfigKit must not treat a domain version
|
||||
label as a request to replace an entire definition.
|
||||
|
||||
### Imports And Existing Structural Directives
|
||||
|
||||
An input with imports enabled must be able to use its existing source-root
|
||||
`configkit.imports` directive while contributing application data at a nested
|
||||
destination. The directive remains structural configuration, not an application
|
||||
field below that destination.
|
||||
|
||||
For the minimum API, imports inherit their importing input's destination,
|
||||
including transitive imports. Import paths continue to resolve relative to
|
||||
their declaring source's filesystem context. A mounted document must not send
|
||||
its imported application fields back to the configuration root accidentally.
|
||||
|
||||
Existing opt-in, containment, missing-input, cycle, and repeated-source behavior
|
||||
must remain applicable. A different destination is not a request to bypass the
|
||||
current rejection of a repeated physical source within a strict group.
|
||||
|
||||
Sibling `extends` must resolve against mappings at their effective destinations.
|
||||
For example, two independently supplied profile fragments mounted beneath the
|
||||
same collection's `profiles` mapping can establish `extends: brief` once they
|
||||
compose. Inheritance remains ConfigKit-owned; PromptKit does not perform another
|
||||
relationship-resolution pass.
|
||||
|
||||
### Protected Specification Paths
|
||||
|
||||
The distinction between source-local field names and effective application
|
||||
paths is essential. PromptKit's main document will declare `version: 1` at the
|
||||
configuration root. A separate collection metadata file can contain:
|
||||
|
||||
```yaml
|
||||
version: "2.4.0"
|
||||
```
|
||||
|
||||
Mounted at `[collections, weatherreporter]`, that field is an ordinary collection
|
||||
release label. It must not be rejected as an attempt to contribute the protected
|
||||
root specification field. The same applies to a prompt's nested optional
|
||||
version. Only the main-owned specification declaration uses the application's
|
||||
configured [VersionRule](../config.md#application-schema-versions).
|
||||
|
||||
Conversely, placement must not permit an underlay or imported fragment to claim
|
||||
or replace the actual protected specification path or an ancestor that would
|
||||
replace it. The public contract must define protection in terms of effective
|
||||
application paths, rather than reserving the spelling `version` in every source.
|
||||
Protection must remain valid with imports, inheritance, overlays, and explicit
|
||||
CLI overrides.
|
||||
|
||||
PromptKit needs one main-owned specification check with the existing lifecycle.
|
||||
It does not need main mounted away from the root, integer-to-string conversion
|
||||
for every nested `version`, or separate compatibility checks for every fragment.
|
||||
|
||||
### Typed Loading, Diagnostics, And Inspection
|
||||
|
||||
The destination must be visible consistently through the ordinary public
|
||||
loading and inspection surface:
|
||||
|
||||
- Final typed configuration and application callbacks use effective destination
|
||||
paths. Defaults, resolution, and validation retain the existing single-load
|
||||
lifecycle; fragments are not independently validated as whole applications.
|
||||
- Metadata and presence queries distinguish omission, explicit null, and other
|
||||
explicit values at the mounted paths.
|
||||
- Diagnostics identify the effective conflicting or invalid path and the real
|
||||
contributing source locations. Consumers can determine which source and
|
||||
placement produced a value without recreating that relationship themselves.
|
||||
- Effective rendering, explanations, history, and configuration comparison agree
|
||||
with the final configuration. Existing sensitivity and disclosure rules apply
|
||||
to mounted fields, including application path-based secret classification.
|
||||
- Unsupported destination forms and destination-related loading failures use
|
||||
the normal structured error boundary. Exact new diagnostic codes, if needed,
|
||||
are for ConfigKit to define.
|
||||
|
||||
PromptKit does not require a separate mount registry or new inspection facade.
|
||||
Existing inspection APIs are sufficient if they expose accurate effective paths,
|
||||
source attribution, and placement information. ConfigKit should document how
|
||||
source-local locations relate to effective paths.
|
||||
|
||||
### Source-Relative Resources And Lifecycle
|
||||
|
||||
For a mounted field such as `content_file: ./summary.md`, obtaining its effective
|
||||
origin and calling `Result.SourceContext` or `Resolution.SourceContext` must
|
||||
still identify the declaring source's filesystem and base directory. The same
|
||||
holds when that field survives an override or is inherited into another mapping.
|
||||
Its configuration destination must not change the meaning of the relative file
|
||||
reference.
|
||||
|
||||
Retain existing source ownership, load completion, result lifetime, containment,
|
||||
and resource-access behavior. Destinations must participate in documented load
|
||||
limits without creating a way to bypass them; this request does not prescribe
|
||||
new limit values or their accounting machinery.
|
||||
|
||||
## Optional Follow-Ups, Not Prerequisites
|
||||
|
||||
**Import destinations in YAML.** A destination on an import directive could let
|
||||
publishers describe assembly in configuration rather than Go. Proposal 3
|
||||
recommends destinations relative to the importing document's destination, with
|
||||
omission retaining that destination. This is useful, but per-input placement
|
||||
through the public API is sufficient for initial PromptKit adoption. No new
|
||||
directive spelling is selected by this request.
|
||||
|
||||
**Independent specification headers.** A collection entry document might later
|
||||
declare its own supported specification version. That needs a distinct contract
|
||||
for checking the header and determining its role in the composed configuration.
|
||||
Mounting collection release metadata does not supply this capability. Keep it a
|
||||
separate request if PromptKit decides it is necessary.
|
||||
|
||||
**Cross-collection inheritance.** Nested collection profiles are not immediate
|
||||
siblings across collections. Current sibling inheritance remains sufficient for
|
||||
the initial proposal, with authored definitions flattened where necessary.
|
||||
Generic cross-path inheritance could be considered separately if consumer needs
|
||||
justify revisiting the [deferred scope](deferred.md). Mounting must not silently
|
||||
reinterpret `extends: provider/brief` as a cross-collection reference.
|
||||
|
||||
PromptKit also does not request collection installation, automatic directory
|
||||
discovery, remote fetching, namespace ownership enforcement, dependency solving,
|
||||
live reload, filename-derived identity, or arbitrary document transformations.
|
||||
Its qualified resource IDs, optional prompt version semantics, collection
|
||||
inventory, and execution APIs remain PromptKit responsibilities.
|
||||
|
||||
## Public Acceptance Scenarios
|
||||
|
||||
Evaluate the extension through supported public loading and inspection APIs:
|
||||
|
||||
| Scenario | Expected consumer-visible result |
|
||||
| --- | --- |
|
||||
| Destination omitted or empty | Existing root-placement behavior |
|
||||
| Same fragment shape mounted under two distinct collection keys from independent sources | Two separate effective resources |
|
||||
| Disjoint inputs at the same destination in one group | Mapping contributions combine |
|
||||
| Equal or unequal overlapping scalar fields in one group | Structured composition conflict with both source locations |
|
||||
| Same destination across deliberate precedence tiers | Existing recursive overlay behavior |
|
||||
| Version-only prompt override | Label changes; omitted prompt body fields survive |
|
||||
| Nested mounted import without a destination directive | Imported fields contribute at the inherited destination |
|
||||
| Local profile inheritance across mounted peer fragments | Parent resolves at the effective sibling scope |
|
||||
| Collection metadata containing `version` mounted below root | Ordinary nested field, independent of root VersionRule |
|
||||
| Mounted/imported contribution actually targeting the protected version path | Rejected under version ownership rules |
|
||||
| Mounted field using source-relative content, including inheritance | Original declaring resource context remains available |
|
||||
| Mounted explicit null and omitted field | Distinct effective presence information |
|
||||
| Mounted validation error or secret field | Effective paths, physical locations, and existing disclosure rules agree |
|
||||
| File, native filesystem, byte, reader, or mapping input with equivalent content | Equivalent application values, subject to existing source contracts |
|
||||
| Unsupported destination or failed load | Normal structured failure, no usable partial result |
|
||||
|
||||
The minimum usable outcome is a consumer that selects collection sources and
|
||||
destinations, performs one ordinary layered load, and receives final typed
|
||||
configuration with reliable diagnostics and inspection. It should need no
|
||||
consumer-side source rewriting, generic merging, or provenance reconstruction.
|
||||
@@ -1,89 +0,0 @@
|
||||
# Input destination review findings
|
||||
|
||||
Reviewed `d3d468a..945e111` against the approved
|
||||
[input-destinations roadmap](input-destinations.md),
|
||||
[implementation plan](implementation.md), and
|
||||
[ADR 0011](../adr/0011-input-destinations.md).
|
||||
|
||||
The root-owned destination fields, placement before strict composition,
|
||||
transitive import placement, effective-path version ownership, final lifecycle,
|
||||
source records, resource contexts, retained history, and existing disclosure
|
||||
mechanisms broadly match the selected architecture. The required correction
|
||||
below concerns an early diagnostic path, not loading or composition semantics.
|
||||
No additional unintended strengthening of contracts was identified.
|
||||
|
||||
## 1. Rebase known Mapping-source failure paths to the input destination (P2)
|
||||
|
||||
Status: complete. Known Mapping value-builder paths are now prefixed with the
|
||||
normalized destination at acquisition, preserving semantic key/index segments.
|
||||
Public regressions cover mounted main and underlay failures, escaped keys,
|
||||
sequence indices, cyclic values, unchanged root paths, and source-constructor
|
||||
errors retaining source coordinates. The mounted value cases failed before the
|
||||
fix and pass afterward. Existing directive/syntax checks remain unchanged.
|
||||
Focused destination/source/diagnostic/import tests, `./scripts/check.sh`, and
|
||||
`GOTOOLCHAIN=local GOPROXY=off go test -race ./...` passed on
|
||||
`go1.26.8 linux/amd64`.
|
||||
|
||||
Affected: `assembleBaseInputTracked` in
|
||||
[load_input.go](../../internal/engine/load_input.go), lines 27–32 at the reviewed
|
||||
commit; the Mapping failure originates in `readSource` in
|
||||
[source.go](../../internal/engine/source.go), lines 206–208.
|
||||
|
||||
`Mapping` snapshots its input through the existing value builder. When a nested
|
||||
value is unsupported, cyclic, or exceeds a value-builder limit, the retained
|
||||
issue already identifies a semantic path inside that application mapping.
|
||||
`readSource` turns it into a structured diagnostic. The early return from
|
||||
`assembleBaseInputTracked` annotates the source record with its destination but
|
||||
returns those diagnostic paths unchanged.
|
||||
|
||||
For example, select this input as either main or an underlay:
|
||||
|
||||
```go
|
||||
configkit.Input{
|
||||
Source: configkit.Mapping("bad mapping", map[string]any{
|
||||
"item": map[string]any{"bad": func() {}},
|
||||
}),
|
||||
Destination: configkit.Keys("catalog", "entries"),
|
||||
}
|
||||
```
|
||||
|
||||
The load correctly fails with `source.invalid`, but `Diagnostic.Path` is
|
||||
`/item/bad` rather than `/catalog/entries/item/bad`. The source record correctly
|
||||
reports `/catalog/entries`, so the two inspection records use inconsistent
|
||||
application coordinates. A consumer displaying or associating the failure with
|
||||
the final application shape would point to the wrong configuration field.
|
||||
|
||||
This diverges from implementation decision 10 and Stage 6: existing paths that
|
||||
genuinely identify application content must be translated to effective paths
|
||||
exactly once. A Mapping value-builder issue has that information; it is not a
|
||||
source-read or YAML syntax failure for which a logical child path is unavailable.
|
||||
|
||||
Required remediation:
|
||||
|
||||
- Prefix known application-relative Mapping value-builder diagnostic paths with
|
||||
the normalized input destination at the acquisition boundary. Preserve key
|
||||
escaping and sequence-index intent in the existing semantic Path model.
|
||||
- Keep source IDs, original primary/related locations, codes, value-free
|
||||
messages, source destinations, and nil-result behavior unchanged. Root
|
||||
placement must retain its current paths.
|
||||
- Do not blanket-prefix parse/read failures, source-constructor errors, or
|
||||
structural import-directive diagnostics. Do not invent logical child paths
|
||||
for failures that do not identify application content.
|
||||
- Add bounded regression coverage through the public API for mounted main and
|
||||
underlay Mapping failures, alongside unchanged root placement. Reuse existing
|
||||
syntax/directive tests to protect their different coordinate convention.
|
||||
- Run the focused destination/source/diagnostic checks and the everyday check.
|
||||
Reconcile the completed plan's diagnostic evidence with this correction;
|
||||
do not redefine the agreed effective-path behavior to accept the gap.
|
||||
|
||||
## Review validation
|
||||
|
||||
- `GOTOOLCHAIN=local ./scripts/check.sh` passed at `945e111` on
|
||||
`go1.26.8 linux/amd64` (formatting, whitespace, tests, vet, and build).
|
||||
- `GOTOOLCHAIN=local GOPROXY=off go test -race ./...` passed.
|
||||
- `GOTOOLCHAIN=local GOPROXY=off go test -count=1 ./...` passed.
|
||||
- An additional external-package probe in a disposable checkout outside the
|
||||
repository failed for both main and underlay placement, observing `/item/bad`
|
||||
instead of `/catalog/entries/item/bad` with `source.invalid`.
|
||||
- No production fixes or regression tests were added to this repository during
|
||||
the review. No performance measurements or new guarantees were inferred.
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func ExampleLoad() {
|
||||
@@ -19,6 +19,28 @@ func ExampleLoad() {
|
||||
// Output: {true 3} <nil>
|
||||
}
|
||||
|
||||
func ExampleLoad_sequenceDeclaration() {
|
||||
type Item struct {
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
type Config struct {
|
||||
Items []Item `yaml:"items"`
|
||||
}
|
||||
result, err := configkit.Load[Config](configkit.Bytes("catalog.yml", []byte(`config:
|
||||
sequences:
|
||||
- destination: [items]
|
||||
key: id
|
||||
items:
|
||||
- id: prompt
|
||||
`)))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result.Config.Items[0].ID)
|
||||
// Output: prompt
|
||||
}
|
||||
|
||||
func ExampleNoSource() {
|
||||
type Config struct {
|
||||
Workers int `yaml:"workers"`
|
||||
@@ -154,10 +176,18 @@ func ExampleOverride() {
|
||||
})
|
||||
}
|
||||
result, err := configkit.Load(configkit.NoSource(), configkit.Options[Config]{
|
||||
Definition: configkit.Definition[Config]{Defaults: func() Config { return Config{Workers: 4} }},
|
||||
Overrides: overrides,
|
||||
Definition: configkit.Definition[Config]{
|
||||
Defaults: func() Config { return Config{Workers: 4} },
|
||||
Resolve: func(configuration *Config, resolution *configkit.Resolution) error {
|
||||
configkit.Default(resolution, configkit.Keys("workers"), &configuration.Workers, 100,
|
||||
"preserve an explicitly supplied worker count")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Overrides: overrides,
|
||||
})
|
||||
fmt.Println(result.Config.Workers, result.Sources()[0].Label, err)
|
||||
source, _ := result.Source("source-1")
|
||||
fmt.Println(result.Config.Workers, source.Label, err)
|
||||
// Output: 0 --workers <nil>
|
||||
}
|
||||
|
||||
|
||||
26
examples/consumers/config_controls_test.go
Normal file
26
examples/consumers/config_controls_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func Example_documentRootConfig() {
|
||||
type app struct {
|
||||
Config struct {
|
||||
Timeout string `yaml:"timeout"`
|
||||
} `yaml:"config"`
|
||||
}
|
||||
files := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: settings.yml, destination: [config]}]}\n")},
|
||||
"settings.yml": {Data: []byte("timeout: 10s\n")},
|
||||
})
|
||||
result, err := configkit.Load[app](files.File("main.yml"), configkit.Options[app]{Imports: true})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(result.Config.Config.Timeout)
|
||||
// Output: 10s
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// Artifact is a Narratio-shaped generated output declaration.
|
||||
@@ -167,7 +167,7 @@ func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Overri
|
||||
resolveNotariusReference(configuration, resolution, invocationDir)
|
||||
return nil
|
||||
},
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func resolveNotariusReference(configuration *NotariusPathConfig, resolution *con
|
||||
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
|
||||
return
|
||||
}
|
||||
info, found := resolution.InputInfo(path)
|
||||
info, found := resolution.EntryInfo(path)
|
||||
if !found {
|
||||
resolution.Warning(path, "relative reference has no entry provenance")
|
||||
return
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_narratio() {
|
||||
@@ -82,7 +82,7 @@ func Example_weatherStartup() {
|
||||
defer os.RemoveAll(directory)
|
||||
_ = os.WriteFile(filepath.Join(directory, blockedName), []byte("unused-example-value\n"), 0o600)
|
||||
_ = os.WriteFile(filepath.Join(directory, tokenName), []byte("disposable-example-credential\n"), 0o600)
|
||||
report, bootstrapErr := configkit.LoadSecretsDir(directory)
|
||||
report, bootstrapErr := configkit.LoadSecretsIntoEnvironment(directory)
|
||||
var reportOutput bytes.Buffer
|
||||
_, reportWriteErr := report.WriteTo(&reportOutput)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// CollectionBundleConfig is a representative application-owned collection
|
||||
@@ -43,9 +43,9 @@ type CollectionProfile struct {
|
||||
}
|
||||
|
||||
// LoadCollectionBundle demonstrates one layered collection load. Its main
|
||||
// document owns the application schema version; imported collection metadata,
|
||||
// prompt/profile peers, and an operator prompt-version override use ordinary
|
||||
// destinations and domain fields.
|
||||
// document owns the application schema version; the mounted entry declares
|
||||
// relative destinations for collection metadata, prompt/profile peers, and
|
||||
// their nested imports. The operator version override remains a separate tier.
|
||||
func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
|
||||
files, err := fs.Sub(bundledFiles, "testdata/collection-bundle")
|
||||
if err != nil {
|
||||
@@ -54,7 +54,6 @@ func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
|
||||
bundle := configkit.NewFilesystem("collection bundle", files)
|
||||
collectionDestination := configkit.Keys("collections", "weatherreporter")
|
||||
promptDestination := collectionDestination.Key("prompts")
|
||||
profileDestination := collectionDestination.Key("profiles")
|
||||
return configkit.LoadLayered(
|
||||
configkit.Input{Source: configkit.Bytes("application main", []byte("version: 1\n"))},
|
||||
configkit.LayeredOptions[CollectionBundleConfig]{
|
||||
@@ -69,13 +68,9 @@ func LoadCollectionBundle() (*configkit.Result[CollectionBundleConfig], error) {
|
||||
Validate: validateCollectionBundle,
|
||||
},
|
||||
Underlays: []configkit.Underlay{
|
||||
{Inputs: []configkit.Input{
|
||||
{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{{
|
||||
Source: bundle.File("collections/weather/collection.yml"), Imports: true, Destination: collectionDestination,
|
||||
}}},
|
||||
{Inputs: []configkit.Input{{
|
||||
Source: bundle.File("operators/weather-prompt-version.yml"), Destination: promptDestination,
|
||||
}}},
|
||||
@@ -91,7 +86,7 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
||||
continue
|
||||
}
|
||||
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
||||
info, found := resolution.InputInfo(contentPath)
|
||||
info, found := resolution.EntryInfo(contentPath)
|
||||
if !found || len(info.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("prompt resource has no single declaring source")
|
||||
}
|
||||
@@ -109,9 +104,9 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
||||
}
|
||||
prompt.ResourceText = string(data)
|
||||
collection.Prompts[promptID] = prompt
|
||||
resolution.Record(
|
||||
resolution.RecordNormalization(
|
||||
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
|
||||
configkit.ChangeNormalization, "read source-relative prompt content", contentPath,
|
||||
"read source-relative prompt content", contentPath,
|
||||
)
|
||||
}
|
||||
configuration.Collections[collectionID] = collection
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_inputDestination() {
|
||||
@@ -60,7 +60,7 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
|
||||
}
|
||||
|
||||
versionInfo, ok := result.Metadata().Lookup(configkit.Keys("version"))
|
||||
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerBase || len(versionInfo.Origin.Locations) != 1 {
|
||||
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerMain || len(versionInfo.Origin.Locations) != 1 {
|
||||
t.Fatalf("main-owned version metadata = %#v, %v", versionInfo, ok)
|
||||
}
|
||||
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")
|
||||
@@ -69,22 +69,43 @@ func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
|
||||
t.Fatalf("ordinary collection version metadata = %#v, %v", collectionVersion, ok)
|
||||
}
|
||||
var metadataSource configkit.SourceInfo
|
||||
var collectionEntrySource configkit.SourceInfo
|
||||
var versionSource configkit.SourceInfo
|
||||
var parentPromptSource configkit.SourceInfo
|
||||
var promptIndexSource configkit.SourceInfo
|
||||
var profileIndexSource configkit.SourceInfo
|
||||
var promptVersionSource configkit.SourceInfo
|
||||
for _, source := range result.Sources() {
|
||||
switch source.Path {
|
||||
case "collections/weather/collection.yml":
|
||||
collectionEntrySource = source
|
||||
case "collections/weather/metadata.yml":
|
||||
metadataSource = source
|
||||
case "collections/weather/version.yml":
|
||||
versionSource = source
|
||||
case "collections/weather/prompts/index.yml":
|
||||
promptIndexSource = source
|
||||
case "collections/weather/prompts/brief.yml":
|
||||
parentPromptSource = source
|
||||
case "collections/weather/profiles/index.yml":
|
||||
profileIndexSource = source
|
||||
case "operators/weather-prompt-version.yml":
|
||||
promptVersionSource = source
|
||||
}
|
||||
}
|
||||
collectionVersionLocation := collectionVersion.Origin.Locations[0]
|
||||
if metadataSource.ID == "" || collectionVersionLocation.SourceID != metadataSource.ID ||
|
||||
metadataSource.Destination.String() != "/collections/weatherreporter" || collectionVersionLocation.Line != 1 {
|
||||
t.Fatalf("imported collection version source = %#v, %#v", metadataSource, collectionVersionLocation)
|
||||
if collectionEntrySource.ID != "source-2" || collectionEntrySource.Destination.String() != "/collections/weatherreporter" ||
|
||||
metadataSource.ID == "" || metadataSource.Destination.String() != "/collections/weatherreporter" ||
|
||||
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 {
|
||||
t.Fatalf("prompt version source = %#v", promptVersionSource)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// FileComparisonConfig is a small application-owned shape for comparing
|
||||
@@ -101,8 +101,8 @@ func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resoluti
|
||||
url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port)
|
||||
if endpoint.URL != url {
|
||||
endpoint.URL = url
|
||||
resolution.Record(configkit.Keys("service", "endpoint", "url"),
|
||||
configkit.ChangeNormalization, "derived endpoint URL",
|
||||
resolution.RecordNormalization(configkit.Keys("service", "endpoint", "url"),
|
||||
"derived endpoint URL",
|
||||
configkit.Keys("service", "endpoint", "host"),
|
||||
configkit.Keys("service", "endpoint", "port"))
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func ExampleCompareFiles() {
|
||||
|
||||
46
examples/consumers/discovery_test.go
Normal file
46
examples/consumers/discovery_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package consumers_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
type workingDirectoryPresetConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
func Example_workingDirectoryPreset() {
|
||||
original, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.Chdir(original) }()
|
||||
root, err := os.MkdirTemp("", "configkit-working-search-")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
if err := os.Chdir(root); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "service.yml"), []byte("name: example\n"), 0o600); err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
result, err := configkit.Load[workingDirectoryPresetConfig](
|
||||
configkit.SearchConfig("service", configkit.WorkingDirectory()),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
source, _ := result.Source("source-1")
|
||||
fmt.Println(result.Config.Name, filepath.Base(source.Path))
|
||||
// Output: example service.yml
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package consumers
|
||||
|
||||
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
import configkit "go.maximumdirect.net/configkit"
|
||||
|
||||
// PresetCatalog is ordinary application data; no inheritance fields or
|
||||
// registration hooks are required in the typed configuration.
|
||||
|
||||
@@ -3,8 +3,8 @@ package consumers_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"io/fs"
|
||||
"path"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/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 testdata/source-version
|
||||
var bundledFiles embed.FS
|
||||
|
||||
// BundledConfig is a small configuration loaded directly from an embedded file.
|
||||
@@ -18,6 +18,30 @@ type BundledConfig struct {
|
||||
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.
|
||||
func LoadBundled() (*configkit.Result[BundledConfig], error) {
|
||||
files, err := fs.Sub(bundledFiles, "testdata/native-source")
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_nativeFilesystem() {
|
||||
@@ -20,6 +20,34 @@ func Example_nativeFilesystem() {
|
||||
// Output: embedded false
|
||||
}
|
||||
|
||||
func Example_rootRelativeImports() {
|
||||
result, err := consumers.LoadPromptBundle()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
source, _ := result.Source("source-3")
|
||||
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() {
|
||||
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
|
||||
if err != nil {
|
||||
@@ -32,6 +60,58 @@ func Example_layeredCatalog() {
|
||||
// Output: production false 0 bundle example Welcome from the bundle. <nil>
|
||||
}
|
||||
|
||||
func Example_sourceVersionAdoption() {
|
||||
result, err := consumers.LoadVersionedService(configkit.SourceVersionModeIfPresent)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
checks, redacted := 0, true
|
||||
for _, source := range result.Sources() {
|
||||
if check := source.VersionEvidence; check != nil {
|
||||
checks++
|
||||
redacted = redacted && check.Redacted && check.Version.Text == ""
|
||||
}
|
||||
}
|
||||
required, requiredErr := consumers.LoadVersionedService(configkit.SourceVersionModeRequired)
|
||||
fmt.Println(result.Config.Service.Version, result.Config.Service.Metadata.Release,
|
||||
checks, redacted, required != nil && requiredErr == nil)
|
||||
// Output: service-v2 application-2026.10 2 true true
|
||||
}
|
||||
|
||||
func TestVersionedServiceModesAndMountedProtection(t *testing.T) {
|
||||
for _, mode := range []configkit.SourceVersionMode{
|
||||
configkit.SourceVersionModeIfPresent,
|
||||
configkit.SourceVersionModeRequired,
|
||||
} {
|
||||
result, err := consumers.LoadVersionedService(mode)
|
||||
if err != nil || result == nil || result.Config.Service.Version != "service-v2" ||
|
||||
result.Config.Service.Name != "example service" || result.Config.Service.Endpoint != "https://service.example.invalid" ||
|
||||
result.Config.Service.Metadata.Release != "application-2026.10" {
|
||||
t.Fatalf("versioned service mode %d = %#v, %v", mode, result, err)
|
||||
}
|
||||
sources := result.Sources()
|
||||
if len(sources) != 2 {
|
||||
t.Fatalf("versioned service sources = %#v", sources)
|
||||
}
|
||||
for _, source := range sources {
|
||||
check := source.VersionEvidence
|
||||
if check == nil || check.Path.String() != "/version" || !check.Version.Present ||
|
||||
!check.Redacted || check.Version.Text != "" {
|
||||
t.Fatalf("safe source version evidence = %#v", sources)
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err := consumers.LoadVersionedService(configkit.SourceVersionModeIfPresent,
|
||||
configkit.Override{Path: configkit.Keys("service", "version"), Value: "cli-v3", Flag: "--service-version"})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 ||
|
||||
loadError.Diagnostics()[0].Code != configkit.CodeOverrideVersion ||
|
||||
loadError.Diagnostics()[0].Path.String() != "/service/version" {
|
||||
t.Fatalf("mounted main version override error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
|
||||
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
|
||||
if err != nil {
|
||||
@@ -39,7 +119,7 @@ func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) {
|
||||
}
|
||||
for _, key := range []string{"enabled", "workers"} {
|
||||
info, found := result.Metadata().Lookup(configkit.Keys(key))
|
||||
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerBase {
|
||||
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerMain {
|
||||
t.Fatalf("%s presence = %#v, %v", key, info, found)
|
||||
}
|
||||
}
|
||||
|
||||
67
examples/consumers/prompt_catalog.go
Normal file
67
examples/consumers/prompt_catalog.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// PromptCatalogConfig is an application-owned prompt collection shape.
|
||||
type PromptCatalogConfig struct {
|
||||
Prompts []PromptRecord `yaml:"prompts"`
|
||||
}
|
||||
|
||||
// PromptRecord keeps its identity and message meaning in the consumer.
|
||||
type PromptRecord struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Messages []string `yaml:"messages"`
|
||||
Resource string `yaml:"resource"`
|
||||
}
|
||||
|
||||
// LoadPromptCatalog discovers explicitly registered prompt records in the main
|
||||
// tier and replaces the lower-priority default collection as a whole.
|
||||
func LoadPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig], error) {
|
||||
return configkit.LoadLayered(configkit.Input{
|
||||
Source: configkit.File(mainPath),
|
||||
Imports: true,
|
||||
}, configkit.LayeredOptions[PromptCatalogConfig]{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
||||
Source: configkit.Bytes("default prompts", []byte("prompts:\n - id: default\n name: Default prompt\n messages: [Default message]\n")),
|
||||
}}}},
|
||||
ImportDefaults: configkit.ImportDefaults{
|
||||
Pattern: []string{"curated*.yml", "generated-*.yml"},
|
||||
},
|
||||
Definition: promptCatalogDefinition(),
|
||||
})
|
||||
}
|
||||
|
||||
// LoadRequiredPromptCatalog keeps the common one-file import shape small.
|
||||
func LoadRequiredPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig], error) {
|
||||
return configkit.Load(configkit.File(mainPath), configkit.Options[PromptCatalogConfig]{
|
||||
Imports: true, Definition: promptCatalogDefinition(),
|
||||
})
|
||||
}
|
||||
|
||||
func promptCatalogDefinition() configkit.Definition[PromptCatalogConfig] {
|
||||
return configkit.Definition[PromptCatalogConfig]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
},
|
||||
Validate: validatePromptCatalog,
|
||||
}
|
||||
}
|
||||
|
||||
func validatePromptCatalog(configuration *PromptCatalogConfig, validation *configkit.Validation) {
|
||||
for index, prompt := range configuration.Prompts {
|
||||
if strings.TrimSpace(prompt.Name) == "" {
|
||||
validation.Error(configkit.Keys("prompts").Index(index).Key("name"), "must not be empty")
|
||||
}
|
||||
if len(prompt.Messages) == 0 {
|
||||
validation.Error(configkit.Keys("prompts").Index(index).Key("messages"), "must contain at least one message")
|
||||
}
|
||||
}
|
||||
if len(configuration.Prompts) == 0 {
|
||||
validation.Error(configkit.Keys("prompts"), "must contain at least one prompt")
|
||||
}
|
||||
}
|
||||
135
examples/consumers/prompt_catalog_test.go
Normal file
135
examples/consumers/prompt_catalog_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package consumers_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
pathpkg "path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_identifiedPromptCatalog() {
|
||||
result, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "config.yml"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(result.Config.Prompts))
|
||||
for _, prompt := range result.Config.Prompts {
|
||||
ids = append(ids, prompt.ID)
|
||||
}
|
||||
emptySearches := 0
|
||||
for _, source := range result.Sources() {
|
||||
for _, selection := range source.EmptyImportSelections {
|
||||
if selection.Selector == configkit.ImportSelectorDirectory {
|
||||
emptySearches++
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(ids, emptySearches, err)
|
||||
// Output: [weather curated welcome main] 1 <nil>
|
||||
}
|
||||
|
||||
func Example_requiredPromptCatalog() {
|
||||
result, err := consumers.LoadRequiredPromptCatalog(fixture("prompt-catalog", "simple.yml"))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result.Config.Prompts[0].ID, err)
|
||||
// Output: required <nil>
|
||||
}
|
||||
|
||||
func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
||||
result, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "config.yml"))
|
||||
if err != nil {
|
||||
var loadError *configkit.LoadError
|
||||
if errors.As(err, &loadError) {
|
||||
t.Fatalf("load prompt catalog: %v (%#v)", err, loadError.Diagnostics())
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Config.Prompts) != 4 {
|
||||
t.Fatalf("prompt records = %#v", result.Config.Prompts)
|
||||
}
|
||||
ids := []string{"weather", "curated", "welcome", "main"}
|
||||
for index, id := range ids {
|
||||
if result.Config.Prompts[index].ID != id {
|
||||
t.Fatalf("prompt %d ID = %q, want %q", index, result.Config.Prompts[index].ID, id)
|
||||
}
|
||||
}
|
||||
for _, prompt := range result.Config.Prompts {
|
||||
if prompt.ID == "default" {
|
||||
t.Fatalf("lower-priority default survived main sequence replacement: %#v", result.Config.Prompts)
|
||||
}
|
||||
if prompt.ID == "ignored" {
|
||||
t.Fatalf("excluded generated prompt was selected: %#v", result.Config.Prompts)
|
||||
}
|
||||
}
|
||||
|
||||
var itemImports, helperImports int
|
||||
for _, source := range result.Sources() {
|
||||
switch filepath.Base(source.Path) {
|
||||
case "curated.yml", "weather-feature.yml", "generated-welcome.yml":
|
||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/prompts" {
|
||||
t.Fatalf("item source placement = %#v", source)
|
||||
}
|
||||
itemImports++
|
||||
case "curated-id.yml":
|
||||
if source.PlacementMode != configkit.PlacementModeMapping || source.ImportedFrom == nil {
|
||||
t.Fatalf("helper source = %#v", source)
|
||||
}
|
||||
helperImports++
|
||||
}
|
||||
}
|
||||
if itemImports != 3 || helperImports != 1 {
|
||||
t.Fatalf("item imports = %d, helper imports = %d, sources = %#v", itemImports, helperImports, result.Sources())
|
||||
}
|
||||
if result.Config.Prompts[1].ID != "curated" {
|
||||
t.Fatalf("helper did not provide prompt ID: %#v", result.Config.Prompts[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptCatalogMountsResourceFromScheduledPromptOrigin(t *testing.T) {
|
||||
result, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "config.yml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Prompts[2].ID != "welcome" || result.Config.Prompts[2].Resource != "welcome.txt" {
|
||||
t.Fatalf("scheduled resource prompt = %#v", result.Config.Prompts)
|
||||
}
|
||||
resourcePath := configkit.Keys("prompts").Index(2).Key("resource")
|
||||
info, ok := result.Metadata().Lookup(resourcePath)
|
||||
if !ok || len(info.Origin.Locations) != 1 {
|
||||
t.Fatalf("resource origin = %#v, %v", info, ok)
|
||||
}
|
||||
context, ok := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||
if !ok || context.FS == nil || filepath.Base(context.BaseDir) != "prompts" {
|
||||
t.Fatalf("resource source context = %#v, %v", context, ok)
|
||||
}
|
||||
contents, err := fs.ReadFile(context.FS, pathpkg.Join(context.BaseDir, result.Config.Prompts[2].Resource))
|
||||
if err != nil || string(contents) != "Welcome resource text.\n" {
|
||||
t.Fatalf("mounted resource = %q, %v", contents, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredPromptCatalogUsesOneRequiredFile(t *testing.T) {
|
||||
result, err := consumers.LoadRequiredPromptCatalog(fixture("prompt-catalog", "simple.yml"))
|
||||
if err != nil || len(result.Config.Prompts) != 1 || result.Config.Prompts[0].ID != "required" {
|
||||
t.Fatalf("simple required prompt = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptCatalogLeavesDomainValidationWithConsumer(t *testing.T) {
|
||||
_, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "invalid.yml"))
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 ||
|
||||
loadError.Diagnostics()[0].Code != configkit.CodeValidateError ||
|
||||
loadError.Diagnostics()[0].Path.String() != "/prompts/0/messages" {
|
||||
t.Fatalf("consumer domain validation error = %v", err)
|
||||
}
|
||||
}
|
||||
145
examples/consumers/sequence_catalog.go
Normal file
145
examples/consumers/sequence_catalog.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// SequenceCatalogConfig is a source-backed prompt catalog with an application
|
||||
// supplied run label and a value derived from that label during resolution.
|
||||
type SequenceCatalogConfig struct {
|
||||
RunLabel string `yaml:"run_label"`
|
||||
ResolvedLabel string `yaml:"resolved_label"`
|
||||
Prompts []SequenceCatalogEntry `yaml:"prompts"`
|
||||
}
|
||||
|
||||
// SequenceCatalogEntry keeps prompt meaning, resources, and credentials in the
|
||||
// consuming application while ConfigKit handles collection assembly.
|
||||
type SequenceCatalogEntry struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Messages []string `yaml:"messages"`
|
||||
Resource string `yaml:"resource"`
|
||||
Credential configkit.Secret `yaml:"credential"`
|
||||
}
|
||||
|
||||
// LoadSequenceCatalog searches its caller-selected main path before a custom
|
||||
// fallback directory, assembles imported fragments over a file-backed underlay,
|
||||
// applies one selected collection overlay, then resolves and validates once.
|
||||
func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Result[SequenceCatalogConfig], error) {
|
||||
absoluteMain, err := filepath.Abs(mainPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root := filepath.Dir(absoluteMain)
|
||||
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
|
||||
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
|
||||
mainSearch := configkit.SearchConfig("", configkit.Searches(
|
||||
configkit.SearchPaths(absoluteMain),
|
||||
configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{
|
||||
Filenames: []configkit.FilenameGroup{configkit.CustomFilenames(filepath.Base(absoluteMain))},
|
||||
}),
|
||||
), configkit.SearchOptions{Required: true})
|
||||
definition := configkit.Definition[SequenceCatalogConfig]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
},
|
||||
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
|
||||
configuration.ResolvedLabel = "for:" + configuration.RunLabel
|
||||
resolution.RecordNormalization(configkit.Keys("resolved_label"),
|
||||
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
|
||||
return nil
|
||||
},
|
||||
Validate: validateSequenceCatalog,
|
||||
}
|
||||
return configkit.LoadLayered(configkit.Input{
|
||||
Source: mainSearch,
|
||||
Imports: true,
|
||||
Filesystem: &configkit.FilesystemContext{Root: root},
|
||||
}, configkit.LayeredOptions[SequenceCatalogConfig]{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
||||
Source: filesystem.File("underlay.yml"),
|
||||
}}}},
|
||||
Overlay: overlayName,
|
||||
OverlayFilesystem: filesystemContext,
|
||||
Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("run_label"), Value: runLabel, Flag: "--run-label",
|
||||
}},
|
||||
Definition: definition,
|
||||
})
|
||||
}
|
||||
|
||||
// ReadSequenceCatalogResource follows the final prompt field's source context,
|
||||
// including when record inheritance retained a resource from an underlay.
|
||||
func ReadSequenceCatalogResource(result *configkit.Result[SequenceCatalogConfig], promptID string) (string, error) {
|
||||
if result == nil {
|
||||
return "", errors.New("sequence catalog result is nil")
|
||||
}
|
||||
for index, prompt := range result.Config.Prompts {
|
||||
if prompt.ID != promptID {
|
||||
continue
|
||||
}
|
||||
resourcePath := configkit.Keys("prompts").Index(index).Key("resource")
|
||||
info, found := result.Metadata().Lookup(resourcePath)
|
||||
if !found || len(info.Origin.Locations) != 1 {
|
||||
return "", errors.New("prompt resource has no single declaring source")
|
||||
}
|
||||
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||
if !found {
|
||||
return "", errors.New("prompt resource source context is unavailable")
|
||||
}
|
||||
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, prompt.Resource))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read prompt resource: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
return "", fmt.Errorf("prompt %q is unavailable", promptID)
|
||||
}
|
||||
|
||||
func validateSequenceCatalog(configuration *SequenceCatalogConfig, validation *configkit.Validation) {
|
||||
for index, prompt := range configuration.Prompts {
|
||||
if strings.TrimSpace(prompt.Name) == "" {
|
||||
validation.Error(configkit.Keys("prompts").Index(index).Key("name"), "must not be empty")
|
||||
}
|
||||
if len(prompt.Messages) == 0 {
|
||||
validation.Error(configkit.Keys("prompts").Index(index).Key("messages"), "must contain at least one message")
|
||||
}
|
||||
}
|
||||
if len(configuration.Prompts) == 0 {
|
||||
validation.Error(configkit.Keys("prompts"), "must contain at least one prompt")
|
||||
}
|
||||
}
|
||||
|
||||
// NamespacedMapCatalogConfig demonstrates typed application maps whose
|
||||
// independent prompt sequences use root and dynamic namespace registrations.
|
||||
type NamespacedMapCatalogConfig struct {
|
||||
Prompts []map[string]any `yaml:"prompts"`
|
||||
Collections map[string]map[string]any `yaml:"collections"`
|
||||
}
|
||||
|
||||
// LoadNamespacedMapCatalog applies the same scalar identity constraint to one
|
||||
// root collection and each namespace's independently scoped collection.
|
||||
func LoadNamespacedMapCatalog(source configkit.Source) (*configkit.Result[NamespacedMapCatalogConfig], error) {
|
||||
selector := configkit.SequenceSelector{
|
||||
configkit.SequenceLiteralKey("collections"),
|
||||
configkit.SequenceAnyMappingKey(),
|
||||
configkit.SequenceLiteralKey("prompts"),
|
||||
}
|
||||
return configkit.Load(source, configkit.Options[NamespacedMapCatalogConfig]{
|
||||
Definition: configkit.Definition[NamespacedMapCatalogConfig]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{
|
||||
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
||||
configkit.SequenceIdentityMatching(selector, "id"),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
239
examples/consumers/sequence_catalog_test.go
Normal file
239
examples/consumers/sequence_catalog_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package consumers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/configkit/examples/consumers"
|
||||
)
|
||||
|
||||
func Example_sequenceCatalog() {
|
||||
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
|
||||
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
|
||||
restoreOld, restoreNew := preserveEnvironment(oldSecret), preserveEnvironment(newSecret)
|
||||
defer restoreOld()
|
||||
defer restoreNew()
|
||||
_ = os.Setenv(oldSecret, "disposable-old-example-credential")
|
||||
_ = os.Setenv(newSecret, "disposable-new-example-credential")
|
||||
|
||||
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), "upsert", "preview")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(result.Config.Prompts))
|
||||
for _, prompt := range result.Config.Prompts {
|
||||
ids = append(ids, prompt.ID)
|
||||
}
|
||||
shared := configkit.Keys("prompts").Index(1).Key("name")
|
||||
info, _ := result.Metadata().Lookup(shared)
|
||||
resource, resourceErr := consumers.ReadSequenceCatalogResource(result, "concise")
|
||||
fmt.Println(ids, result.Config.ResolvedLabel, info.Explicit, len(result.Metadata().History(shared)), resource, resourceErr == nil, err)
|
||||
// Output: [foundation shared welcome concise extra] for:preview true 2 Foundation resource text.
|
||||
// true <nil>
|
||||
}
|
||||
|
||||
func TestSequenceCatalogSelectedOverlayOperations(t *testing.T) {
|
||||
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
|
||||
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
|
||||
t.Setenv(oldSecret, "sequence-catalog-old-canary")
|
||||
t.Setenv(newSecret, "sequence-catalog-new-canary")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
wantIDs []string
|
||||
shared *consumers.SequenceCatalogEntry
|
||||
}{
|
||||
{name: "append", wantIDs: []string{"foundation", "shared", "welcome", "concise", "extra"}},
|
||||
{name: "upsert", wantIDs: []string{"foundation", "shared", "welcome", "concise", "extra"}, shared: &consumers.SequenceCatalogEntry{ID: "shared", Name: "Shared replacement", Messages: []string{"Replacement message"}, Credential: configkit.SecretEnv(newSecret)}},
|
||||
{name: "replace", wantIDs: []string{"replacement", "last"}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), test.name, "test")
|
||||
if err != nil {
|
||||
var loadError *configkit.LoadError
|
||||
if errors.As(err, &loadError) {
|
||||
t.Fatalf("load selected overlay: %v (%#v)", err, loadError.Diagnostics())
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotIDs := make([]string, 0, len(result.Config.Prompts))
|
||||
for _, prompt := range result.Config.Prompts {
|
||||
gotIDs = append(gotIDs, prompt.ID)
|
||||
}
|
||||
if strings.Join(gotIDs, ",") != strings.Join(test.wantIDs, ",") {
|
||||
t.Fatalf("prompt order = %v, want %v", gotIDs, test.wantIDs)
|
||||
}
|
||||
if test.shared == nil {
|
||||
return
|
||||
}
|
||||
shared := result.Config.Prompts[1]
|
||||
if shared.ID != test.shared.ID || shared.Name != test.shared.Name || len(shared.Messages) != 1 ||
|
||||
shared.Messages[0] != test.shared.Messages[0] || shared.Description != "" || shared.Resource != "" || shared.Credential.EnvName() != newSecret {
|
||||
t.Fatalf("upsert did not replace the complete matched record: %#v", shared)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceCatalogInspectionTracksResourcesAndSecretHistory(t *testing.T) {
|
||||
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
|
||||
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
|
||||
const oldCanary = "sequence-catalog-old-canary"
|
||||
const newCanary = "sequence-catalog-new-canary"
|
||||
t.Setenv(oldSecret, oldCanary)
|
||||
t.Setenv(newSecret, newCanary)
|
||||
|
||||
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), "upsert", "history")
|
||||
if err != nil {
|
||||
var loadError *configkit.LoadError
|
||||
if errors.As(err, &loadError) {
|
||||
t.Fatalf("load sequence catalog: %v (%#v)", err, loadError.Diagnostics())
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
resourcePath := configkit.Keys("prompts").Index(3).Key("resource")
|
||||
resourceInfo, found := result.Metadata().Lookup(resourcePath)
|
||||
if !found || len(resourceInfo.Origin.Locations) != 1 || len(resourceInfo.Origin.Inheritance) != 1 {
|
||||
t.Fatalf("inherited resource inspection = %#v, %v", resourceInfo, found)
|
||||
}
|
||||
resource, err := consumers.ReadSequenceCatalogResource(result, "concise")
|
||||
if err != nil || resource != "Foundation resource text.\n" {
|
||||
t.Fatalf("resource from inherited source context = %q, %v", resource, err)
|
||||
}
|
||||
if len(result.Metadata().History(configkit.Keys("resolved_label"))) == 0 {
|
||||
t.Fatal("recorded CLI-driven resolution was absent from final inspection")
|
||||
}
|
||||
credentialPath := configkit.Keys("prompts").Index(1).Key("credential")
|
||||
credentialHistory := result.Metadata().History(credentialPath)
|
||||
if len(credentialHistory) == 0 {
|
||||
t.Fatal("upsert did not retain the replaced secret reference in history")
|
||||
}
|
||||
var explanation bytes.Buffer
|
||||
if err := result.WriteExplanation(&explanation, credentialPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(explanation.String(), oldCanary) || strings.Contains(explanation.String(), newCanary) {
|
||||
t.Fatalf("secret explanation exposed a credential value: %s", explanation.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespacedTypedMapIdentityScopesAreIndependent(t *testing.T) {
|
||||
source := configkit.Bytes("namespaced maps", []byte(`
|
||||
prompts:
|
||||
- id: shared
|
||||
text: root prompt
|
||||
collections:
|
||||
team:
|
||||
prompts:
|
||||
- id: shared
|
||||
text: team prompt
|
||||
operations:
|
||||
prompts:
|
||||
- id: shared
|
||||
text: operations prompt
|
||||
`))
|
||||
result, err := consumers.LoadNamespacedMapCatalog(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Prompts[0]["text"] != "root prompt" ||
|
||||
result.Config.Collections["team"]["prompts"].([]any)[0].(map[string]any)["text"] != "team prompt" ||
|
||||
result.Config.Collections["operations"]["prompts"].([]any)[0].(map[string]any)["text"] != "operations prompt" {
|
||||
t.Fatalf("typed map collections = %#v", result.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespacedTypedMapIdentityFindsCollectionsIntroducedByInheritance(t *testing.T) {
|
||||
result, err := consumers.LoadNamespacedMapCatalog(configkit.Bytes("inherited namespace", []byte(`
|
||||
collections:
|
||||
source:
|
||||
prompts:
|
||||
- id: shared
|
||||
text: inherited prompt
|
||||
copied:
|
||||
extends: source
|
||||
`)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copied := result.Config.Collections["copied"]["prompts"].([]any)
|
||||
if len(copied) != 1 || copied[0].(map[string]any)["text"] != "inherited prompt" {
|
||||
t.Fatalf("inherited typed-map collection = %#v", result.Config.Collections["copied"])
|
||||
}
|
||||
}
|
||||
|
||||
func Example_yamlDeclaredIdentityOnly() {
|
||||
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
prompts:
|
||||
- id: initial
|
||||
`)), configkit.Options[map[string]any]{
|
||||
Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("prompts"),
|
||||
Value: []any{
|
||||
map[string]any{"id": "same"},
|
||||
map[string]any{"id": "same"},
|
||||
},
|
||||
Flag: "--prompts",
|
||||
}},
|
||||
})
|
||||
fmt.Println(len(result.Config["prompts"].([]any)), err)
|
||||
// Output: 2 <nil>
|
||||
}
|
||||
|
||||
func TestYAMLDeclaredIdentityDoesNotRegisterFinalTypedConstraint(t *testing.T) {
|
||||
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
prompts:
|
||||
- id: initial
|
||||
`)), configkit.Options[map[string]any]{
|
||||
Overrides: []configkit.Override{{
|
||||
Path: configkit.Keys("prompts"),
|
||||
Value: []any{
|
||||
map[string]any{"id": "same"},
|
||||
map[string]any{"id": "same"},
|
||||
},
|
||||
Flag: "--prompts",
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("YAML-only identity incorrectly became an application constraint: %v", err)
|
||||
}
|
||||
prompts := result.Config["prompts"].([]any)
|
||||
if len(prompts) != 2 || prompts[0].(map[string]any)["id"] != prompts[1].(map[string]any)["id"] {
|
||||
t.Fatalf("CLI result = %#v", result.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespacedTypedMapRejectsDuplicatesWithinOneCollection(t *testing.T) {
|
||||
_, err := consumers.LoadNamespacedMapCatalog(configkit.Bytes("duplicate namespace", []byte(`
|
||||
collections:
|
||||
team:
|
||||
prompts:
|
||||
- id: duplicate
|
||||
- id: duplicate
|
||||
`)))
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 ||
|
||||
loadError.Diagnostics()[0].Code != configkit.CodeComposeConflict ||
|
||||
loadError.Diagnostics()[0].Path.String() != "/collections/team/prompts" {
|
||||
diagnostics := []configkit.Diagnostic(nil)
|
||||
if loadError != nil {
|
||||
diagnostics = loadError.Diagnostics()
|
||||
}
|
||||
t.Fatalf("duplicate namespaced identity error = %v (%#v)", err, diagnostics)
|
||||
}
|
||||
}
|
||||
64
examples/consumers/source_version.go
Normal file
64
examples/consumers/source_version.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package consumers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
// VersionedServiceConfig demonstrates a mounted main input whose source-local
|
||||
// schema version is retained at the effective service path.
|
||||
type VersionedServiceConfig struct {
|
||||
Service struct {
|
||||
Version string `yaml:"version"`
|
||||
Name string `yaml:"name"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
Metadata struct {
|
||||
Release string `yaml:"release"`
|
||||
} `yaml:"metadata"`
|
||||
} `yaml:"service"`
|
||||
}
|
||||
|
||||
// LoadVersionedService checks the main and underlay declarations with one
|
||||
// application policy. The local rule path is "version" for each source; the
|
||||
// main input's destination places its protected value at /service/version.
|
||||
// Optional CLI overrides are passed through to ConfigKit for normal protection.
|
||||
func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configkit.Override) (*configkit.Result[VersionedServiceConfig], error) {
|
||||
files, err := fs.Sub(bundledFiles, "testdata/source-version")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
main, err := fs.ReadFile(files, "main.yml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
underlay, err := fs.ReadFile(files, "underlay.yml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definition := configkit.Definition[VersionedServiceConfig]{
|
||||
Version: &configkit.VersionRule{
|
||||
Path: configkit.Keys("version"),
|
||||
Check: func(version configkit.Version, _ *configkit.Validation) error {
|
||||
if !version.Present || version.Null || version.Text == "" {
|
||||
return errors.New("a nonempty source version is required")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/service/version"
|
||||
},
|
||||
}
|
||||
return configkit.LoadLayered(configkit.Input{
|
||||
Source: configkit.Bytes("service main", main), Destination: configkit.Keys("service"),
|
||||
}, configkit.LayeredOptions[VersionedServiceConfig]{
|
||||
Definition: definition,
|
||||
SourceVersionMode: mode,
|
||||
Overrides: overrides,
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
||||
Source: configkit.Bytes("service defaults", underlay), Destination: configkit.Keys("service"),
|
||||
}}}},
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: metadata.yml
|
||||
destination: []
|
||||
- path: prompts/index.yml
|
||||
destination: [prompts]
|
||||
- path: profiles/index.yml
|
||||
destination: [profiles]
|
||||
name: Weather Reporter
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
version: "2.4.0"
|
||||
config:
|
||||
imports:
|
||||
- path: version.yml
|
||||
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 @@
|
||||
config:
|
||||
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 @@
|
||||
config:
|
||||
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"
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: ../shared.yml
|
||||
enabled: true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: artifacts.yml
|
||||
poll_interval: 1m
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
configkit:
|
||||
config:
|
||||
imports:
|
||||
- path: declarations/path.yml
|
||||
sensitive: PATH_REFERENCE_SECRET_CANARY
|
||||
|
||||
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 @@
|
||||
config:
|
||||
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.
|
||||
19
examples/consumers/testdata/prompt-catalog/config.yml
vendored
Normal file
19
examples/consumers/testdata/prompt-catalog/config.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: replace
|
||||
imports:
|
||||
- dir: prompts
|
||||
exclude: [generated-ignored.yml]
|
||||
destination: [prompts]
|
||||
placement: sequence-item
|
||||
- dir: featured
|
||||
pattern: [weather-*.yml]
|
||||
destination: [prompts]
|
||||
placement: sequence-item
|
||||
- dir: optional
|
||||
prompts:
|
||||
- id: main
|
||||
name: Main authored prompt
|
||||
messages: [Use the main prompt.]
|
||||
4
examples/consumers/testdata/prompt-catalog/featured/weather-feature.yml
vendored
Normal file
4
examples/consumers/testdata/prompt-catalog/featured/weather-feature.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
id: weather
|
||||
name: Weather forecast
|
||||
messages:
|
||||
- Describe today's forecast.
|
||||
9
examples/consumers/testdata/prompt-catalog/invalid.yml
vendored
Normal file
9
examples/consumers/testdata/prompt-catalog/invalid.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: replace
|
||||
prompts:
|
||||
- id: invalid
|
||||
name: Invalid prompt
|
||||
messages: []
|
||||
1
examples/consumers/testdata/prompt-catalog/optional/ignored.txt
vendored
Normal file
1
examples/consumers/testdata/prompt-catalog/optional/ignored.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
This file does not match the application import defaults.
|
||||
6
examples/consumers/testdata/prompt-catalog/prompts/curated.yml
vendored
Normal file
6
examples/consumers/testdata/prompt-catalog/prompts/curated.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
config:
|
||||
imports:
|
||||
- path: helpers/curated-id.yml
|
||||
name: Curated summary
|
||||
messages:
|
||||
- Summarize the supplied content.
|
||||
3
examples/consumers/testdata/prompt-catalog/prompts/generated-ignored.yml
vendored
Normal file
3
examples/consumers/testdata/prompt-catalog/prompts/generated-ignored.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
id: ignored
|
||||
name: Excluded generated prompt
|
||||
messages: [This record is excluded.]
|
||||
5
examples/consumers/testdata/prompt-catalog/prompts/generated-welcome.yml
vendored
Normal file
5
examples/consumers/testdata/prompt-catalog/prompts/generated-welcome.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
id: welcome
|
||||
name: Welcome
|
||||
messages:
|
||||
- Welcome the user.
|
||||
resource: welcome.txt
|
||||
1
examples/consumers/testdata/prompt-catalog/prompts/helpers/curated-id.yml
vendored
Normal file
1
examples/consumers/testdata/prompt-catalog/prompts/helpers/curated-id.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
id: curated
|
||||
3
examples/consumers/testdata/prompt-catalog/prompts/required.yml
vendored
Normal file
3
examples/consumers/testdata/prompt-catalog/prompts/required.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
id: required
|
||||
name: Required prompt
|
||||
messages: [Use the required prompt.]
|
||||
1
examples/consumers/testdata/prompt-catalog/prompts/welcome.txt
vendored
Normal file
1
examples/consumers/testdata/prompt-catalog/prompts/welcome.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Welcome resource text.
|
||||
7
examples/consumers/testdata/prompt-catalog/simple.yml
vendored
Normal file
7
examples/consumers/testdata/prompt-catalog/simple.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
config:
|
||||
imports:
|
||||
- path: prompts/required.yml
|
||||
required: true
|
||||
destination: [prompts]
|
||||
placement: sequence-item
|
||||
prompts: []
|
||||
7
examples/consumers/testdata/sequence-catalog/config.yml
vendored
Normal file
7
examples/consumers/testdata/sequence-catalog/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
run_label: source-default
|
||||
config:
|
||||
imports:
|
||||
- dir: fragments
|
||||
pattern: '*.yml'
|
||||
destination: [prompts]
|
||||
placement: sequence-item
|
||||
1
examples/consumers/testdata/sequence-catalog/foundation.txt
vendored
Normal file
1
examples/consumers/testdata/sequence-catalog/foundation.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Foundation resource text.
|
||||
4
examples/consumers/testdata/sequence-catalog/fragments/01-welcome.yml
vendored
Normal file
4
examples/consumers/testdata/sequence-catalog/fragments/01-welcome.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
id: welcome
|
||||
name: Welcome
|
||||
messages: [Welcome message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
3
examples/consumers/testdata/sequence-catalog/fragments/02-concise.yml
vendored
Normal file
3
examples/consumers/testdata/sequence-catalog/fragments/02-concise.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
id: concise
|
||||
extends: {id: foundation}
|
||||
name: Concise
|
||||
10
examples/consumers/testdata/sequence-catalog/overlays/append.yml
vendored
Normal file
10
examples/consumers/testdata/sequence-catalog/overlays/append.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: append
|
||||
prompts:
|
||||
- id: extra
|
||||
name: Extra
|
||||
messages: [Extra message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
14
examples/consumers/testdata/sequence-catalog/overlays/replace.yml
vendored
Normal file
14
examples/consumers/testdata/sequence-catalog/overlays/replace.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: replace
|
||||
prompts:
|
||||
- id: replacement
|
||||
name: Replacement
|
||||
messages: [Replacement message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
- id: last
|
||||
name: Last
|
||||
messages: [Last message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
14
examples/consumers/testdata/sequence-catalog/overlays/upsert.yml
vendored
Normal file
14
examples/consumers/testdata/sequence-catalog/overlays/upsert.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: upsert
|
||||
prompts:
|
||||
- id: shared
|
||||
name: Shared replacement
|
||||
messages: [Replacement message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET
|
||||
- id: extra
|
||||
name: Extra
|
||||
messages: [Extra message]
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
18
examples/consumers/testdata/sequence-catalog/underlay.yml
vendored
Normal file
18
examples/consumers/testdata/sequence-catalog/underlay.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
config:
|
||||
sequences:
|
||||
- destination: [prompts]
|
||||
key: id
|
||||
mode: append
|
||||
prompts:
|
||||
- id: foundation
|
||||
name: Foundation
|
||||
description: Shared foundation instruction
|
||||
messages: [Foundation message]
|
||||
resource: foundation.txt
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
- id: shared
|
||||
name: Shared draft
|
||||
description: This field must not survive a whole-record replacement
|
||||
messages: [Draft message]
|
||||
resource: shared.txt
|
||||
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
|
||||
4
examples/consumers/testdata/source-version/main.yml
vendored
Normal file
4
examples/consumers/testdata/source-version/main.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
version: service-v2
|
||||
name: example service
|
||||
metadata:
|
||||
release: application-2026.10
|
||||
2
examples/consumers/testdata/source-version/underlay.yml
vendored
Normal file
2
examples/consumers/testdata/source-version/underlay.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
version: catalog-v1
|
||||
endpoint: https://service.example.invalid
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
|
||||
func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||
t.Parallel()
|
||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
const packagePath = "go.maximumdirect.net/configkit"
|
||||
for _, candidate := range []reflect.Type{
|
||||
reflect.TypeFor[configkit.Path](), reflect.TypeFor[configkit.Source](),
|
||||
reflect.TypeFor[configkit.Secret](), reflect.TypeFor[configkit.Diagnostic](),
|
||||
@@ -140,7 +140,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
report, err := configkit.LoadSecretsDir(directory)
|
||||
report, err := configkit.LoadSecretsIntoEnvironment(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -164,7 +164,7 @@ func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
||||
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
|
||||
}
|
||||
|
||||
failedReport, err := configkit.LoadSecretsDir("")
|
||||
failedReport, err := configkit.LoadSecretsIntoEnvironment("")
|
||||
var secretsError *configkit.SecretsError
|
||||
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
|
||||
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
|
||||
@@ -203,7 +203,7 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
if !resolution.IsExplicit(configkit.Keys("input")) {
|
||||
resolution.Error(configkit.Keys("input"), "input is required")
|
||||
}
|
||||
input, ok := resolution.InputInfo(configkit.Keys("input"))
|
||||
input, ok := resolution.EntryInfo(configkit.Keys("input"))
|
||||
if !ok || !input.Explicit {
|
||||
resolution.Error(configkit.Keys("input"), "missing input provenance")
|
||||
}
|
||||
@@ -245,6 +245,8 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
}
|
||||
for _, context := range []func(){
|
||||
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
|
||||
func() { retainedResolution.RecordDerivedDefault(configkit.Keys("input"), "too late") },
|
||||
func() { retainedResolution.RecordNormalization(configkit.Keys("input"), "too late") },
|
||||
func() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
|
||||
} {
|
||||
func() {
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -27,23 +26,6 @@ type facadeCompatibilityConfig struct {
|
||||
func TestFacadeCompatibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
for _, candidate := range []struct {
|
||||
name string
|
||||
type_ reflect.Type
|
||||
}{
|
||||
{"Path", reflect.TypeFor[configkit.Path]()},
|
||||
{"Source", reflect.TypeFor[configkit.Source]()},
|
||||
{"Secret", reflect.TypeFor[configkit.Secret]()},
|
||||
{"Definition", reflect.TypeFor[configkit.Definition[facadeCompatibilityConfig]]()},
|
||||
{"Options", reflect.TypeFor[configkit.Options[facadeCompatibilityConfig]]()},
|
||||
{"Result", reflect.TypeFor[configkit.Result[facadeCompatibilityConfig]]()},
|
||||
} {
|
||||
if candidate.type_.PkgPath() != packagePath {
|
||||
t.Errorf("%s belongs to %q, want %q", candidate.name, candidate.type_.PkgPath(), packagePath)
|
||||
}
|
||||
}
|
||||
|
||||
constructed := configkit.Options[facadeCompatibilityConfig]{
|
||||
Definition: configkit.Definition[facadeCompatibilityConfig]{},
|
||||
Filesystem: &configkit.FilesystemContext{BaseDir: "configs", Root: "configs"},
|
||||
@@ -56,11 +38,6 @@ func TestFacadeCompatibility(t *testing.T) {
|
||||
if got := constructed.Overrides[0].Path.String(); got != "/value" {
|
||||
t.Fatalf("constructed override path = %q, want /value", got)
|
||||
}
|
||||
publicResult := configkit.Result[facadeCompatibilityConfig]{Config: facadeCompatibilityConfig{Value: "constructed"}}
|
||||
if publicResult.Config.Value != "constructed" {
|
||||
t.Fatal("constructed result lost its public configuration")
|
||||
}
|
||||
|
||||
result, err := configkit.Load(configkit.Bytes("facade.yml", []byte("value: input\n")), configkit.Options[facadeCompatibilityConfig]{
|
||||
Definition: configkit.Definition[facadeCompatibilityConfig]{
|
||||
Resolve: func(configuration *facadeCompatibilityConfig, resolution *configkit.Resolution) error {
|
||||
|
||||
108
facade_config_directives_test.go
Normal file
108
facade_config_directives_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestDocumentRootControlsAndNestedApplicationConfig(t *testing.T) {
|
||||
type settings struct {
|
||||
Imports string `yaml:"imports"`
|
||||
}
|
||||
type app struct {
|
||||
Service struct {
|
||||
Config settings `yaml:"config"`
|
||||
} `yaml:"service"`
|
||||
}
|
||||
result, err := configkit.Load[app](configkit.Bytes("main", []byte("config: {sequences: []}\nservice: {config: {imports: ordinary}}\n")))
|
||||
if err != nil || result.Config.Service.Config.Imports != "ordinary" {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountedConfigDataKeepsControlEvidenceAndHiding(t *testing.T) {
|
||||
type settings struct {
|
||||
Imports string `yaml:"imports"`
|
||||
Token string `yaml:"token" configkit:"secret"`
|
||||
}
|
||||
type app struct {
|
||||
Config settings `yaml:"config"`
|
||||
}
|
||||
fs := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("config: {imports: [{path: missing.yml}, {path: data.yml, destination: [config]}]}\n")},
|
||||
"data.yml": {Data: []byte("imports: ordinary\ntoken: disposable-canary\n")},
|
||||
})
|
||||
result, err := configkit.Load[app](fs.File("main.yml"), configkit.Options[app]{Imports: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Config.Config.Imports != "ordinary" {
|
||||
t.Fatalf("config = %#v", result.Config)
|
||||
}
|
||||
sources := result.Sources()
|
||||
if len(sources) != 2 || len(sources[0].EmptyImportSelections) != 1 || sources[0].EmptyImportSelections[0].Path.String() != "/config/imports/0" {
|
||||
t.Fatalf("sources = %#v", sources)
|
||||
}
|
||||
if _, ok := result.Metadata().Lookup(configkit.Keys("config", "imports")); !ok {
|
||||
t.Fatal("missing application value")
|
||||
}
|
||||
if _, ok := result.Explain(configkit.Keys("config", "imports")); !ok {
|
||||
t.Fatal("missing explanation")
|
||||
}
|
||||
var output strings.Builder
|
||||
if err := result.WriteEffective(&output); err != nil || strings.Contains(output.String(), "disposable-canary") || !strings.Contains(output.String(), "ordinary") {
|
||||
t.Fatalf("effective output = %q, %v", output.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOldControlSpellingsAreOrdinaryData(t *testing.T) {
|
||||
body := "configkit: {imports: [{path: nonexistent.yml}]}\n_configkit: ordinary\n"
|
||||
result, err := configkit.Load[map[string]any](configkit.Bytes("old", []byte(body)))
|
||||
if err != nil || result.Config["configkit"] == nil || result.Config["_configkit"] != "ordinary" {
|
||||
t.Fatalf("result = %#v, error = %v", result, err)
|
||||
}
|
||||
if _, err := configkit.Load[struct{}](configkit.Bytes("strict", []byte(body))); err == nil {
|
||||
t.Fatal("strict schema accepted unknown data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultsOverridesAndResolutionAtConfigRoot(t *testing.T) {
|
||||
type settings struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
type app struct {
|
||||
Config settings `yaml:"config"`
|
||||
}
|
||||
entryExplicit := false
|
||||
entryCountExplicit := false
|
||||
result, err := configkit.Load[app](configkit.NoSource(), configkit.Options[app]{
|
||||
Definition: configkit.Definition[app]{
|
||||
Defaults: func() app { return app{Config: settings{Enabled: true, Count: 2}} },
|
||||
Resolve: func(value *app, r *configkit.Resolution) error {
|
||||
info, ok := r.EntryInfo(configkit.Keys("config", "enabled"))
|
||||
entryExplicit = ok && info.Explicit && info.Origin.Layer == configkit.LayerCLI
|
||||
entryCountExplicit = r.IsExplicit(configkit.Keys("config", "count"))
|
||||
value.Config.Count = 7
|
||||
r.RecordNormalization(configkit.Keys("config", "count"), "normalize count")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Overrides: []configkit.Override{{Path: configkit.Keys("config", "enabled"), Value: false, Flag: "--enabled"}},
|
||||
})
|
||||
if err != nil || result == nil || !entryExplicit || entryCountExplicit || result.Config.Config.Enabled || result.Config.Config.Count != 7 {
|
||||
t.Fatalf("result = %#v, entry explicit = %t, error = %v", result, entryExplicit, err)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(configkit.Keys("config", "enabled")); !ok || !info.Explicit || info.Origin.Layer != configkit.LayerCLI {
|
||||
t.Fatalf("final enabled info = %#v, %t", info, ok)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(configkit.Keys("config", "count")); !ok || info.Origin.Layer != configkit.LayerResolution {
|
||||
t.Fatalf("final count info = %#v, %t", info, ok)
|
||||
}
|
||||
if _, ok := result.Explain(configkit.Keys("config", "count")); !ok {
|
||||
t.Fatal("missing resolution explanation")
|
||||
}
|
||||
}
|
||||
41
facade_container_origin_test.go
Normal file
41
facade_container_origin_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestOverlayExplanationUsesQueryScopedOrigin(t *testing.T) {
|
||||
t.Parallel()
|
||||
filesystem := configkit.NewFilesystem("origin", fstest.MapFS{
|
||||
"main.yml": {Data: []byte("a: base\ngroup: {x: X, y: Y}\n")},
|
||||
"overlays/dev.yml": {Data: []byte("a: overlay\ngroup: {x: O}\n")},
|
||||
})
|
||||
result, err := configkit.Load[map[string]any](filesystem.File("main.yml"), configkit.Options[map[string]any]{Overlay: "dev"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
path configkit.Path
|
||||
line, column int
|
||||
}{
|
||||
{configkit.Keys(), 1, 1},
|
||||
{configkit.Keys("group"), 2, 8},
|
||||
{configkit.Keys("group", "x"), 2, 12},
|
||||
} {
|
||||
explanation, found := result.Explain(test.path)
|
||||
if !found || len(explanation.History) == 0 {
|
||||
t.Fatalf("missing explanation for %s", test.path)
|
||||
}
|
||||
origin := explanation.History[len(explanation.History)-1].Origin
|
||||
if origin.Layer != configkit.LayerOverlay || len(origin.Locations) != 1 {
|
||||
t.Fatalf("overlay origin for %s = %#v", test.path, origin)
|
||||
}
|
||||
location := origin.Locations[0]
|
||||
if location.SourceID != "source-2" || location.Line != test.line || location.Column != test.column {
|
||||
t.Fatalf("overlay location for %s = %#v; want line %d column %d", test.path, location, test.line, test.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package configkit_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||
@@ -24,10 +24,11 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||
call func()
|
||||
}{
|
||||
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
|
||||
{"InputInfo", func() { state.resolution.InputInfo(path) }},
|
||||
{"EntryInfo", func() { state.resolution.EntryInfo(path) }},
|
||||
{"Source", func() { state.resolution.Source("source") }},
|
||||
{"SourceContext", func() { state.resolution.SourceContext("source") }},
|
||||
{"Record", func() { state.resolution.Record(path, configkit.ChangeNormalization, "reason") }},
|
||||
{"RecordDerivedDefault", func() { state.resolution.RecordDerivedDefault(path, "reason") }},
|
||||
{"RecordNormalization", func() { state.resolution.RecordNormalization(path, "reason") }},
|
||||
{"Resolution.Error", func() { state.resolution.Error(path, "message") }},
|
||||
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
|
||||
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
configkit "go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestFacadeInputDestinationsAcrossSourceKinds(t *testing.T) {
|
||||
@@ -109,7 +109,7 @@ func TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths(t *testing.T)
|
||||
wantReads int
|
||||
}{
|
||||
{name: "index segment", source: configkit.Reader("unread", reader), destination: configkit.Keys("catalog").Index(0)},
|
||||
{name: "reserved configkit key", source: configkit.NoSource(), destination: configkit.Keys("configkit")},
|
||||
{name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("extends")},
|
||||
{name: "reserved extends key", source: configkit.NoSource(), destination: configkit.Keys("catalog", "extends")},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -129,7 +129,7 @@ func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := filepath.Join(directory, "main.yml")
|
||||
fragmentPath := filepath.Join(directory, "fragment.yml")
|
||||
if err := os.WriteFile(mainPath, []byte("configkit: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil {
|
||||
if err := os.WriteFile(mainPath, []byte("config: {imports: [{path: fragment.yml}]}\nmain: present\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(fragmentPath, []byte("imported: present\n"), 0o600); err != nil {
|
||||
@@ -155,6 +155,139 @@ func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeRelativeImportDestinationsForRootAndMountedLoads(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
write := func(relative, contents string) string {
|
||||
t.Helper()
|
||||
filename := filepath.Join(directory, filepath.FromSlash(relative))
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
mainPath := write("main.yml", `config:
|
||||
imports:
|
||||
- path: metadata.yml
|
||||
destination: [metadata]
|
||||
- glob: prompts/*.yml
|
||||
destination: [prompts]
|
||||
- path: entries/main.yml
|
||||
destination: [profiles]
|
||||
- path: sibling.yml
|
||||
destination: [sibling]
|
||||
root: main
|
||||
`)
|
||||
write("metadata.yml", "name: imported\n")
|
||||
write("prompts/a.yml", "a: {name: alpha}\n")
|
||||
write("prompts/b.yml", "b: {name: beta}\n")
|
||||
write("entries/main.yml", `config:
|
||||
imports:
|
||||
- path: nested/details.yml
|
||||
destination: [details]
|
||||
name: profile
|
||||
`)
|
||||
write("entries/nested/details.yml", "summary: nested\n")
|
||||
write("sibling.yml", "name: separate\n")
|
||||
|
||||
root, err := configkit.Load(
|
||||
configkit.File(mainPath), configkit.Options[map[string]any]{Imports: true},
|
||||
)
|
||||
if err != nil {
|
||||
if loadError, ok := err.(*configkit.LoadError); ok {
|
||||
t.Fatalf("ordinary root Load: %v, diagnostics %#v", err, loadError.Diagnostics())
|
||||
}
|
||||
t.Fatalf("ordinary root Load: %v", err)
|
||||
}
|
||||
mountedDestination := configkit.Keys("collections", "weatherreporter")
|
||||
mounted, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: mountedDestination,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("mounted layered Load: %v", err)
|
||||
}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
result *configkit.Result[map[string]any]
|
||||
prefix []string
|
||||
}{
|
||||
{name: "ordinary root", result: root},
|
||||
{name: "mounted entry", result: mounted, prefix: []string{"collections", "weatherreporter"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
at := func(keys ...string) []string {
|
||||
return append(append([]string(nil), test.prefix...), keys...)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("root")...); got != "main" {
|
||||
t.Fatalf("root value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("metadata", "name")...); got != "imported" {
|
||||
t.Fatalf("metadata value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("prompts", "a", "name")...); got != "alpha" {
|
||||
t.Fatalf("first glob value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("prompts", "b", "name")...); got != "beta" {
|
||||
t.Fatalf("second glob value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("profiles", "name")...); got != "profile" {
|
||||
t.Fatalf("entry value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("profiles", "details", "summary")...); got != "nested" {
|
||||
t.Fatalf("nested import value = %#v", got)
|
||||
}
|
||||
if got := facadeDestinationValue(t, test.result.Config, at("sibling", "name")...); got != "separate" {
|
||||
t.Fatalf("subsequent sibling value = %#v", got)
|
||||
}
|
||||
|
||||
wantPaths := []string{
|
||||
"main.yml", "metadata.yml", "a.yml", "b.yml", "main.yml", "details.yml", "sibling.yml",
|
||||
}
|
||||
wantPhysicalPaths := []string{
|
||||
mainPath,
|
||||
filepath.Join(directory, "metadata.yml"),
|
||||
filepath.Join(directory, "prompts", "a.yml"),
|
||||
filepath.Join(directory, "prompts", "b.yml"),
|
||||
filepath.Join(directory, "entries", "main.yml"),
|
||||
filepath.Join(directory, "entries", "nested", "details.yml"),
|
||||
filepath.Join(directory, "sibling.yml"),
|
||||
}
|
||||
wantImportParents := []string{"", "source-1", "source-1", "source-1", "source-1", "source-5", "source-1"}
|
||||
wantDestinations := []configkit.Path{configkit.Keys(test.prefix...)}
|
||||
for _, suffix := range [][]string{{"metadata"}, {"prompts"}, {"prompts"}, {"profiles"}, {"profiles", "details"}, {"sibling"}} {
|
||||
destination := append([]string(nil), test.prefix...)
|
||||
for _, segment := range suffix {
|
||||
destination = append(destination, segment)
|
||||
}
|
||||
wantDestinations = append(wantDestinations, configkit.Keys(destination...))
|
||||
}
|
||||
sources := test.result.Sources()
|
||||
if len(sources) != len(wantPaths) {
|
||||
t.Fatalf("sources = %#v, want %d", sources, len(wantPaths))
|
||||
}
|
||||
for index, source := range sources {
|
||||
if filepath.Base(source.Path) != wantPaths[index] || source.Path != wantPhysicalPaths[index] || source.Destination.String() != wantDestinations[index].String() {
|
||||
t.Errorf("source %d path/destination = (%q, %q), want physical path %q and destination %q", index, source.Path, source.Destination.String(), wantPhysicalPaths[index], wantDestinations[index].String())
|
||||
}
|
||||
if source.ID != fmt.Sprintf("source-%d", index+1) || source.Kind != configkit.SourceKindFile {
|
||||
t.Errorf("source %d identity/kind = (%q, %q)", index, source.ID, source.Kind)
|
||||
}
|
||||
if wantImportParents[index] == "" {
|
||||
if source.ImportedFrom != nil {
|
||||
t.Errorf("entry source unexpectedly has an import location: %#v", source.ImportedFrom)
|
||||
}
|
||||
} else if source.ImportedFrom == nil || source.ImportedFrom.SourceID != wantImportParents[index] || source.ImportedFrom.Line == 0 || source.ImportedFrom.Column == 0 {
|
||||
t.Errorf("source %d import location = %#v, want declaration in %s", index, source.ImportedFrom, wantImportParents[index])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSameFragmentMountsAtSeparateCollectionDestinations(t *testing.T) {
|
||||
weatherDestination := configkit.Keys("collections", "weather", "prompts")
|
||||
homeDestination := configkit.Keys("collections", "homewatch", "prompts")
|
||||
@@ -219,7 +352,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
||||
callbacks.resolve++
|
||||
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
||||
input, ok := resolution.InputInfo(resourcePath)
|
||||
input, ok := resolution.EntryInfo(resourcePath)
|
||||
if !ok || len(input.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("mounted resource input has no single source origin")
|
||||
}
|
||||
@@ -237,11 +370,11 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
}
|
||||
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
|
||||
entry["resource_text"] = string(data)
|
||||
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
|
||||
resolution.RecordNormalization(configkit.Keys("catalog", "entries", "item", "resource_text"), "read source-relative summary", resourcePath)
|
||||
return nil
|
||||
},
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -272,7 +405,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
if err != nil || string(data) != "source-relative summary" {
|
||||
t.Fatalf("completed relative resource = %q, %v", data, err)
|
||||
}
|
||||
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerBase {
|
||||
if info, ok := result.Metadata().Lookup(titlePath); !ok || info.Origin.Layer != configkit.LayerMain {
|
||||
t.Fatalf("mounted title metadata = %#v, %v", info, ok)
|
||||
}
|
||||
if history := result.Metadata().History(titlePath); len(history) < 2 {
|
||||
@@ -357,6 +490,135 @@ func TestFacadeFailedLoadExposesDetachedSourceDestination(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeRelativeImportFailuresRetainSourceRecordsAndCoordinates(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
write := func(name, contents string) string {
|
||||
t.Helper()
|
||||
filename := filepath.Join(directory, name)
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
t.Run("imported parse failure retains effective source destination", func(t *testing.T) {
|
||||
mainPath := write("parse/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("parse/child.yml", "count: [unterminated\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeYAMLInvalid || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
||||
t.Fatalf("imported parse diagnostic = %#v", diagnostics)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
||||
t.Fatalf("imported parse failure sources = %#v", sources)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directive failure stays source local", func(t *testing.T) {
|
||||
mainPath := write("directive/main.yml", "config:\n imports:\n - path: parent.yml\n destination: [tenant]\n")
|
||||
parentPath := write("directive/parent.yml", "config:\n imports:\n - path: details.yml\n destination: [3]\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
wantPath := configkit.Keys("config", "imports").Index(0).Key("destination").Index(0)
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportInvalid || !reflect.DeepEqual(diagnostics[0].Path, wantPath) || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
||||
t.Fatalf("import directive diagnostic = %#v", diagnostics)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != parentPath {
|
||||
t.Fatalf("directive failure sources = %#v", sources)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("composition failure reports effective path and all sources", func(t *testing.T) {
|
||||
mainPath := write("compose/main.yml", "config:\n imports:\n - path: left.yml\n destination: [tenant]\n - path: right.yml\n destination: [tenant]\n")
|
||||
write("compose/left.yml", "value: same\n")
|
||||
write("compose/right.yml", "value: same\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeComposeConflict || diagnostics[0].Path.String() != "/mount/tenant/value" {
|
||||
t.Fatalf("composition diagnostic = %#v", diagnostics)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 3 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[2].Destination.String() != "/mount/tenant" {
|
||||
t.Fatalf("composition failure sources = %#v", sources)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("version failure reports effective path and imported source", func(t *testing.T) {
|
||||
mainPath := write("version/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("version/child.yml", "version: forbidden\n")
|
||||
_, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
}, configkit.LayeredOptions[map[string]any]{
|
||||
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
|
||||
Path: configkit.Keys("mount", "tenant", "version"),
|
||||
Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
||||
}},
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeImportVersion || diagnostics[0].Path.String() != "/mount/tenant/version" || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" {
|
||||
t.Fatalf("version diagnostic = %#v", diagnostics)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
||||
t.Fatalf("version failure sources = %#v", sources)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("typed validation reports effective path and authored location", func(t *testing.T) {
|
||||
type configuration struct {
|
||||
Mount struct {
|
||||
Tenant struct {
|
||||
Count int `yaml:"count"`
|
||||
} `yaml:"tenant"`
|
||||
} `yaml:"mount"`
|
||||
}
|
||||
mainPath := write("typed/main.yml", "config:\n imports:\n - path: child.yml\n destination: [tenant]\n")
|
||||
childPath := write("typed/child.yml", "count: invalid\n")
|
||||
_, err := configkit.LoadLayered[configuration](configkit.Input{
|
||||
Source: configkit.File(mainPath), Imports: true, Destination: configkit.Keys("mount"),
|
||||
})
|
||||
var loadError *configkit.LoadError
|
||||
if !errors.As(err, &loadError) {
|
||||
t.Fatalf("load error = %T %v", err, err)
|
||||
}
|
||||
diagnostics := loadError.Diagnostics()
|
||||
if len(diagnostics) != 1 || diagnostics[0].Code != configkit.CodeDecodeType || diagnostics[0].Path.String() != "/mount/tenant/count" || diagnostics[0].Primary == nil || diagnostics[0].Primary.SourceID != "source-2" || diagnostics[0].Primary.Line != 1 {
|
||||
t.Fatalf("typed validation diagnostic = %#v", diagnostics)
|
||||
}
|
||||
sources := loadError.Sources()
|
||||
if len(sources) != 2 || sources[0].Destination.String() != "/mount" || sources[1].Destination.String() != "/mount/tenant" || sources[1].Path != childPath {
|
||||
t.Fatalf("typed validation sources = %#v", sources)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFacadeDestinationMappingFailurePaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user