Compare commits
472 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 315f464dfc | |||
| a029a2a754 | |||
| 945e111c4e | |||
| 2d8df22dd1 | |||
| 53601e68ed | |||
| 751787ec24 | |||
| f50122d9f8 | |||
| c71faf2065 | |||
| 2f5d57ce1d | |||
| a1c557603f | |||
| a2fab66aba | |||
| 2f1f7f4b8a | |||
| 667a536ae7 | |||
| ea55ab43ce | |||
| d3d468a8b4 | |||
| 6353598bd6 | |||
| 3c2d7facb3 | |||
| 6fffde4af8 | |||
| 9e04a397c4 | |||
| c54a9bf51e | |||
| 8f3396d6f0 | |||
| 1c2610c2c0 | |||
| 92025d65cb | |||
| a4b5a6840a | |||
| 2ebdfb539b | |||
| a37e6895e1 | |||
| 8ae542b3a3 | |||
| 174efb65f6 | |||
| 756149beaa | |||
| e5dddef204 | |||
| ade5d6a273 | |||
| c20ea4b916 | |||
| 2e17e365a1 | |||
| fcf914a2ab | |||
| 3697a51124 | |||
| 9194c5b52a | |||
| e82b1c9054 | |||
| 6f59cdd237 | |||
| 1a885934c2 | |||
| 5917cebf50 | |||
| 8e91a2b16b | |||
| 08402d098b | |||
| 53b79528d9 | |||
| f849e06b77 | |||
| b37948cfea | |||
| 9323f89ad1 | |||
| 6c62226838 | |||
| 799ebab5a6 | |||
| c8546ae599 | |||
| d32c94eca3 | |||
| 344f4f73d7 | |||
| 5fedc11c8f | |||
| 5b8b7a2415 | |||
| e591e97f22 | |||
| 71e142afb4 | |||
| ca1ba04676 | |||
| 528495c75a | |||
| aa3b4a5881 | |||
| db2d8ed217 | |||
| d34a98d34b | |||
| 9ac65ccc08 | |||
| edc6660654 | |||
| a2084df11a | |||
| b59f002314 | |||
| 52793301ba | |||
| 0c77deed05 | |||
| 48619de024 | |||
| c546124029 | |||
| 673faea097 | |||
| 91ae4fa7ef | |||
| f1722274c0 | |||
| d53021ea2c | |||
| a22aaa5dfa | |||
| b1ecd9a078 | |||
| 84b59c690d | |||
| ac90b9835b | |||
| f887fac809 | |||
| c03457c814 | |||
| e9b6b63e5e | |||
| 1dc50d8cdf | |||
| 0e92085002 | |||
| f96f2b8295 | |||
| 1917dcf921 | |||
| 0ac4731903 | |||
| c42930c6d5 | |||
| 2d3ea588f1 | |||
| 5aafd943b4 | |||
| c0605fab44 | |||
| dc09959cca | |||
| 83c59ea7d3 | |||
| b45f7a0410 | |||
| 2ca1f80094 | |||
| df9a02539c | |||
| 501d59f21b | |||
| f916e7d1f9 | |||
| 462e57df3e | |||
| 4207110e2f | |||
| fdbc306cfb | |||
| 78d4b59bbe | |||
| f564fef84c | |||
| 01e38b5840 | |||
| ac1eec2e30 | |||
| 0a4b774bfe | |||
| b604aa167b | |||
| 15f5083282 | |||
| cbf031aedc | |||
| 34e18c84c1 | |||
| 36362ee6af | |||
| 5a77728a81 | |||
| 1cdf90d831 | |||
| 90c6e8ed7e | |||
| 1ff3c812a7 | |||
| 6111cb23a1 | |||
| de28718941 | |||
| 8d9a3a8dfa | |||
| 27df838513 | |||
| 36172ed20e | |||
| e1ce24aabb | |||
| e4bbedc695 | |||
| 798091b276 | |||
| 5c1aa1814c | |||
| ec26591b27 | |||
| 99338baabb |
95
README.md
95
README.md
@@ -5,18 +5,50 @@ An opinionated Go library for applications that use YAML configuration files.
|
|||||||
Linux is release-validated; macOS support is best-effort. See the
|
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) for details.
|
||||||
|
|
||||||
ConfigKit's initial loader accepts one strict YAML
|
ConfigKit's simple `Load` accepts one strict YAML source (or explicit
|
||||||
source (or explicit defaults-only input), optionally traverses explicit path and glob
|
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
|
||||||
imports, strictly composes their disjoint definitions, applies typed defaults, and
|
source groups with an independently selected main input. Sources can be host
|
||||||
can overlay one selected fixed overlay and check an application schema version. It
|
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
|
||||||
returns detached metadata and safe diagnostics.
|
Enabled explicit 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,
|
Applications may perform audited defaulting and normalization through a resolver,
|
||||||
then report domain errors and warnings through read-only semantic validation. Secret
|
then report domain errors and warnings through read-only semantic validation. Secret
|
||||||
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
|
||||||
references resolve environment names before validation, and explicit
|
references resolve environment names before validation, and explicit
|
||||||
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
secrets-directory bootstrap is available as a separate startup operation. Explicit
|
||||||
CLI override records apply above overlays. Value explanation is available through
|
CLI override records apply after inheritance and typed decoding, before application
|
||||||
detached structured records and deterministic redacted output.
|
resolution. 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.
|
||||||
|
|
||||||
|
Sources can also be explicitly placed as sequence items. Configuration
|
||||||
|
declarations and application registrations can give selected sequences scalar
|
||||||
|
identities and choose how they combine across precedence and inheritance.
|
||||||
|
Strict peers combine distinct records, while higher-priority collections can
|
||||||
|
replace, append, or upsert whole records. See the
|
||||||
|
[sequence contract](docs/config.md#strict-document-composition),
|
||||||
|
[maintained catalog examples](docs/consumers/README.md#identified-prompt-sequences),
|
||||||
|
and [ADR 0017](docs/adr/0017-collection-sequence-policy.md).
|
||||||
|
|
||||||
|
Imports are also explicit: a source may name files, search declared directories,
|
||||||
|
or select files with path globs. Applications can set load-wide import defaults
|
||||||
|
while keeping declaration-specific exceptions. Files are acquired in authored
|
||||||
|
declaration order; dependency-ready composition uses a shared stable scheduler.
|
||||||
|
ConfigKit does not search conventional application paths implicitly. See the
|
||||||
|
[import contract](docs/config.md#explicit-imports), [consumer examples](docs/consumers/README.md),
|
||||||
|
and [ADR 0015](docs/adr/0015-shared-dependency-ready-ordering.md) and
|
||||||
|
[ADR 0016](docs/adr/0016-independent-import-controls.md).
|
||||||
|
|
||||||
|
Main-file discovery is explicit too: applications choose and order `Search`
|
||||||
|
descriptions and pass `SearchConfig` to the existing load APIs. See the
|
||||||
|
[discovery contract](docs/config.md#configuration-discovery), [consumer guide](docs/consumers/README.md#explicit-main-file-discovery),
|
||||||
|
and [ADR 0019](docs/adr/0019-explicit-configuration-discovery.md).
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -33,13 +65,48 @@ func ReadConfig(path string) (Config, error) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The [configuration contract](docs/config.md) describes implemented behavior. The
|
## Design philosophy
|
||||||
[consumer adoption guide](docs/consumers/README.md) provides maintained Narratio-,
|
|
||||||
Notarius-, WeatherReporter-, and Seriatim-shaped examples and migration guidance.
|
ConfigKit favors one coherent, predictable configuration model over flexibility
|
||||||
The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions. See the
|
for reproducing each consumer's conventions. Its goal is to let applications
|
||||||
[development guide](docs/development.md) for repository orientation and the
|
load once, inspect where final values came from, and distinguish explicit values
|
||||||
[release procedure](docs/release.md) for maintainer-only candidate and publication
|
from omissions with minimal configuration bookkeeping. Applications retain their
|
||||||
steps.
|
domain rules and execution responsibilities.
|
||||||
|
|
||||||
|
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 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 applies before inheritance, while
|
||||||
|
explicit CLI overrides apply after decoding and before application resolution.
|
||||||
|
The [configuration contract](docs/config.md#strict-document-composition) owns
|
||||||
|
the detailed combination rules and input-specific constraints. Declared or
|
||||||
|
registered collections combine distinct identified records within strict tiers
|
||||||
|
and use their selected operation at precedence boundaries; other sequences keep
|
||||||
|
their strict-conflict and whole-value-replacement behavior.
|
||||||
|
|
||||||
|
The [architecture goals](docs/policy/architecture.md#success-criteria) explain
|
||||||
|
these priorities and how genuine new needs can justify extending the shared
|
||||||
|
model. The [future feature list](docs/roadmap/future.md) preserves candidates for
|
||||||
|
consideration in future releases.
|
||||||
|
|
||||||
|
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, mounted-collection, and identified-prompt examples alongside the
|
||||||
|
migration shapes. Input placement decisions are recorded in
|
||||||
|
[ADR 0011](docs/adr/0011-input-destinations.md) and sequence placement and
|
||||||
|
composition in [ADR 0014](docs/adr/0014-sequence-placement-and-composition.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.
|
||||||
|
|
||||||
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).
|
||||||
|
|
||||||
|
|||||||
@@ -43,16 +43,74 @@ func toEngineLocations(values []Location) []engine.Location {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
|
||||||
return SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Layer: Layer(value.Layer), ImportedFrom: fromEngineLocationPointer(value.ImportedFrom)}
|
return SourceInfo{
|
||||||
|
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
|
||||||
|
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
|
||||||
|
DestinationUnresolved: engine.SourceInfoDestinationUnresolvedForFacade(value),
|
||||||
|
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
|
||||||
|
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionCheck: fromEngineSourceVersionCheck(value.VersionCheck),
|
||||||
|
ImportSelections: fromEngineImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
func toEngineSourceInfo(value SourceInfo) engine.SourceInfo {
|
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
|
||||||
return engine.SourceInfo{ID: value.ID, Label: value.Label, Path: value.Path, Layer: engine.Layer(value.Layer), ImportedFrom: toEngineLocationPointer(value.ImportedFrom)}
|
if values == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]ImportSelection, len(values))
|
||||||
|
for index, value := range values {
|
||||||
|
result[index] = ImportSelection{
|
||||||
|
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
|
||||||
|
Selector: ImportSelector(value.Selector), Outcome: ImportSelectionOutcome(value.Outcome),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.Follow,
|
||||||
|
Hidden: value.Hidden,
|
||||||
|
Recursive: value.Recursive,
|
||||||
|
Pattern: value.Pattern,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromEngineSourceVersionCheck(value *engine.SourceVersionCheck) *SourceVersionCheck {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &SourceVersionCheck{
|
||||||
|
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 {
|
func fromEngineOrigin(value engine.Origin) Origin {
|
||||||
return Origin{Layer: Layer(value.Layer), Locations: fromEngineLocations(value.Locations), Operation: ChangeKind(value.Operation)}
|
result := Origin{Layer: Layer(value.Layer), Locations: fromEngineLocations(value.Locations), Operation: ChangeKind(value.Operation)}
|
||||||
}
|
if value.Inheritance != nil {
|
||||||
func toEngineOrigin(value Origin) engine.Origin {
|
result.Inheritance = make([]InheritanceStep, len(value.Inheritance))
|
||||||
return engine.Origin{Layer: engine.Layer(value.Layer), Locations: toEngineLocations(value.Locations), Operation: engine.ChangeKind(value.Operation)}
|
for index, step := range value.Inheritance {
|
||||||
|
result.Inheritance[index] = InheritanceStep{
|
||||||
|
From: fromEnginePath(step.From), To: fromEnginePath(step.To), Reference: fromEnginePath(step.Reference),
|
||||||
|
Locations: fromEngineLocations(step.Locations),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
func fromEngineDiagnostic(value engine.Diagnostic) Diagnostic {
|
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)}
|
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)}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
cmd := exec.Command("/bin/bash", filepath.Join(root, "scripts", scriptName))
|
cmd := exec.Command("/bin/bash", filepath.Join(root, "scripts", scriptName))
|
||||||
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), "CONFIGKIT_BASELINE_TOOLCHAIN=test-baseline", "CONFIGKIT_NEWER_TOOLCHAIN=", "CONFIGKIT_TEST_FMT_LOG="+log, "CONFIGKIT_TEST_GO_LOG="+calls)
|
cmd.Env = append(os.Environ(), "PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"), "CONFIGKIT_TEST_FMT_LOG="+log, "CONFIGKIT_TEST_GO_LOG="+calls)
|
||||||
// Log only targets selected by the script, not inherited host settings.
|
// Log only targets selected by the script, not inherited host settings.
|
||||||
cmd.Env = append(cmd.Env, "GOOS=", "GOARCH=")
|
cmd.Env = append(cmd.Env, "GOOS=", "GOARCH=")
|
||||||
if name == "failed-go-check" {
|
if name == "failed-go-check" {
|
||||||
@@ -108,90 +108,3 @@ func TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckScriptsReleaseGates(t *testing.T) {
|
|
||||||
script, err := os.ReadFile("scripts/release-check.sh")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, scenario := range []string{"success", "failed-test", "missing-toolchain"} {
|
|
||||||
t.Run(scenario, func(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
bin := t.TempDir()
|
|
||||||
for _, directory := range []string{"scripts", "nested"} {
|
|
||||||
if err := os.Mkdir(filepath.Join(root, directory), 0700); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checker := filepath.Join(root, "scripts", "release-check.sh")
|
|
||||||
if err := os.WriteFile(checker, script, 0700); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
calls := filepath.Join(bin, "go.calls")
|
|
||||||
goStub := `#!/bin/sh
|
|
||||||
printf '%s|%s|%s|%s|%s|%s\n' "$GOTOOLCHAIN" "$GOPROXY" "${CGO_ENABLED-}" "${GOOS-}" "${GOARCH-}" "$*" >> "$CONFIGKIT_TEST_GO_LOG"
|
|
||||||
if [ "${CONFIGKIT_TEST_FAIL-}" = toolchain ] && [ "${1-}" = version ]; then exit 37; fi
|
|
||||||
if [ "${CONFIGKIT_TEST_FAIL-}" = test ] && [ "${1-}" = test ]; then exit 37; fi
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(filepath.Join(bin, "go"), []byte(goStub), 0700); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cmd := exec.Command("/bin/bash", checker)
|
|
||||||
cmd.Dir = filepath.Join(root, "nested")
|
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
"PATH="+bin+string(os.PathListSeparator)+os.Getenv("PATH"),
|
|
||||||
"CONFIGKIT_BASELINE_TOOLCHAIN=test-baseline",
|
|
||||||
"CONFIGKIT_TEST_GO_LOG="+calls,
|
|
||||||
"GOOS=", "GOARCH=", "CGO_ENABLED=",
|
|
||||||
)
|
|
||||||
switch scenario {
|
|
||||||
case "failed-test":
|
|
||||||
cmd.Env = append(cmd.Env, "CONFIGKIT_TEST_FAIL=test")
|
|
||||||
case "missing-toolchain":
|
|
||||||
cmd.Env = append(cmd.Env, "CONFIGKIT_TEST_FAIL=toolchain")
|
|
||||||
}
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if scenario == "success" && err != nil {
|
|
||||||
t.Fatalf("checker: %v\n%s", err, output)
|
|
||||||
}
|
|
||||||
if scenario != "success" && err == nil {
|
|
||||||
t.Fatalf("checker accepted %s", scenario)
|
|
||||||
}
|
|
||||||
data, err := os.ReadFile(calls)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if !strings.HasPrefix(line, "test-baseline|off|") {
|
|
||||||
t.Fatalf("toolchain or offline setting lost: %q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if scenario == "missing-toolchain" {
|
|
||||||
if len(lines) != 1 || !strings.HasSuffix(lines[0], "|version") {
|
|
||||||
t.Fatalf("checker ran after unavailable toolchain: %q", lines)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(lines) < 2 || lines[1] != "test-baseline|off||||test -mod=readonly -vet=off -race -v ./..." {
|
|
||||||
t.Fatalf("release suite command changed: %q", lines)
|
|
||||||
}
|
|
||||||
if scenario == "failed-test" {
|
|
||||||
if len(lines) != 2 {
|
|
||||||
t.Fatalf("checker continued after failure: %q", lines)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
want := []string{
|
|
||||||
"test-baseline|off||||version",
|
|
||||||
"test-baseline|off||||test -mod=readonly -vet=off -race -v ./...",
|
|
||||||
"test-baseline|off||||vet -mod=readonly ./...",
|
|
||||||
"test-baseline|off|0|linux|amd64|build -mod=readonly ./...",
|
|
||||||
"test-baseline|off|0|linux|arm64|build -mod=readonly ./...",
|
|
||||||
}
|
|
||||||
if strings.Join(lines, "\n") != strings.Join(want, "\n") {
|
|
||||||
t.Fatalf("unexpected release gates:\n%s", data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
53
diff.go
Normal file
53
diff.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/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.
|
||||||
|
type Difference struct {
|
||||||
|
Path Path
|
||||||
|
Left ValueSnapshot
|
||||||
|
Right ValueSnapshot
|
||||||
|
HiddenChanged bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Diff compares the frozen effective values of two successful loads of the
|
||||||
|
// same configuration type. 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.
|
||||||
|
//
|
||||||
|
// Both results must be non-nil successful loads with inspection state; nil or
|
||||||
|
// zero results return an error and no records. Load errors are returned by
|
||||||
|
// Load, so callers must handle those before calling Diff. Snapshots are safe
|
||||||
|
// for presentation but are not a complete reloadable configuration.
|
||||||
|
func Diff[T any](left, right *Result[T]) ([]Difference, error) {
|
||||||
|
var leftInspection, rightInspection *engine.Inspection
|
||||||
|
if left != nil {
|
||||||
|
leftInspection = left.inspection
|
||||||
|
}
|
||||||
|
if right != nil {
|
||||||
|
rightInspection = right.inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
values, err := engine.Diff(leftInspection, rightInspection)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
differences := make([]Difference, len(values))
|
||||||
|
for index, value := range values {
|
||||||
|
differences[index] = Difference{
|
||||||
|
Path: fromEnginePath(value.Path),
|
||||||
|
Left: fromEngineValueSnapshot(value.Left),
|
||||||
|
Right: fromEngineValueSnapshot(value.Right),
|
||||||
|
HiddenChanged: value.HiddenChanged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return differences, nil
|
||||||
|
}
|
||||||
90
discovery.go
Normal file
90
discovery.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import "gitea.maximumdirect.net/eric/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...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths describes literal host-file candidates in the supplied order.
|
||||||
|
func Paths(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...)}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# ADR 0005: Record explicit CLI overrides and safe value explanation
|
# 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
|
Date: 2026-09-06
|
||||||
|
|
||||||
|
|||||||
120
docs/adr/0007-source-backed-underlays.md
Normal file
120
docs/adr/0007-source-backed-underlays.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# ADR 0007: Add Source-Backed Underlays with Consistent Recursive Precedence
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
This extends [ADR 0001](0001-separate-composition-and-overlay.md) and preserves
|
||||||
|
its distinction between strict composition and intentional overlay.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-09-29
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Configuration-backed defaults provide lower-priority settings that applications
|
||||||
|
can override. These sources carry imports, locations, and relative-resource
|
||||||
|
contexts. Integrating them into ConfigKit's loading lifecycle gives their values
|
||||||
|
the same presence tracking and inspectable provenance as other configuration
|
||||||
|
inputs.
|
||||||
|
|
||||||
|
The [architecture goals](../policy/architecture.md#success-criteria) prioritize
|
||||||
|
one coherent, predictable configuration model with consistent rules across
|
||||||
|
source representations and loading interfaces. Consumers should load once,
|
||||||
|
receive a complete validated configuration, and inspect its final values' origins.
|
||||||
|
Reducing integration code is important subject to those principles.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Add underlays to the existing precedence model
|
||||||
|
|
||||||
|
Introduce explicitly supplied source-backed underlays between typed defaults
|
||||||
|
and main configuration. The proposed precedence, from lowest to highest, is:
|
||||||
|
|
||||||
|
**Typed application defaults < source underlays < strictly composed main
|
||||||
|
configuration < selected overlay < explicit CLI overrides.**
|
||||||
|
|
||||||
|
Typed defaults remain the Go-value baseline. Underlays supply lower-priority
|
||||||
|
configuration with source provenance and, where applicable, imports and resource
|
||||||
|
context. This ordering describes value precedence; typed defaults fill fields
|
||||||
|
absent from the assembled input under the existing defaulting rules.
|
||||||
|
|
||||||
|
Preserve strict composition of the main document and its imports before applying
|
||||||
|
the resulting configuration over underlays. Independent contributions within an
|
||||||
|
underlay tier also compose strictly. Explicitly ordered tiers express intentional
|
||||||
|
precedence. Strict composition reports conflicting contributions within each tier.
|
||||||
|
|
||||||
|
Across precedence tiers, reuse the existing recursive mapping-overlay semantics.
|
||||||
|
Omitted children retain lower-priority input; existing scalar, sequence, null,
|
||||||
|
and cross-kind replacement semantics remain unchanged. Reusing these operations
|
||||||
|
keeps the combination rules consistent across the loading model.
|
||||||
|
|
||||||
|
### Keep source representation separate from composition semantics
|
||||||
|
|
||||||
|
Native `fs.FS` and programmatic inputs join the shared loading model.
|
||||||
|
Equivalent inputs using the same
|
||||||
|
operation have the same presence and precedence semantics regardless of medium.
|
||||||
|
Programmatic values enter directly as labeled configuration sources.
|
||||||
|
|
||||||
|
Retain the contributing source's identity and resource context through layering.
|
||||||
|
A retained underlay value keeps its originating source's resource context.
|
||||||
|
Consumers remain responsible for opening and interpreting their domain resources
|
||||||
|
under the supplied filesystem's lifetime and access semantics. Exact
|
||||||
|
context-access and input APIs remain roadmap decisions.
|
||||||
|
|
||||||
|
### Deliver one final configuration and its inspection metadata
|
||||||
|
|
||||||
|
Assemble effective input, then run the typed loading lifecycle once for the load.
|
||||||
|
Preserve explicit zero,
|
||||||
|
false, empty, and null input versus omission through configuration and metadata
|
||||||
|
together.
|
||||||
|
|
||||||
|
Extend existing provenance and history through underlays, defaults, and recorded
|
||||||
|
application resolution. The returned result includes the completed combination,
|
||||||
|
defaulting, and provenance work. Domain resolution and validation
|
||||||
|
remain application-owned within the recorded lifecycle, consistent with
|
||||||
|
[ADR 0003](0003-preserve-presence-and-resolution-history.md).
|
||||||
|
|
||||||
|
Preserve the existing opt-in schema-version ownership, path protections, and
|
||||||
|
check timing before precedence
|
||||||
|
assembly and typed decoding. The roadmap owns how layered input identifies that
|
||||||
|
owner when version checking is configured.
|
||||||
|
|
||||||
|
Favor near-linear processing in input, retained history, and output size through
|
||||||
|
incremental assembly of load-owned state. This is an engineering objective
|
||||||
|
subordinate to clarity and semantic consistency. Preserve the root-owned facade and
|
||||||
|
private implementation boundary established by [ADR 0006](0006-public-facade.md).
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Leave bundled defaults to consumers.** Avoids extending loading, but makes
|
||||||
|
consumers reproduce merging, presence tracking, and provenance reconciliation.
|
||||||
|
It does not meet the goal of obtaining a complete configuration in one load.
|
||||||
|
- **Offer recursive or atomic combination per layer.** Makes both behaviors
|
||||||
|
explicit and could serve more consumers, but expands the semantic surface and
|
||||||
|
its interactions with presence and provenance without a demonstrated need that
|
||||||
|
outweighs a single consistent rule. A future concrete requirement may justify
|
||||||
|
reconsideration; this feature does not add the option.
|
||||||
|
- **Flatten all documents into last-wins layers.** Simplifies ordering but turns
|
||||||
|
accidental main/import conflicts into overrides, weakening the intentional
|
||||||
|
distinction established by ADR 0001.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Consumers gain source-backed defaults within the shared loading lifecycle.
|
||||||
|
Strict contributions and intentional precedence remain distinct, with consistent
|
||||||
|
combination rules across source media. Presence and provenance remain
|
||||||
|
part of the same loading lifecycle as the final values.
|
||||||
|
|
||||||
|
Higher-priority mappings retain omitted lower-priority children, so application
|
||||||
|
validation assesses the final combined configuration.
|
||||||
|
|
||||||
|
Implementation must integrate source context and underlay history into the shared
|
||||||
|
engine with attention to assembly and history-retention costs. Current behavior
|
||||||
|
documentation will change when the feature lands. The roadmap retains ownership
|
||||||
|
of the remaining API decisions.
|
||||||
82
docs/adr/0008-complete-on-return-loading.md
Normal file
82
docs/adr/0008-complete-on-return-loading.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# ADR 0008: Synchronous, Complete-on-Return Configuration Loading
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed draft for review. The
|
||||||
|
[configuration contract](../config.md#completion-and-resource-lifetime) describes
|
||||||
|
the implemented loading lifecycle and
|
||||||
|
[source resource contexts](../config.md#source-resource-contexts).
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-09-29
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Applications need a clear point at which configuration is ready for use.
|
||||||
|
Completing configuration processing and inspection metadata together gives
|
||||||
|
applications a coherent result whose values and origins agree. A bounded loading
|
||||||
|
lifecycle also makes resource ownership and startup failures straightforward to
|
||||||
|
reason about.
|
||||||
|
|
||||||
|
ConfigKit's [architecture goals](../policy/architecture.md#success-criteria)
|
||||||
|
emphasize complete configuration, inspectable provenance, predictable behavior,
|
||||||
|
and minimal consumer bookkeeping. Applications own their execution lifecycle and
|
||||||
|
the timing of configuration acquisition.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Make loading a synchronous, complete-on-return operation. Within each load,
|
||||||
|
ConfigKit completes input assembly, defaulting, typed decoding, configured
|
||||||
|
application resolution, secret lookup, validation, and snapshotting before
|
||||||
|
returning the final configuration and metadata. Reported failures return
|
||||||
|
diagnostics and a nil result.
|
||||||
|
|
||||||
|
Release all library-owned loading handles before returning on success or failure.
|
||||||
|
The resulting configuration and metadata are independently usable, caller-owned
|
||||||
|
data with a retained inspection snapshot. Caller-supplied readers and filesystems
|
||||||
|
remain under caller ownership and lifetime control.
|
||||||
|
|
||||||
|
After the call completes, ConfigKit remains inactive until explicitly invoked
|
||||||
|
again. Applications normally load once at or near startup and may initiate
|
||||||
|
additional loads when needed. Each load follows the same lifecycle and source
|
||||||
|
reuse contracts. Inspection, rendering, and separately requested operations run
|
||||||
|
in response to explicit application calls.
|
||||||
|
|
||||||
|
For the planned resource-context capability, retain source context as passive
|
||||||
|
information or a borrowed caller-supplied filesystem. Subsequent resource access
|
||||||
|
acquires its own handles with explicit ownership. This preserves a clear boundary
|
||||||
|
between configuration completion and application resource use.
|
||||||
|
|
||||||
|
These choices give consumers one completion boundary for configuration and
|
||||||
|
provenance, predictable resource lifetimes, and control over when ConfigKit runs.
|
||||||
|
Application callbacks remain responsible for their own effects and resources.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Background watching or refresh.** Can automate responses to source changes,
|
||||||
|
but adds scheduling, shutdown, publication, and resource-lifetime concerns to
|
||||||
|
the library. Explicit calls keep those responsibilities with the application.
|
||||||
|
- **Lazy completion after return.** Can defer work until particular values are
|
||||||
|
used, but moves configuration failures into application execution and makes
|
||||||
|
the readiness of values and metadata harder to establish.
|
||||||
|
- **Transactional atomic loading.** Could imply simultaneous observation of all
|
||||||
|
external inputs and rollback of callback effects. Filesystems, environment
|
||||||
|
variables, and application callbacks do not share a transaction mechanism.
|
||||||
|
Synchronous completion provides the required usable-result boundary without
|
||||||
|
promising those external guarantees.
|
||||||
|
- **Retain loading handles in results.** Can preserve open filesystem access,
|
||||||
|
but gives configuration results an ongoing resource lifecycle and cleanup
|
||||||
|
obligation. Passive source context supports later explicit resource access
|
||||||
|
while keeping loading resources scoped to the load call.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Applications can treat successful return as the point at which the final
|
||||||
|
configuration and its metadata are ready for use. Startup loading remains simple,
|
||||||
|
and additional loads have an explicit application-owned trigger.
|
||||||
|
|
||||||
|
Configuration inspection operates on retained data. Resource access observes the
|
||||||
|
backing resource when explicitly requested and has its own failure and ownership
|
||||||
|
boundary. Implementations of new source kinds and layering features preserve the
|
||||||
|
same completion boundary, including cleanup on reported load failures.
|
||||||
82
docs/adr/0009-sibling-inheritance.md
Normal file
82
docs/adr/0009-sibling-inheritance.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# ADR 0009: Resolve Sibling Inheritance After Source Assembly
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
2026-09-29
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Named configuration mappings often share settings while retaining individual
|
||||||
|
overrides. ConfigKit already distinguishes strict composition of independent
|
||||||
|
source contributions from recursive overlay between explicit precedence tiers.
|
||||||
|
Doing another untracked merge in applications would lose omission, source
|
||||||
|
context, and historical explanation information. A shared structural operation
|
||||||
|
must preserve the same presence and provenance model as the rest of loading.
|
||||||
|
|
||||||
|
Inheritance also interacts with source precedence, nested namespaces, version
|
||||||
|
ownership, and secrets. Resolving it before source assembly would validate
|
||||||
|
superseded references and copy values that a later tier should replace. Fully
|
||||||
|
resolving nested relationships inside a reusable parent would bind them to the
|
||||||
|
wrong sibling scope when copied into a child.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Reserve `extends` as a built-in input key on mappings. A string names one exact
|
||||||
|
sibling mapping; null cancels the relationship. Complete source acquisition,
|
||||||
|
strict composition, selected overlay, and CLI precedence first. Then resolve
|
||||||
|
each effective relationship in the parser-independent document tree, before
|
||||||
|
typed defaults and decoding. A child recursively overlays its own payload on
|
||||||
|
its resolved parent using the existing mapping-combination rules. This creates
|
||||||
|
neither a source tier nor a configurable merge strategy.
|
||||||
|
|
||||||
|
Resolve enclosing sibling chains before nested relationships. Reuse memoized
|
||||||
|
parent templates with unresolved nested instructions; evaluate those instructions
|
||||||
|
at each destination. Preserve original source applications separately from
|
||||||
|
structural transfer/control evidence. Effective origins carry concrete
|
||||||
|
source-to-destination derivation, and inspection projects relevant inheritance
|
||||||
|
events between source replay and typed reconciliation. Inherited input counts as
|
||||||
|
explicit presence, while direct replacement retains its own effective origin.
|
||||||
|
|
||||||
|
The existing main-input version check remains at its original point. Reject
|
||||||
|
structural contributions or directive consumption affecting its protected path.
|
||||||
|
Classify sensitivity at corresponding source and destination paths and retain
|
||||||
|
historical redaction. Secret references remain unresolved input until the
|
||||||
|
existing lookup point. Keep all machinery behind the root-owned public facade.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Application-owned inheritance after `Load`.** Leaves every consumer to
|
||||||
|
recreate ordering, presence, provenance, error reporting, and redaction after
|
||||||
|
ConfigKit has already declared a result complete.
|
||||||
|
- **Resolve each source before precedence assembly.** Makes superseded
|
||||||
|
references active and can copy a parent definition before higher-priority
|
||||||
|
input finishes changing it.
|
||||||
|
- **Resolve nested relationships in a cached parent subtree.** Shares more
|
||||||
|
ready-made output but binds nested sibling lookups to the parent's scope,
|
||||||
|
giving incorrect values at a child destination.
|
||||||
|
- **Registered collections, configurable keywords, or multiple-parent merge
|
||||||
|
modes.** Increase public setup and semantic branches without a requirement
|
||||||
|
that warrants diverging from one structural rule.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Consumers can load a configuration once and inspect direct, inherited,
|
||||||
|
defaulted, and application-derived values through the existing result surfaces.
|
||||||
|
`extends` is no longer an ordinary application field, so existing configurations
|
||||||
|
using that key as data must migrate. Expanding many children can materialize
|
||||||
|
substantially more output than the input contains; projection and retained-history
|
||||||
|
budgets bound that work without charging copies as newly acquired sources.
|
||||||
|
Applications still own domain validation, entry selection, and runtime resource
|
||||||
|
use.
|
||||||
99
docs/adr/0010-effective-configuration-diff.md
Normal file
99
docs/adr/0010-effective-configuration-diff.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# ADR 0010: Compare Frozen Effective Configuration Snapshots
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted and implemented. The
|
||||||
|
[configuration contract](../config.md#effective-configuration-diffs) owns current
|
||||||
|
behavior. Completed planning and remediation records are preserved in Git
|
||||||
|
history at `1c2610c` under `docs/roadmap/`.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-10-09
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Applications need to inspect how two completed configurations differ, including
|
||||||
|
the effective impact of an overlay after defaults and application resolution.
|
||||||
|
ConfigKit already retains canonical typed inspection snapshots, semantic paths,
|
||||||
|
presence information, and sensitivity classification for successful loads.
|
||||||
|
|
||||||
|
Extending this inspection model lets applications share configuration comparison
|
||||||
|
and safe disclosure while keeping their presentation and domain interpretation.
|
||||||
|
The operation should follow ConfigKit's existing equality, ownership, and secret
|
||||||
|
boundaries so consumers can reason about loading and inspection consistently.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Provide structured effective-value comparison between successful results of the
|
||||||
|
same configuration type. Compare their retained canonical inspection snapshots
|
||||||
|
through the root-owned public facade. Each comparison uses the completed load
|
||||||
|
state and returns detached records with a semantic path and safe left and right
|
||||||
|
value snapshots.
|
||||||
|
|
||||||
|
Use canonical value equality and the existing structural comparison rules.
|
||||||
|
Mappings compare recursively, sequences report atomic changes, and scalar or
|
||||||
|
kind replacements report the affected region. Presence distinguishes additions
|
||||||
|
and removals from explicit null. Equal effective values produce an empty diff;
|
||||||
|
source attribution, explicit-input status, and history remain separately
|
||||||
|
inspectable through each result. Canonical pointer ordering makes reporting
|
||||||
|
deterministic.
|
||||||
|
|
||||||
|
Detect changes before masking values. Apply sensitivity from both loads to both
|
||||||
|
returned sides, including historical and dependency-derived classification.
|
||||||
|
Preserve public content where the shapes permit corresponding sensitive regions
|
||||||
|
to be identified. When a structural replacement prevents safe correspondence,
|
||||||
|
mask the complete reported replacement region on both present sides. Absence
|
||||||
|
retains its own representation.
|
||||||
|
|
||||||
|
Keep masking and hidden-change information distinct. Each record indicates
|
||||||
|
whether an actual change is withheld by redaction, allowing applications to
|
||||||
|
display a changed-sensitive-value indicator even when safe snapshots render
|
||||||
|
identically. Public-only changes alongside unchanged masked content retain that
|
||||||
|
distinction.
|
||||||
|
|
||||||
|
Typed secret references participate through their configured environment names.
|
||||||
|
Resolved credential bytes remain behind the existing explicit secret accessor.
|
||||||
|
The comparison uses the same frozen inspection boundary as effective rendering
|
||||||
|
and explanation.
|
||||||
|
|
||||||
|
ConfigKit owns comparison and paired safe disclosure. Applications own labels,
|
||||||
|
layout, and rendering of the returned changed regions. Reusing the existing
|
||||||
|
snapshot representation and engine boundary keeps the extension consistent with
|
||||||
|
the library's established inspection model.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Compare input text or separately rendered YAML.** Supports familiar textual
|
||||||
|
displays, but input text precedes composition and resolution, and independently
|
||||||
|
redacted output can hide changes or apply different disclosure rules. Canonical
|
||||||
|
comparison with paired masking represents the completed configuration.
|
||||||
|
- **Leave comparison to applications using returned Go values.** Offers direct
|
||||||
|
access to application types, but duplicates structural and redaction machinery
|
||||||
|
and observes caller mutation. Retained inspection snapshots preserve the
|
||||||
|
completed load's values and established ownership boundary.
|
||||||
|
- **Align sequence elements and return context-rich rendering data.** Enables
|
||||||
|
finer-grained displays, but requires additional correspondence and disclosure
|
||||||
|
rules. Atomic sequence records and existing value snapshots satisfy the
|
||||||
|
selected changed-region workflows with a smaller public model.
|
||||||
|
- **Compare resolved credentials or retain credential-derived tokens.** Could
|
||||||
|
detect rotation behind an unchanged reference, but expands retained sensitive
|
||||||
|
state and introduces a separate trusted comparison mechanism. Reference-name
|
||||||
|
comparison preserves the existing inspection boundary.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Applications can compare independently loaded configurations using ConfigKit's
|
||||||
|
shared equality, presence, and disclosure rules. Existing composition and load
|
||||||
|
semantics continue to determine the values being compared. Caller mutation of a
|
||||||
|
returned configuration leaves its retained comparison snapshot unchanged.
|
||||||
|
|
||||||
|
Paired redaction can conceal public content within a structurally incompatible
|
||||||
|
replacement. The changed region and hidden-change indicator still communicate
|
||||||
|
its effective impact. Credential rotation behind an unchanged environment name
|
||||||
|
produces equal reference values for comparison.
|
||||||
|
|
||||||
|
The implementation requires operation-local disclosure preparation and focused
|
||||||
|
coverage for presence, shape changes, historical sensitivity, and detached
|
||||||
|
results. Applications retain control of presentation; changed-region records
|
||||||
|
supply the safe data needed for that responsibility.
|
||||||
74
docs/adr/0011-input-destinations.md
Normal file
74
docs/adr/0011-input-destinations.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# ADR 0011: Place Configuration Inputs at Mapping Destinations
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-10-14
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Consumers assembling collections of independently authored configuration
|
||||||
|
fragments need each source to contribute beneath a selected application path.
|
||||||
|
Requiring consumers to rewrite source documents or merge completed values would
|
||||||
|
separate placement from ConfigKit's existing composition, precedence, source
|
||||||
|
identity, and inspection behavior. Placement should remain part of the ordinary
|
||||||
|
loading lifecycle and preserve the root-owned public facade.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add an optional `Destination Path` to root-owned `Input` and expose the effective
|
||||||
|
destination on existing `SourceInfo` records. The empty path means the existing
|
||||||
|
application root. A non-empty destination is a sequence of literal mapping keys;
|
||||||
|
explicit sequence-index segments are invalid. Existing reserved structural keys
|
||||||
|
remain reserved, while keys that merely contain their spelling remain ordinary
|
||||||
|
keys.
|
||||||
|
|
||||||
|
Acquire and interpret each source using its original physical identity and
|
||||||
|
source-local directives, then place its application body before strict
|
||||||
|
composition and the existing final lifecycle. Imported application bodies
|
||||||
|
inherit the input destination. Placement applies to main and underlay inputs;
|
||||||
|
ordinary root loading, selected overlays, and CLI inputs retain root placement.
|
||||||
|
The main-owned version rule continues to check its effective application path
|
||||||
|
at its existing lifecycle point.
|
||||||
|
|
||||||
|
Placement preserves authored node locations and source identity. Introduced
|
||||||
|
mapping structure belongs to the contributing source and uses that document's
|
||||||
|
root location as its source-coordinate fallback. A present empty mapping
|
||||||
|
materializes its destination; an absent `NoSource` contributes nothing. The
|
||||||
|
result continues through the existing strict composition, overlay, inheritance,
|
||||||
|
typed loading, diagnostics, provenance, history, sensitivity, and resource
|
||||||
|
context mechanisms.
|
||||||
|
|
||||||
|
The added exported fields preserve keyed and zero-value struct use. Callers
|
||||||
|
using unkeyed composite literals for `Input` or `SourceInfo` must migrate to
|
||||||
|
keyed literals. Treat this feature as minor-release work; select the version and
|
||||||
|
document the compatibility impact during separately authorized release
|
||||||
|
preparation, consistent with the release policy.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Rewrite or merge sources in consumers.** This makes each consumer reproduce
|
||||||
|
placement, strict composition, source attribution, and inspection semantics.
|
||||||
|
- **Add a separate loading entry point or public wrapper type.** This creates a
|
||||||
|
parallel API for a behavior that belongs to existing per-input loading.
|
||||||
|
- **Load each fragment as a complete application before placement.** This runs
|
||||||
|
defaults, decoding, resolution, and validation on incomplete fragments rather
|
||||||
|
than once on the assembled application.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Consumers can assemble contributions at effective mapping paths in one ordinary
|
||||||
|
layered load and inspect each source's placement through existing source
|
||||||
|
records. Paths, conflicts, and later application operations use the placed
|
||||||
|
structure while source coordinates and relative-resource contexts continue to
|
||||||
|
identify their original sources. Public field additions require release-note
|
||||||
|
guidance for callers using unkeyed literals.
|
||||||
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/`.
|
||||||
960
docs/config.md
960
docs/config.md
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,32 @@ applications. Consumers still own their configuration structs, defaults, semanti
|
|||||||
validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
validation, schema-version policy, CLI parsing, runtime matching and conversion,
|
||||||
resource checks, remote acquisition, and service initialization.
|
resource checks, remote acquisition, and service initialization.
|
||||||
|
|
||||||
|
## Choosing a loading entry point
|
||||||
|
|
||||||
|
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
|
||||||
|
[`SearchConfig`](../../discovery.go) is also a source, so its explicit
|
||||||
|
discovery result can be passed to `Load` or used as the main input to
|
||||||
|
[`LoadLayered`](../../layered.go).
|
||||||
|
- Use `LoadLayered` when inputs must be composed in underlay groups before the
|
||||||
|
main source. 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).
|
||||||
|
|
||||||
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
|
||||||
four sanitized shapes using only ConfigKit and the Go standard library:
|
sanitized shapes using only ConfigKit and the Go standard library:
|
||||||
|
|
||||||
| Shape | Demonstrated fit | Application code deliberately retained |
|
| Shape | Demonstrated fit | Application code deliberately retained |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -15,6 +39,13 @@ four sanitized shapes using only ConfigKit and the Go standard library:
|
|||||||
| 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` |
|
| 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 |
|
| 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 |
|
| 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
|
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
|
||||||
executable examples and ordinary tests. It contains no private application data,
|
executable examples and ordinary tests. It contains no private application data,
|
||||||
@@ -48,6 +79,180 @@ both without redefining their syntax. ConfigKit overlays are configuration overl
|
|||||||
they do not select or replace an application's LLM, model, pipeline, report, or
|
they do not select or replace an application's LLM, model, pipeline, report, or
|
||||||
execution profile concepts.
|
execution profile concepts.
|
||||||
|
|
||||||
|
## 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 root-owned [discovery API](../../discovery.go) and [configuration
|
||||||
|
discovery contract](../config.md#configuration-discovery) define ordered
|
||||||
|
concatenation, directory-first Cartesian expansion, optional exhaustion, and
|
||||||
|
selected-file behavior. [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`, `Follow`, `Hidden`,
|
||||||
|
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.
|
||||||
|
|
||||||
|
`Result.Sources`, `LoadError.Sources`, and `Resolution.Source` expose each
|
||||||
|
declaring source's `ImportSelections`. These records identify the source-local
|
||||||
|
declaration path, its authored location and selector, and the `none` outcome;
|
||||||
|
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.VersionCheck` 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
|
||||||
|
|
||||||
|
[`CompareFiles`](../../examples/consumers/diff.go) loads two caller-selected files
|
||||||
|
with one application definition, handles each load error, calls `configkit.Diff`,
|
||||||
|
and presents the safe records through consumer-owned `FormatDifferences`.
|
||||||
|
[`CompareRolloutOverlay`](../../examples/consumers/diff.go) loads the same base
|
||||||
|
once without an overlay and once with the selected overlay, so omitted lower
|
||||||
|
priority values remain present. Its resolver records a derived endpoint URL after
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Named sibling inheritance
|
||||||
|
|
||||||
|
[`LoadPresets`](../../examples/consumers/inheritance.go) loads the maintained
|
||||||
|
[`presets.yml`](../../examples/consumers/testdata/inheritance/presets.yml) in one
|
||||||
|
call. `custom` inherits `enabled: false` from `base`, while its nested
|
||||||
|
`settings.primary` changes the inherited `settings.secondary` result at the
|
||||||
|
custom destination. The application struct has no `extends` field; ConfigKit
|
||||||
|
consumes the relationship before strict typed decoding.
|
||||||
|
|
||||||
|
The [executable example](../../examples/consumers/consumers_test.go) reads
|
||||||
|
`Metadata.IsExplicit` for the inherited false value, then follows
|
||||||
|
`Origin.Inheritance` back to the parent path while `Origin.Locations` identifies
|
||||||
|
the declaring source. Applications still choose which preset to execute and
|
||||||
|
validate domain meaning. The [configuration contract](../config.md#sibling-inheritance)
|
||||||
|
owns exact lookup, precedence, cancellation, and migration rules.
|
||||||
|
|
||||||
|
## Native filesystem and layered input
|
||||||
|
|
||||||
|
[`LoadBundled`](../../examples/consumers/layered.go) keeps the simple `Load` shape:
|
||||||
|
it selects a file in an application-owned embedded filesystem and needs no
|
||||||
|
underlay setup. [`LoadCatalog`](../../examples/consumers/layered.go) uses
|
||||||
|
`LoadLayered` with an imported embedded-defaults group, a separate
|
||||||
|
`Mapping` group, and a host main file supplied by its caller. The groups
|
||||||
|
compose strictly within themselves and recursively across precedence boundaries;
|
||||||
|
the main file's explicit `false` and `0` replace lower values without being
|
||||||
|
mistaken for omissions. Its validator owns allowed modes and resource-name policy.
|
||||||
|
|
||||||
|
[`ReadWelcome`](../../examples/consumers/layered.go) follows the effective field's
|
||||||
|
origin location to a source ID, queries `Result.SourceContext`, and opens the
|
||||||
|
resource relative to that context's `BaseDir`. It does not assume the main file
|
||||||
|
owns a retained underlay value. A composite origin needs an application choice;
|
||||||
|
ConfigKit does not guess a directory. The context query is passive, while later
|
||||||
|
resource reads observe the caller-owned filesystem or a freshly opened host root
|
||||||
|
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. 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
|
||||||
|
second input without a destination to show that root placement remains the
|
||||||
|
default. Consumers pass paths, not filenames or namespace strings for ConfigKit
|
||||||
|
to interpret.
|
||||||
|
|
||||||
|
[`LoadCollectionBundle`](../../examples/consumers/destinations.go) assembles an
|
||||||
|
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
|
||||||
|
application-owned [`ReadCollectionPromptContent`](../../examples/consumers/destinations.go)
|
||||||
|
demonstrates the same lookup after loading; ConfigKit does not open or interpret
|
||||||
|
the resource. [`destinations_test.go`](../../examples/consumers/destinations_test.go)
|
||||||
|
checks source destinations and authored coordinates, presence, inherited values,
|
||||||
|
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
|
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
|
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-
|
fields are known; validation selects which fields apply to each module. Runtime-
|
||||||
@@ -126,6 +331,10 @@ focused behavioral owner:
|
|||||||
| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
|
| 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 |
|
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
|
||||||
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
|
| `LoadSecretsDir`, 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` |
|
||||||
|
| Effective configuration diffs and consumer-owned presentation | `ExampleDiff`, `ExampleCompareFiles`, and `ExampleCompareRolloutOverlay` |
|
||||||
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
|
||||||
|
|
||||||
The package-level import-boundary test covers the consumer examples too: supported
|
The package-level import-boundary test covers the consumer examples too: supported
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ public behavior.
|
|||||||
| Task | Read |
|
| Task | Read |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Repository orientation or component responsibility | [Component overview](internal/overview.md) |
|
| 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 |
|
| 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 |
|
| 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), [remediation roadmap](roadmap/roadmap.md), and relevant ADRs listed below |
|
||||||
| Code and test changes | The owning contract and tests from the map 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) |
|
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
|
||||||
| Release preparation | [Release procedure](release.md) |
|
| Release preparation | [Release procedure](release.md) |
|
||||||
@@ -49,11 +51,19 @@ public behavior.
|
|||||||
Accepted decisions:
|
Accepted decisions:
|
||||||
|
|
||||||
- [Separate composition and overlay](adr/0001-separate-composition-and-overlay.md)
|
- [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)
|
- [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)
|
- [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)
|
- [Secret references and explicit bootstrap](adr/0004-secret-references-and-explicit-bootstrap.md)
|
||||||
- [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.md)
|
- [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.md)
|
||||||
- [Keep the public facade root-owned](adr/0006-public-facade.md)
|
- [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)
|
||||||
|
|
||||||
For cross-cutting changes, follow every applicable row. Completed implementation
|
For cross-cutting changes, follow every applicable row. Completed implementation
|
||||||
and remediation plans are preserved in Git history at `cb5b435`; use current
|
and remediation plans are preserved in Git history at `cb5b435`; use current
|
||||||
@@ -64,8 +74,9 @@ git show cb5b435:docs/roadmap/remediation.md
|
|||||||
```
|
```
|
||||||
|
|
||||||
Release-candidate, publication, and verification work follows the operational
|
Release-candidate, publication, and verification work follows the operational
|
||||||
[release procedure](release.md). Its single publication command validates the
|
[release procedure](release.md). Its module-selected ReleaseKit `publish`
|
||||||
committed candidate and publishes only the intended refs.
|
invocation validates the committed candidate and publishes only the intended
|
||||||
|
tag; those release checks remain distinct from the everyday development check.
|
||||||
|
|
||||||
The completed string-and-integer schema-version implementation and remediation
|
The completed string-and-integer schema-version implementation and remediation
|
||||||
records are preserved in Git history at `244104f`. The
|
records are preserved in Git history at `244104f`. The
|
||||||
@@ -80,14 +91,127 @@ and current facade ownership belongs to the [component overview](internal/overvi
|
|||||||
|
|
||||||
## Baseline validation
|
## Baseline validation
|
||||||
|
|
||||||
|
The completed filesystem-source and underlay roadmap, original feature request,
|
||||||
|
implementation plan, and resolved remediation record are preserved in Git history
|
||||||
|
at `b604aa1` under `docs/roadmap/`. Current behavior belongs to the
|
||||||
|
[configuration contract](config.md#layered-loading). Built-in sibling
|
||||||
|
inheritance is now part of the [configuration contract](config.md#sibling-inheritance).
|
||||||
|
The completed request, roadmap, plan, and resolved review findings are preserved
|
||||||
|
in Git history at `83c59ea` under `docs/roadmap/`.
|
||||||
|
|
||||||
The completed overlay rename plan and release-workflow simplification record are
|
The completed overlay rename plan and release-workflow simplification record are
|
||||||
preserved in Git history at `73ce6e9` under `docs/roadmap/`. Current overlay
|
preserved in Git history at `73ce6e9` under `docs/roadmap/`. Current overlay
|
||||||
behavior belongs to the [configuration contract](config.md#selected-overlays),
|
behavior belongs to the [configuration contract](config.md#selected-overlays),
|
||||||
and release operations belong to the [release procedure](release.md).
|
and release operations belong to the [release procedure](release.md).
|
||||||
|
|
||||||
Install Go 1.25.0 or a supported patched/newer Go toolchain. Acquire declared
|
The completed ReleaseKit adoption, startup performance request and experiments,
|
||||||
dependencies explicitly before entering an offline development or validation
|
benchmark evidence, performance audit, implementation plan, and remediation
|
||||||
environment:
|
record are preserved in Git history at `8e91a2b` under `docs/roadmap/`. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git show 8e91a2b:docs/roadmap/startup-benchmark.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Remaining optimization candidates belong to
|
||||||
|
[future performance experiments](roadmap/future.md#5-performance-experiments).
|
||||||
|
|
||||||
|
The completed effective-configuration diff roadmap, implementation plan, and
|
||||||
|
resolved review findings are preserved in Git history at `1c2610c` under
|
||||||
|
`docs/roadmap/`. The
|
||||||
|
[configuration contract](config.md#effective-configuration-diffs) owns current
|
||||||
|
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's scope is recorded in the
|
||||||
|
[remediation roadmap](roadmap/roadmap.md). 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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go mod download
|
go mod download
|
||||||
@@ -117,11 +241,10 @@ Use focused checks while iterating, with validation proportionate to the change
|
|||||||
under the testing policy. Documentation-only work normally needs the local-link
|
under the testing policy. Documentation-only work normally needs the local-link
|
||||||
check below and whitespace review; behavior-bearing examples and commands also
|
check below and whitespace review; behavior-bearing examples and commands also
|
||||||
need execution. Use `go test -race ./...` for ownership or concurrency changes.
|
need execution. Use `go test -race ./...` for ownership or concurrency changes.
|
||||||
The [publication command](release.md#4-publish-the-committed-candidate) invokes
|
The [release procedure](release.md#4-publish-the-committed-candidate) performs
|
||||||
the release checker once. It performs one minimum-toolchain offline race-suite
|
its selected validation through one `publish` invocation. The everyday checker
|
||||||
run, one full vet pass, and Linux cross-builds using readonly module mode.
|
remains a development gate and is not a separate release prerequisite. CI never
|
||||||
It already covers examples, links, and package boundaries; do not additionally
|
gates a release.
|
||||||
run the everyday checker just to repeat those checks. CI never gates a release.
|
|
||||||
|
|
||||||
## Task-to-test map
|
## Task-to-test map
|
||||||
|
|
||||||
@@ -132,16 +255,29 @@ code changes. Use the component overview to find the corresponding implementatio
|
|||||||
| Task | Behavioral test owners | Focused command |
|
| 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'` |
|
| 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'` |
|
| 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'` |
|
||||||
| 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'` |
|
| 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 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'` |
|
| 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'` |
|
| 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'` |
|
| 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'` |
|
||||||
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_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'` |
|
| 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 and release checker behavior | [Shell regression](../release_check_test.go) | `go test . -run 'TestCheckScripts\|TestDevelopmentCheck'` |
|
| Development check behavior | [Development checker test](../development_check_test.go) | `go test . -run '^TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure$'` |
|
||||||
| Guarded release publication and recovery | [Publication regression](../release_publish_test.go) | `go test . -run '^TestReleasePublication'` |
|
| Release profile wiring | [Release profile test](../releasekit_profile_test.go) | `go test . -run '^TestReleaseKitProfile$'` |
|
||||||
|
|
||||||
## Fuzz validation
|
## 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,52 @@ private state; public signatures and fields do not expose engine types.
|
|||||||
|
|
||||||
Implemented component owners:
|
Implemented component owners:
|
||||||
|
|
||||||
- `internal/engine` owns source acquisition, composition, schema inspection,
|
- `internal/engine` owns host and native-filesystem source acquisition and
|
||||||
decoding, lifecycle coordination, callback auditing, provenance/history,
|
explicitly selected configuration discovery. A search resolves once during
|
||||||
secret resolution and bootstrap, and safe inspection rendering. It never
|
shared acquisition; the selected concrete file or absent-input state then
|
||||||
imports the root package.
|
follows the existing composition, metadata, and resource-context lifecycle.
|
||||||
- Root `Source`, `Path`, `Secret`, records, lifecycle callbacks, results, and
|
It also owns grouped strict composition, recursive precedence assembly,
|
||||||
bootstrap reports are root-defined wrappers or records. Callback wrappers pass
|
per-input mapping and sequence-item placement, YAML collection declarations
|
||||||
only live engine context during a callback; `Result.Config` is detached and its
|
and operations for identified sequences, and application identity registrations for strict
|
||||||
private inspection remains frozen independently.
|
composition, inheritance, and final validation. It applies the selected
|
||||||
- Root conversion files explicitly cross the path, record, callback, secret, and
|
collection operation at precedence boundaries and owns dependency-ready,
|
||||||
error boundaries. There are no public aliases, anonymous engine embeddings, or
|
destination-local inheritance before typed decoding. CLI assignment follows
|
||||||
public engine bridge methods.
|
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, 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,
|
||||||
|
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.
|
||||||
- `internal/document` owns the parser-independent ordered tree and cloning.
|
- `internal/document` owns the parser-independent ordered tree and cloning.
|
||||||
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
`internal/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
|
||||||
- Engine mechanism tests own source, composition, decoding, callback, provenance,
|
- Engine mechanism tests own source, composition, inheritance, decoding,
|
||||||
secret, history, rendering, and bootstrap behavior. Root tests own public API
|
callback, provenance,
|
||||||
compatibility, facade integration, documentation links, and dependency/public
|
source-context, secret, history, rendering, and bootstrap behavior. Root tests
|
||||||
boundary checks. Consumer examples remain under `examples/consumers`.
|
own public API compatibility, facade integration, documentation links, and
|
||||||
|
dependency/public boundary checks. Consumer examples remain under
|
||||||
|
`examples/consumers`.
|
||||||
|
|
||||||
Dependency direction is root to engine to document/YAML support. The root may
|
Dependency direction is root to engine to document/YAML support. The root may
|
||||||
import only `internal/engine` among project internal production packages; engine
|
import only `internal/engine` among project internal production packages; engine
|
||||||
@@ -32,8 +61,100 @@ these constraints and rejects exported aliases, fields, or signatures involving
|
|||||||
internal types.
|
internal types.
|
||||||
|
|
||||||
The [configuration contract](../config.md) owns implemented YAML and lifecycle
|
The [configuration contract](../config.md) owns implemented YAML and lifecycle
|
||||||
semantics. The [consumer adoption guide](../consumers/README.md) owns application
|
semantics, including
|
||||||
guidance. The [architecture policy](../policy/architecture.md) owns contributor
|
[configuration discovery](../config.md#configuration-discovery),
|
||||||
boundaries, and [ADR 0006](../adr/0006-public-facade.md) records the facade
|
[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
|
||||||
|
and [ADR 0010](../adr/0010-effective-configuration-diff.md) records the diff
|
||||||
|
decision; [ADR 0011](../adr/0011-input-destinations.md) records the placement
|
||||||
decision. See the [development guide](../development.md) for validation commands
|
decision. See the [development guide](../development.md) for validation commands
|
||||||
and [release procedure](../release.md) for source-candidate gates.
|
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. [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; 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
## Standard Repository Policies
|
## Standard Repository Policies
|
||||||
|
|
||||||
Material: `docs/policy/meta.md`, `docs/policy/oss.md`,
|
Material: `docs/policy/release.md`, adapted from
|
||||||
|
`e833faddf120c789a9b45348edaf75720de16348`; `docs/policy/meta.md`, `docs/policy/oss.md`,
|
||||||
`docs/policy/documentation.md`, `docs/policy/agents.md`,
|
`docs/policy/documentation.md`, `docs/policy/agents.md`,
|
||||||
`docs/policy/architecture.md`, `docs/policy/testing.md`, and
|
`docs/policy/architecture.md`, `docs/policy/testing.md`; root `AGENTS.md`; and the
|
||||||
`docs/policy/release.md`; root `AGENTS.md`; and the Policy Reading Guide in
|
Policy Reading Guide in `docs/development.md`, adapted from
|
||||||
`docs/development.md`, adapted for this repository.
|
`d84a38ebab26f048cafbe59a2246700c35d29843` for this repository.
|
||||||
|
|
||||||
Copyright (C) 2026 Eric Rakestraw
|
Copyright (C) 2026 Eric Rakestraw
|
||||||
|
|
||||||
@@ -14,10 +15,14 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||||||
|
|
||||||
License and notices: [CC BY-SA 4.0](licenses/CC-BY-SA-4.0.txt) |
|
License and notices: [CC BY-SA 4.0](licenses/CC-BY-SA-4.0.txt) |
|
||||||
[COPYRIGHT](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/COPYRIGHT) |
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/COPYRIGHT) |
|
||||||
[LICENSE](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/LICENSE).
|
[LICENSE](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/LICENSE) |
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/COPYRIGHT) |
|
||||||
|
[LICENSE](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/LICENSE).
|
||||||
|
|
||||||
[Source](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843).
|
[Source](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843).
|
||||||
|
|
||||||
|
[Source](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348).
|
||||||
|
|
||||||
## go-yaml v4
|
## go-yaml v4
|
||||||
|
|
||||||
Selected module: `go.yaml.in/yaml/v4` at `v4.0.0-rc.6`.
|
Selected module: `go.yaml.in/yaml/v4` at `v4.0.0-rc.6`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Base Policy Version:** 1.3
|
**Base Policy Version:** 1.3
|
||||||
|
|
||||||
**Last Updated**: 2026-09-28
|
**Last Updated**: 2026-10-22
|
||||||
|
|
||||||
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/architecture.md)
|
**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
|
ConfigKit is an opinionated Go library for consistent YAML configuration loading
|
||||||
and resolution across consuming applications. The [component overview](../internal/overview.md)
|
and resolution across consuming applications. The [component overview](../internal/overview.md)
|
||||||
owns implementation inventory, the [configuration contract](../config.md) owns
|
owns implementation inventory, the [configuration contract](../config.md) owns
|
||||||
implemented behavior, and the [deferred scope](../roadmap/deferred.md) preserves
|
implemented behavior, and the [future feature list](../roadmap/future.md) preserves
|
||||||
deferred scope.
|
candidates for future consideration.
|
||||||
|
|
||||||
## Adopted Constraints And Implemented Structure
|
## Adopted Constraints And Implemented Structure
|
||||||
|
|
||||||
@@ -32,8 +32,31 @@ structure as implemented when its changes land.
|
|||||||
Constraints must reflect deliberately selected requirements and decisions.
|
Constraints must reflect deliberately selected requirements and decisions.
|
||||||
Do not promote incidental implementation choices into architectural invariants.
|
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
|
## Success Criteria
|
||||||
|
|
||||||
|
ConfigKit prioritizes a coherent configuration model: consistency and
|
||||||
|
predictability take precedence over flexibility for accommodating individual
|
||||||
|
downstream conventions. Consumers should learn one set of rules, not different
|
||||||
|
combination or presence semantics depending on the source format or consuming
|
||||||
|
application. Distinct operations such as strict composition and intentional
|
||||||
|
overlay express distinct purposes within that model.
|
||||||
|
|
||||||
|
Consumers should be able to load once and receive a complete, validated
|
||||||
|
configuration with inspectable provenance for its final values, including
|
||||||
|
defaults and recorded application-derived values. Configuration and metadata
|
||||||
|
together must distinguish explicit zero, false, empty, and null input from
|
||||||
|
omission; ordinary Go values alone need not encode those distinctions.
|
||||||
|
Application-owned resolution belongs within the recorded loading lifecycle, so
|
||||||
|
consumers need no subsequent generic merging, defaulting, or provenance
|
||||||
|
reconstruction. This does not transfer domain execution or runtime resource
|
||||||
|
management into ConfigKit.
|
||||||
|
|
||||||
1. **Reduce application infrastructure:** Can applications delete substantial
|
1. **Reduce application infrastructure:** Can applications delete substantial
|
||||||
configuration infrastructure while retaining clear ownership of their domain
|
configuration infrastructure while retaining clear ownership of their domain
|
||||||
rules?
|
rules?
|
||||||
@@ -41,8 +64,11 @@ Do not promote incidental implementation choices into architectural invariants.
|
|||||||
configuration model, with only the flexibility needed to express genuine
|
configuration model, with only the flexibility needed to express genuine
|
||||||
application requirements?
|
application requirements?
|
||||||
|
|
||||||
Both criteria must hold. ConfigKit should not preserve divergent consumer
|
Both criteria must hold. Minimize consumer loading and processing code subject
|
||||||
conventions merely for compatibility, nor impose restrictions that prevent useful
|
to the consistency, presence, and provenance principles above, rather than
|
||||||
|
trading those principles for consumer-specific shortcuts. ConfigKit should not
|
||||||
|
preserve divergent consumer conventions merely for compatibility, nor impose
|
||||||
|
restrictions that prevent useful
|
||||||
domain information from being expressed. Prefer a canonical representation using
|
domain information from being expressed. Prefer a canonical representation using
|
||||||
existing primitives before adding another mechanism. Extend the model when a
|
existing primitives before adding another mechanism. Extend the model when a
|
||||||
concrete requirement cannot be accommodated cleanly, not simply because a
|
concrete requirement cannot be accommodated cleanly, not simply because a
|
||||||
@@ -71,23 +97,127 @@ boundaries, and keep simple consumers independent of optional composition
|
|||||||
features. The [consumer adoption guide](../consumers/README.md) demonstrates the
|
features. The [consumer adoption guide](../consumers/README.md) demonstrates the
|
||||||
implemented surface against representative simple and complex shapes.
|
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
|
## Responsibility Boundaries
|
||||||
|
|
||||||
ConfigKit owns configuration input processing and the coordination of decoding,
|
ConfigKit owns configuration input processing and the coordination of decoding,
|
||||||
resolution, validation, and inspection. Applications own their configuration
|
resolution, validation, and inspection. Applications own their configuration
|
||||||
types, field meanings, defaults, normalization, semantic validation, and schema
|
types, field meanings, defaults, normalization, semantic validation, and schema
|
||||||
version policy. Applications also own main-file search conventions, CLI wiring,
|
version policy. Applications choose and order their main-file search conventions;
|
||||||
remote acquisition, runtime initialization, and resource checks.
|
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, 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. [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
|
Library code must not depend on consuming applications or initialize their
|
||||||
runtime resources. It must not perform implicit network access, discover
|
runtime resources. It must not perform implicit network access, implicitly
|
||||||
application-specific paths, or change process-global state during ordinary `Load`.
|
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
|
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
|
invoke it during startup, and it reports writes, skips and partial failures. It
|
||||||
must never run implicitly as part of configuration loading.
|
must never run implicitly as part of configuration loading.
|
||||||
Remote data can enter through application-supplied input without introducing a
|
Remote data can enter through application-supplied input without introducing a
|
||||||
network provider into the library.
|
network provider into the library.
|
||||||
|
|
||||||
|
## Loading Lifecycle
|
||||||
|
|
||||||
|
Configuration loading is a synchronous, complete-on-return operation. A successful
|
||||||
|
load provides the final configuration and inspection metadata after the configured
|
||||||
|
processing and validation phases complete. A failed load reports diagnostics and
|
||||||
|
returns no usable partial result. The normal application pattern is one load near
|
||||||
|
startup; applications may explicitly initiate additional loads.
|
||||||
|
|
||||||
|
ConfigKit releases its library-owned loading handles before returning and remains
|
||||||
|
inactive until the application explicitly invokes it again. It performs no
|
||||||
|
background watching, refresh, or mutation of published results. Caller-owned
|
||||||
|
resources and application callback effects remain the caller's responsibility.
|
||||||
|
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 structural directives; 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
|
## Public API And Dependency Direction
|
||||||
|
|
||||||
Keep one small, coherent public surface for the supported lifecycle. Simple
|
Keep one small, coherent public surface for the supported lifecycle. Simple
|
||||||
@@ -116,23 +246,60 @@ framework on this library.
|
|||||||
|
|
||||||
## Configuration Integrity
|
## Configuration Integrity
|
||||||
|
|
||||||
Composition and intentional overlay are distinct operations. Composition must
|
Within a configuration tier, independent contributions use strict composition.
|
||||||
not silently become last-wins override through file ordering. The decision and
|
Shared mappings combine recursively. Sequences with an applicable identity
|
||||||
alternatives are recorded in [ADR 0001](../adr/0001-separate-composition-and-overlay.md).
|
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 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.
|
||||||
|
|
||||||
|
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
|
ConfigKit must define supported YAML semantics explicitly rather than inherit
|
||||||
parser defaults. Processing order and diagnostics must be deterministic for the
|
parser defaults. Processing order and diagnostics must be deterministic for the
|
||||||
same inputs and options. Invalid input must not produce a successful usable
|
same inputs and options. Invalid input must not produce a successful usable
|
||||||
configuration. Do not run application hooks on invalid typed configuration.
|
configuration. Do not run application hooks on invalid typed configuration.
|
||||||
|
|
||||||
User-controlled precedence is application defaults, composed base, selected
|
User-controlled precedence is application defaults, ordered underlays, composed
|
||||||
overlay, then explicitly supplied CLI overrides. Applications own flag parsing;
|
main input, selected overlay, then explicitly supplied CLI overrides.
|
||||||
ConfigKit owns overlay semantics and provenance. Domain resolution follows these
|
This priority order is distinct from processing order: typed defaults are applied
|
||||||
inputs and must remain recorded, not become a hidden override channel.
|
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
|
Preserve node-based composition through source assembly and inheritance until
|
||||||
decoding. Preserve presence and source information through processing. Ordinary Go zero
|
typed decoding. Preserve presence and source information through later CLI and
|
||||||
values must not be used to infer whether input was absent. Final configuration,
|
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
|
metadata, and effective inspection must agree, including application resolution
|
||||||
changes; see [ADR 0003](../adr/0003-preserve-presence-and-resolution-history.md).
|
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,
|
Safe historical values and an explanation renderer are part of the implemented contract,
|
||||||
@@ -142,6 +309,79 @@ See [ADR 0005](../adr/0005-explicit-overrides-and-safe-explanation.md).
|
|||||||
Detailed syntax and operation tables belong to the
|
Detailed syntax and operation tables belong to the
|
||||||
[current configuration contract](../config.md).
|
[current configuration contract](../config.md).
|
||||||
|
|
||||||
|
The frozen inspection boundary also supports comparing completed same-type
|
||||||
|
results without reopening sources or rerunning the load lifecycle. The
|
||||||
|
[configuration contract](../config.md#effective-configuration-diffs) owns
|
||||||
|
comparison and disclosure behavior, the [component overview](../internal/overview.md)
|
||||||
|
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
|
## Ownership And Confidentiality
|
||||||
|
|
||||||
Returned configuration and metadata must not share mutable state across loads.
|
Returned configuration and metadata must not share mutable state across loads.
|
||||||
@@ -168,7 +408,7 @@ for safe messages supplied by their hooks.
|
|||||||
Do not introduce automatic directory discovery, network providers, arbitrary
|
Do not introduce automatic directory discovery, network providers, arbitrary
|
||||||
merge strategies, additional configuration languages, or application runtime
|
merge strategies, additional configuration languages, or application runtime
|
||||||
management without concrete consumer evidence and an explicit design decision.
|
management without concrete consumer evidence and an explicit design decision.
|
||||||
The [deferred scope](../roadmap/deferred.md#explicit-non-goals-and-deferred-scope)
|
Record significant decisions to exclude capabilities, and their rationale, in
|
||||||
owns the deferred-feature inventory. Significant boundary
|
ADRs. Significant boundary changes require an ADR and corresponding
|
||||||
changes require an ADR and corresponding policy/contract updates; implementation
|
policy/contract updates; implementation convenience alone is not a reason to
|
||||||
convenience alone is not a reason to broaden the public API.
|
broaden the public API.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Base Policy Version:** 1.11
|
**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)
|
**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
|
implemented. Current architecture, user, operator, integration, and internal
|
||||||
documentation are updated when the behavior lands.
|
documentation are updated when the behavior lands.
|
||||||
|
|
||||||
`docs/roadmap/deferred.md` owns deliberate exclusions; task-specific plans
|
`docs/roadmap/future.md` owns potential enhancements and their open questions;
|
||||||
own execution order and progress. Completed plans retire during release
|
task-specific plans own execution order and progress. Record significant
|
||||||
preparation.
|
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
|
### 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
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.
|
changes, create a new ADR and update the earlier ADR's status to superseded.
|
||||||
Rejected architectural alternatives belong in the ADR; rejected product ideas
|
Rejected architectural alternatives and significant architectural exclusions
|
||||||
belong in the roadmap. Routine choices need no ADR.
|
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
|
## Maintenance
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Release Policy
|
# Release Policy
|
||||||
|
|
||||||
**Base Policy Version:** 1.4
|
**Base Policy Version:** 1.6
|
||||||
|
|
||||||
**Last Updated**: 2026-09-28
|
**Last Updated**: 2026-10-09
|
||||||
|
|
||||||
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/release.md)
|
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ This policy defines a proportionate workflow for validating and publishing an
|
|||||||
intended source revision under a fresh version. Releases should require little
|
intended source revision under a fresh version. Releases should require little
|
||||||
routine intervention, avoid repeated work, and preserve published contents.
|
routine intervention, avoid repeated work, and preserve published contents.
|
||||||
|
|
||||||
|
Adopt stronger verification or reproducibility requirements through an explicit
|
||||||
|
project-specific decision justified by the intended audience and support needs.
|
||||||
|
|
||||||
The repository-specific commands belong in `docs/release.md`. Historical release
|
The repository-specific commands belong in `docs/release.md`. Historical release
|
||||||
summaries belong in `docs/releases/`. The procedure must agree with the adopted
|
summaries belong in `docs/releases/`. The procedure must agree with the adopted
|
||||||
policy and link to it rather than introduce additional release requirements.
|
policy and link to it rather than introduce additional release requirements.
|
||||||
@@ -54,10 +57,11 @@ authoritative primary branch, completes the release. The source commit may be
|
|||||||
pushed before the tag or published together with it through an atomic push.
|
pushed before the tag or published together with it through an atomic push.
|
||||||
The procedure must choose and document the supported approach.
|
The procedure must choose and document the supported approach.
|
||||||
|
|
||||||
ConfigKit uses sequential explicit pushes: first the captured candidate to
|
ConfigKit uses sequential publication: first push the committed candidate to
|
||||||
authoritative `main`, then the annotated tag. Its
|
authoritative `main`, then invoke ReleaseKit once to validate that candidate and
|
||||||
[release procedure](../release.md) names `origin` and checks that its fetch and
|
publish its annotated tag. The [release procedure](../release.md) names
|
||||||
push destinations resolve to the authoritative URL.
|
`origin`; ReleaseKit checks its exact effective push URL before validation and
|
||||||
|
tag publication.
|
||||||
|
|
||||||
Release commands must finish at successful publication. They must not wait for
|
Release commands must finish at successful publication. They must not wait for
|
||||||
or poll CI, require successful CI status, inspect downloadable artifacts, or
|
or poll CI, require successful CI status, inspect downloadable artifacts, or
|
||||||
@@ -76,8 +80,9 @@ the tagged source. The procedure must identify those deliverables and the
|
|||||||
responsibility of their automation. Published deliverables must be traceable
|
responsibility of their automation. Published deliverables must be traceable
|
||||||
to that source and identify the same version.
|
to that source and identify the same version.
|
||||||
|
|
||||||
ConfigKit currently declares no additional deliverables. Its source-tag push
|
ConfigKit currently declares no additional deliverables. Confirmed publication
|
||||||
is the completion point.
|
of the intended source tag is the completion point. CI, macOS validation,
|
||||||
|
hosted delivery, and consumer retrieval are outside the release gates.
|
||||||
|
|
||||||
## Versioning And Compatibility
|
## Versioning And Compatibility
|
||||||
|
|
||||||
@@ -96,7 +101,8 @@ requires a deliberate update to the policy and procedure where needed.
|
|||||||
For releases at or after `v1.0.0`:
|
For releases at or after `v1.0.0`:
|
||||||
|
|
||||||
- a major release may make intentional incompatible changes;
|
- a major release may make intentional incompatible changes;
|
||||||
- a minor release may add backward-compatible behavior; and
|
- a minor release may add backward-compatible behavior or raise the Go consumer
|
||||||
|
minimum as described below; and
|
||||||
- a patch release may make backward-compatible fixes.
|
- a patch release may make backward-compatible fixes.
|
||||||
|
|
||||||
Before `v1.0.0`, a minor release may change a documented public, operational,
|
Before `v1.0.0`, a minor release may change a documented public, operational,
|
||||||
@@ -108,13 +114,53 @@ Additional compatibility promises and deliberate exceptions belong in the
|
|||||||
adopted policy or canonical contract documentation. Release notes explain
|
adopted policy or canonical contract documentation. Release notes explain
|
||||||
changes; they do not redefine current compatibility contracts.
|
changes; they do not redefine current compatibility contracts.
|
||||||
|
|
||||||
|
## Consumer Minimum And Release Toolchain
|
||||||
|
|
||||||
|
Distinguish the minimum toolchain supported for consumers from the toolchain
|
||||||
|
maintainers use to build and validate a release. Record the consumer minimum in
|
||||||
|
the language's standard metadata and explain its support meaning in canonical
|
||||||
|
documentation. Define the selected release toolchain and its setup commands in
|
||||||
|
`docs/release.md`. Toolchain selection and upgrade cadence are repository-specific
|
||||||
|
choices.
|
||||||
|
|
||||||
|
For Go repositories, normally declare the initial stable release of the selected
|
||||||
|
series as the consumer minimum, such as `go 1.26.0`. Use a higher patch minimum
|
||||||
|
when a dependency or an identified correctness fix requires it, and document
|
||||||
|
the reason.
|
||||||
|
|
||||||
|
For release-policy purposes, successful build and release validation on the
|
||||||
|
selected Go patch release are sufficient to presume support across that Go
|
||||||
|
series, subject to the declared consumer minimum and concrete evidence of
|
||||||
|
incompatibility. Investigate specific compatibility concerns as they arise.
|
||||||
|
|
||||||
|
Use the selected release toolchain for candidate validation and release builds,
|
||||||
|
including CI jobs performing those tasks. A newer validation patch version alone
|
||||||
|
leaves the consumer minimum unchanged.
|
||||||
|
|
||||||
|
A minor release may raise the Go consumer minimum, including from one Go series
|
||||||
|
to the next, both before and after `v1.0.0`. This is an explicit exception to the
|
||||||
|
backward-compatibility requirement for minor releases. Patch releases preserve
|
||||||
|
the consumer minimum. Explain an increase's impact and required consumer action
|
||||||
|
in the release note. Other incompatible changes remain subject to the versioning
|
||||||
|
rules above.
|
||||||
|
|
||||||
|
### ConfigKit Toolchain Selection
|
||||||
|
|
||||||
|
For minor and major releases, select the newest stable Go series available in
|
||||||
|
the development environment. For patch releases, retain the series recorded in
|
||||||
|
`go.mod` and its consumer minimum. Use the latest locally available compatible
|
||||||
|
patch in the selected series, with `GOTOOLCHAIN=local`, to build the
|
||||||
|
module-selected ReleaseKit CLI and validate the candidate. The same Go
|
||||||
|
executable builds the tool and runs its validation commands.
|
||||||
|
|
||||||
## Release Documentation And Preparation
|
## Release Documentation And Preparation
|
||||||
|
|
||||||
Every new release must include `docs/releases/<version>.md` in the tagged
|
Every new release must include `docs/releases/<version>.md` in the tagged
|
||||||
source tree. Use the complete version tag as the filename and begin with a
|
source tree. Use the complete version tag as the filename and begin with a
|
||||||
title containing the product name and that exact version.
|
title containing the product name and that exact version.
|
||||||
|
|
||||||
ConfigKit requires the first line `# ConfigKit <version>`.
|
ConfigKit requires the first line `# ConfigKit <version>`. ReleaseKit's
|
||||||
|
publication guard checks the release-note H1 for the product and version.
|
||||||
|
|
||||||
Include a short summary and any relevant compatibility impact or upgrade
|
Include a short summary and any relevant compatibility impact or upgrade
|
||||||
instructions. Choose sections suited to the release and keep the note
|
instructions. Choose sections suited to the release and keep the note
|
||||||
@@ -146,10 +192,9 @@ checks publication state, and publishes the intended refs. A validation-only
|
|||||||
mode or command may be provided for diagnosis, but must not be a required step
|
mode or command may be provided for diagnosis, but must not be a required step
|
||||||
before publication repeats the same checks.
|
before publication repeats the same checks.
|
||||||
|
|
||||||
ConfigKit uses `./scripts/release.sh vMAJOR.MINOR.PATCH`. The separate
|
ConfigKit uses the module-selected ReleaseKit CLI's `publish
|
||||||
`scripts/release-check.sh` remains an optional diagnostic command; the
|
vMAJOR.MINOR.PATCH` command. Its `check` command is optional for diagnosis; it
|
||||||
publication command invokes it once and does not use a persistent candidate
|
is not a required preliminary pass to publication.
|
||||||
record.
|
|
||||||
|
|
||||||
Run the complete deterministic, offline default test suite once for the final
|
Run the complete deterministic, offline default test suite once for the final
|
||||||
candidate, following the [testing policy](testing.md). For repositories that
|
candidate, following the [testing policy](testing.md). For repositories that
|
||||||
@@ -157,13 +202,17 @@ publish builds, verify the required build targets. Select those targets for
|
|||||||
the application's actual support commitments; native testing on each platform
|
the application's actual support commitments; native testing on each platform
|
||||||
and access to particular hardware are not default release prerequisites.
|
and access to particular hardware are not default release prerequisites.
|
||||||
|
|
||||||
ConfigKit's one maintained Go 1.25 baseline runs the full suite with the race
|
Prepare dependencies and build the tool before offline validation; the
|
||||||
detector and test-time vet disabled. This protects library behavior, ownership,
|
publication command is the required validation run, with no separate successful
|
||||||
and concurrent use; the suite already includes examples, consumer fixtures,
|
test or CI run required before it.
|
||||||
documentation links, and package boundaries. One full vet pass adds static
|
|
||||||
checks beyond the test-time subset. CGO-disabled Linux amd64 and arm64 builds
|
The selected profile runs the complete deterministic, offline default suite
|
||||||
cover supported configurations distinct from native race compilation. macOS
|
once with the race detector and test-time vet disabled. The suite already
|
||||||
and newer-toolchain checks are optional.
|
includes examples, consumer fixtures, documentation links, and package
|
||||||
|
boundaries. One full vet pass adds static checks beyond the test-time subset.
|
||||||
|
CGO-disabled Linux amd64 and arm64 package compilation covers supported
|
||||||
|
configurations distinct from native race compilation. macOS and newer-toolchain
|
||||||
|
checks are optional.
|
||||||
|
|
||||||
Additional gates must address a concrete risk that the existing checks do not
|
Additional gates must address a concrete risk that the existing checks do not
|
||||||
adequately cover. Do not automatically require linting, race testing, module
|
adequately cover. Do not automatically require linting, race testing, module
|
||||||
@@ -172,9 +221,10 @@ expanded platform matrices solely because they are possible. Retain such
|
|||||||
checks when their distinct protection justifies their cost. A one-time migration
|
checks when their distinct protection justifies their cost. A one-time migration
|
||||||
check must not silently become a permanent release requirement.
|
check must not silently become a permanent release requirement.
|
||||||
|
|
||||||
The selected Go commands use readonly module mode with `GOPROXY=off`; acquire
|
ReleaseKit runs its selected Go commands with readonly module mode and
|
||||||
the baseline toolchain and dependencies before validation. Formatting and
|
`GOPROXY=off`; acquire dependencies and build the tool before candidate
|
||||||
whitespace remain ordinary development checks in `scripts/check.sh`.
|
validation. Formatting and whitespace remain ordinary development checks in
|
||||||
|
`scripts/check.sh`.
|
||||||
Review module tidiness when dependencies change, not as a release gate.
|
Review module tidiness when dependencies change, not as a release gate.
|
||||||
|
|
||||||
Release tests must not require real credentials, mutable application services,
|
Release tests must not require real credentials, mutable application services,
|
||||||
@@ -206,16 +256,19 @@ Automate the checks needed to publish the intended source under a fresh version:
|
|||||||
authoritative remote;
|
authoritative remote;
|
||||||
- the worktree and index are clean on the primary release branch;
|
- the worktree and index are clean on the primary release branch;
|
||||||
- the source being published is the exact candidate that passed validation,
|
- the source being published is the exact candidate that passed validation,
|
||||||
including its matching release note;
|
including its matching release note H1;
|
||||||
- the remote primary branch can be advanced to that candidate without
|
- the remote primary branch can be advanced to that candidate without
|
||||||
discarding remote history, and is published before the tag; and
|
discarding remote history, and is published before the tag; and
|
||||||
- the tag identifies the validated candidate and only the intended refs are
|
- the tag identifies the validated candidate and only the intended refs are
|
||||||
published.
|
published.
|
||||||
|
|
||||||
ConfigKit also checks that `origin` has exactly the authoritative fetch and
|
ReleaseKit checks that `origin`'s effective push URL exactly matches the
|
||||||
push destination, including configured push URLs. The command checks the
|
authoritative URL before candidate validation. This does not assert the
|
||||||
candidate and unused version again after validation, and checks local state
|
configured fetch URL or require the release-note H1 to be the first line; the
|
||||||
before creating the tag. Ignored local files do not block publication.
|
required `# ConfigKit <version>` first-line format remains a content rule. The
|
||||||
|
command checks the candidate and unused version again after validation, and
|
||||||
|
checks local state before creating the tag. Ignored local files do not block
|
||||||
|
publication.
|
||||||
|
|
||||||
Fetch the authoritative branch and tag state as needed for these checks.
|
Fetch the authoritative branch and tag state as needed for these checks.
|
||||||
Do not force branch or tag updates to bypass a failed guard. Publish explicit
|
Do not force branch or tag updates to bypass a failed guard. Publish explicit
|
||||||
@@ -248,10 +301,10 @@ release without replacing its contents or reusing its version.
|
|||||||
## Repository Release Procedure
|
## Repository Release Procedure
|
||||||
|
|
||||||
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
|
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
|
||||||
identify prerequisites, the primary branch and authoritative remote, version
|
identify prerequisites, the selected release toolchain, the primary branch and
|
||||||
and note preparation, one publication command with local checks and guards,
|
authoritative remote, version and note preparation, one publication command with
|
||||||
source-only deliverables, the successful-tag-push completion point, and
|
local checks and guards, source-only deliverables, the successful-tag-push
|
||||||
partial or unknown-state recovery.
|
completion point, and partial or unknown-state recovery.
|
||||||
|
|
||||||
Use copyable commands where practical. Keep optional diagnostics and consumer
|
Use copyable commands where practical. Keep optional diagnostics and consumer
|
||||||
verification clearly optional. The procedure must not turn preparation work,
|
verification clearly optional. The procedure must not turn preparation work,
|
||||||
|
|||||||
298
docs/release.md
298
docs/release.md
@@ -1,7 +1,7 @@
|
|||||||
# ConfigKit Release Procedure
|
# ConfigKit Release Procedure
|
||||||
|
|
||||||
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
|
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
|
||||||
ConfigKit publishes one source-only Go module. The immutable annotated tag and
|
ConfigKit publishes a source-only Go module. The immutable annotated tag and
|
||||||
matching checked-in release note are the deliverables; there is no binary,
|
matching checked-in release note are the deliverables; there is no binary,
|
||||||
container image, generated package, or separate registry publication.
|
container image, generated package, or separate registry publication.
|
||||||
|
|
||||||
@@ -9,183 +9,221 @@ container image, generated package, or separate registry publication.
|
|||||||
|
|
||||||
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
|
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
|
||||||
- Primary branch: `main`
|
- Primary branch: `main`
|
||||||
- Authoritative remote: `origin`, with fetch and push destination
|
- Authoritative remote: `origin`
|
||||||
`https://gitea.maximumdirect.net/eric/ConfigKit.git`
|
- Authoritative push URL: `https://gitea.maximumdirect.net/eric/ConfigKit.git`
|
||||||
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
|
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
|
||||||
- Minimum Go line: Go 1.25, validated with one maintained patch
|
- 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
|
||||||
|
recorded in `go.mod`; see the
|
||||||
|
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain)
|
||||||
- Release-gated targets: Linux amd64 and arm64
|
- Release-gated targets: Linux amd64 and arm64
|
||||||
- Best-effort targets: macOS amd64 and arm64, under the
|
- Best-effort targets: macOS amd64 and arm64, under the
|
||||||
[platform support policy](policy/release.md#platform-support)
|
[platform support policy](policy/release.md#platform-support)
|
||||||
|
|
||||||
The checker runs one offline race suite, one full vet pass, and CGO-disabled
|
Release validation runs one offline race-enabled Go suite, one full vet pass,
|
||||||
builds for both Linux targets. It reports skipped filesystem cases. macOS checks
|
and CGO-disabled package compilation for both Linux targets. macOS and CI are
|
||||||
and newer-toolchain tests are optional. CI is never a readiness or completion
|
not release gates; do not wait for or poll CI.
|
||||||
gate; do not wait for or poll it.
|
|
||||||
|
|
||||||
## 1. Prepare access and tools
|
## 1. Select Go and prepare the tool
|
||||||
|
|
||||||
Use a clean checkout with Git, Bash 3.2 or newer, a maintained Go 1.25 patch,
|
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
|
||||||
and a native compiler for race tests. Configure private-host access through the
|
tests. Choose the release type before selecting Go. For a minor or major release,
|
||||||
normal credential manager. Keep credentials and environment dumps outside the
|
select the newest stable Go series available in the development environment.
|
||||||
repository and command transcript.
|
For a patch release, retain the series recorded in `go.mod` and leave its consumer
|
||||||
|
minimum unchanged. Follow the
|
||||||
|
[toolchain selection policy](policy/release.md#configkit-toolchain-selection).
|
||||||
|
|
||||||
Verify access without displaying credentials:
|
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:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git remote get-url origin
|
export GOTOOLCHAIN=local
|
||||||
git ls-remote origin HEAD
|
go version
|
||||||
|
go env GOVERSION
|
||||||
|
command -v go
|
||||||
```
|
```
|
||||||
|
|
||||||
The fetch URL and all configured push destinations must resolve to the
|
Only when a minor or major release advances the consumer minimum to a newer Go
|
||||||
authoritative URL above. The publication command checks this again. Acquire the
|
series, derive that series' initial stable release from the selected executable:
|
||||||
selected toolchain and dependencies explicitly before offline validation:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.14
|
selected_go=$(GOTOOLCHAIN=local go env GOVERSION)
|
||||||
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version
|
selected_series=${selected_go#go}
|
||||||
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download
|
selected_series=${selected_series%.*}
|
||||||
|
release_minimum="$selected_series.0"
|
||||||
|
go mod edit -go="$release_minimum"
|
||||||
```
|
```
|
||||||
|
|
||||||
A maintained patch may replace the example pin. No per-release latest-version
|
If a dependency or identified correctness fix requires a higher patch minimum,
|
||||||
survey is required. Dependencies must be cached; the checker sets `GOPROXY=off`.
|
use that required version instead and document the reason. When the series is
|
||||||
Publication requires user authorization. Standing authorization for local
|
unchanged, leave the minimum unchanged unless a minor or major release
|
||||||
commits does not authorize a push or 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.
|
||||||
|
|
||||||
## 2. Select the version and review compatibility
|
Prepare dependencies whether or not the minimum changes:
|
||||||
|
|
||||||
Choose an unused stable version. Compare the candidate with the previous tag.
|
```sh
|
||||||
Review exported Go signatures and ownership, YAML and scalar semantics,
|
go mod download
|
||||||
precedence, diagnostic codes, minimum Go version, and platform support. A
|
```
|
||||||
signature-preserving change may still affect consumers. Before `v1.0.0`,
|
|
||||||
intentional incompatible minor-release changes need a note explaining impact
|
|
||||||
and consumer action; patch releases remain compatible within their minor line.
|
|
||||||
Review the maintained consumer examples and migration guide.
|
|
||||||
|
|
||||||
When dependencies change, review parser behavior and migration impact as part
|
Build the tool selected by `go.mod` into temporary storage outside the checkout.
|
||||||
of that change. Module tidiness belongs to dependency-change review, not the
|
Ensure `$TMPDIR` names a system temporary location, not the repository. These
|
||||||
release checker. Do not repeat a dependency-version survey for every release.
|
commands never install another ReleaseKit version:
|
||||||
|
|
||||||
## 3. Write and commit the release note
|
```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
|
||||||
|
"$releasekit_bin" version
|
||||||
|
```
|
||||||
|
|
||||||
Create `docs/releases/<version>.md` for the selected version. Its first line
|
The module-selected executable is ReleaseKit v0.4.2. Use the built executable
|
||||||
must be `# ConfigKit <version>`. Give a meaningful summary and include
|
for publication to preserve its exit status and publication-state diagnostics.
|
||||||
compatibility impact or upgrade instructions when applicable. Link to current
|
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
|
||||||
contracts for exact behavior. Review the note and affected canonical documents
|
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
|
||||||
as ordinary preparation, then commit the complete candidate.
|
executable first on `PATH` through the publication command.
|
||||||
|
|
||||||
End every new note with these links, replacing `VERSION` with the complete tag:
|
Verify access to the authoritative remote without displaying credentials:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git ls-remote https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD refs/heads/main
|
||||||
|
```
|
||||||
|
|
||||||
|
Publication requires explicit authorization. Local implementation commits do
|
||||||
|
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
|
||||||
|
migration guidance. Explain any consumer-minimum increase and required consumer
|
||||||
|
action under the
|
||||||
|
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
|
||||||
|
|
||||||
|
Create `docs/releases/<version>.md`. Its first line must be exactly
|
||||||
|
`# ConfigKit <version>`, and it must end with the license and attribution footer:
|
||||||
|
|
||||||
```markdown
|
```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://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)
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm the three target files exist in the candidate and review the version in
|
Replace `VERSION` with the full tag and confirm the three target files exist in
|
||||||
the URLs. The future tag need not resolve remotely yet; this review adds no
|
the candidate. Retire completed roadmaps, preserve unfinished and deferred
|
||||||
network gate. ConfigKit uses checked-in notes without hosted release objects.
|
work, and repair affected links. Review the complete candidate for unintended
|
||||||
If a hosted page is used later, reuse the note and preserve its footer. Do not
|
files and credentials, then commit it on `main`. These are ordinary preparation
|
||||||
rewrite historical notes or add downloadable license assets.
|
tasks, not separate approval gates. Any `go.mod` change belongs in this
|
||||||
|
candidate.
|
||||||
|
|
||||||
|
## 3. Push the candidate to `main`
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
ReleaseKit requires authoritative `main` to equal this candidate before it
|
||||||
|
validates. A failure before this push means ReleaseKit publication has not
|
||||||
|
started. If the push result is uncertain, inspect authoritative `main` and
|
||||||
|
continue only when it is confirmed at the candidate. If validation later fails,
|
||||||
|
no release tag is published; correct the issue in a new commit, push the new
|
||||||
|
candidate to `main`, and invoke validation again.
|
||||||
|
|
||||||
## 4. Publish the committed candidate
|
## 4. Publish the committed candidate
|
||||||
|
|
||||||
Before publication, review compatibility, release-note accuracy, and the file
|
With the authorized candidate already on authoritative `main`, invoke the
|
||||||
list for unintended content or credentials. Retire completed roadmaps as part of
|
module-selected native executable exactly once:
|
||||||
release preparation, preserving unfinished and deferred work and repairing
|
|
||||||
links. These are ordinary preparation tasks, not separate human sign-offs.
|
|
||||||
|
|
||||||
With the release explicitly authorized, run the one publication command.
|
|
||||||
The script resolves the repository root from its own path, so it may be invoked
|
|
||||||
from another directory by using its path there. Replace the sample version with
|
|
||||||
the selected version:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/release.sh v0.5.0
|
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
|
||||||
```
|
```
|
||||||
|
|
||||||
The command requires `main`, a clean index/worktree, an unused version locally
|
Replace the placeholder with the selected version. ReleaseKit checks the
|
||||||
and on the authoritative remote, and a matching note in the captured commit.
|
effective push URL against `expected_push_url`, verifies the candidate on
|
||||||
Ignored local files do not block it. It checks remote ancestry, runs
|
`main`, and checks the release-note H1 for the product and version. It then
|
||||||
[`release-check.sh`](../scripts/release-check.sh) once against that commit,
|
runs the selected race-enabled standard suite once with test-time vet disabled,
|
||||||
rechecks candidate and version state, pushes only that commit to `main`, then
|
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
|
||||||
creates and pushes one annotated tag. The tag message is the checked-in note.
|
arm64. It publishes only the annotated tag for that candidate. Confirmed
|
||||||
It never commits or repairs the checkout.
|
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.
|
||||||
A successful intended tag push completes publication. The command reports the
|
The optional `check`
|
||||||
version and commit immediately. No CI, module retrieval, remote read-back, or
|
command is available for diagnosis but is not a separate required pass:
|
||||||
hosted release object is required. The checker may be run separately for
|
|
||||||
diagnosis, but it is not a prerequisite: a fresh publication invocation
|
|
||||||
validates its candidate once.
|
|
||||||
|
|
||||||
## 5. Recover from a failed or uncertain push
|
|
||||||
|
|
||||||
The command performs one bounded authoritative-state check after a failed push.
|
|
||||||
If the main push failed but authoritative main equals the candidate, it proceeds.
|
|
||||||
If the tag push failed but its exact tag object reached the remote, it reports
|
|
||||||
success. Otherwise it reports branch-only, conflicting, or unknown state and
|
|
||||||
retains any local tag.
|
|
||||||
|
|
||||||
Inspect exact refs before any retry. Set `release_version` to the attempted
|
|
||||||
version and compare main with the candidate from the command's report:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
release_version=v0.5.0
|
"$releasekit_bin" check vMAJOR.MINOR.PATCH
|
||||||
authoritative=https://gitea.maximumdirect.net/eric/ConfigKit.git
|
|
||||||
if remote_main=$(git ls-remote --refs --exit-code "$authoritative" refs/heads/main); then
|
|
||||||
printf 'remote main: %s\n' "$remote_main"
|
|
||||||
else
|
|
||||||
result=$?
|
|
||||||
if [ "$result" -eq 2 ]; then
|
|
||||||
printf 'authoritative main confirmed absent\n' >&2
|
|
||||||
else
|
|
||||||
printf 'remote main state unknown; inspect before retrying\n' >&2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
ref="refs/tags/$release_version"
|
|
||||||
if remote_tag=$(git ls-remote --refs --exit-code "$authoritative" "$ref"); then
|
|
||||||
printf 'remote tag: %s\n' "$remote_tag"
|
|
||||||
else
|
|
||||||
result=$?
|
|
||||||
if [ "$result" -eq 2 ]; then
|
|
||||||
printf 'remote tag confirmed absent\n'
|
|
||||||
else
|
|
||||||
printf 'remote tag state unknown; do not reuse the version\n' >&2
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if local_tag=$(git rev-parse --verify "refs/tags/$release_version" 2>/dev/null); then
|
|
||||||
printf 'local tag object: %s\n' "$local_tag"
|
|
||||||
else
|
|
||||||
printf 'no local tag object\n'
|
|
||||||
fi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A query failure is not proof that the tag is absent. Compare the remote tag
|
Status 0 confirms publication, status 1 reports a known publication failure or
|
||||||
object ID with the local annotated tag object ID; its peeled commit alone is
|
conflict, status 2 reports usage, profile, or version errors, and status 3 means
|
||||||
insufficient to establish identical publication. A conflicting published tag
|
the push outcome remains unknown. Preserve both the native exit status and its
|
||||||
must not be moved or replaced. If remote main advanced but the tag was not
|
diagnostics.
|
||||||
published, report that partial progress. Only after confirming the local tag
|
|
||||||
was never published or used by another channel may a maintainer remove it:
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
below using normal shell quoting. Check that Git still resolves it to itself,
|
||||||
|
then query the authoritative refs once:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
authoritative='COPY_CAPTURED_ENDPOINT'
|
||||||
|
release_version='vMAJOR.MINOR.PATCH'
|
||||||
|
test "$(git ls-remote --get-url -- "$authoritative")" = "$authoritative" &&
|
||||||
|
git ls-remote --refs -- "$authoritative" refs/heads/main "refs/tags/$release_version"
|
||||||
|
```
|
||||||
|
|
||||||
|
A failed URL-equality check or remote query leaves the outcome unresolved.
|
||||||
|
Correct transport configuration before continuing; a rewritten destination
|
||||||
|
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
|
||||||
|
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
|
||||||
|
the reported candidate, remove a retained local tag only after confirming it
|
||||||
|
was not published or used by another channel; then rerun `publish`, which
|
||||||
|
revalidates the candidate:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git tag -d "$release_version"
|
git tag -d "$release_version"
|
||||||
|
"$releasekit_bin" publish "$release_version"
|
||||||
```
|
```
|
||||||
|
|
||||||
A subsequent invocation revalidates the candidate; a correctly published tag
|
A validation failure after the candidate reached `main` leaves an untagged
|
||||||
needs no rerun. Do not roll back main or automatically delete a tag. Transport
|
candidate there. Correct it with a new commit rather than rewriting or rolling
|
||||||
or authentication failure alone does not require a new version. A defect in
|
back `main`. A conflicting published tag must never be moved or reused. A
|
||||||
published source requires a corrected commit, a new semantically appropriate
|
defect in published source requires a corrected commit, a new semantically
|
||||||
version and note, and the release procedure again.
|
appropriate version, and a matching release note.
|
||||||
|
|
||||||
## 6. Optional consumer verification
|
## 6. Optional consumer verification
|
||||||
|
|
||||||
Only when useful or requested, read back the remote tag or retrieve the exact
|
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
|
version into a new module without a local `replace` directive. These checks are
|
||||||
are optional and non-gating. Do not poll CI or wait for module propagation.
|
optional and non-gating. Do not poll CI or wait for module propagation. For
|
||||||
Private-host credentials must already be configured. For direct-origin
|
direct-origin verification, use the exact published version in a temporary
|
||||||
verification, set the version to the release just published (replace the sample
|
consumer module:
|
||||||
below), then use a temporary consumer module and check the exact module path
|
|
||||||
and version reported by `go list -m`:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
release_version=v0.5.0
|
release_version=vMAJOR.MINOR.PATCH
|
||||||
consumer_dir=$(mktemp -d)
|
consumer_dir=$(mktemp -d)
|
||||||
(
|
(
|
||||||
cd "$consumer_dir"
|
cd "$consumer_dir"
|
||||||
|
|||||||
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)
|
||||||
53
docs/releases/v0.5.0.md
Normal file
53
docs/releases/v0.5.0.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# ConfigKit v0.5.0
|
||||||
|
|
||||||
|
This minor release adds source-backed layered configuration and built-in sibling
|
||||||
|
inheritance, so applications can load one complete configuration while retaining
|
||||||
|
presence, source context, and explainable derivation.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- `LoadLayered` combines ordered underlay groups beneath the main input, selected
|
||||||
|
overlay, and explicit CLI overrides. Inputs within a group still compose
|
||||||
|
strictly; precedence tiers combine mappings recursively. The main input
|
||||||
|
remains the owner of the optional schema-version check.
|
||||||
|
- `NewFilesystem(...).File(...)` accepts caller-owned `fs.FS` sources, and
|
||||||
|
`Mapping(...)` snapshots supported built-in values as a labeled source.
|
||||||
|
`Result.SourceContext` and `Resolution.SourceContext` expose passive resource
|
||||||
|
contexts for values from file-backed sources.
|
||||||
|
- `extends` now resolves a mapping against one named sibling after source
|
||||||
|
precedence and before typed decoding. Inherited values count as explicit
|
||||||
|
input. Origins, history, explanations, and redacted rendering retain the
|
||||||
|
source and the structural path by which a value reached its destination.
|
||||||
|
Review corrections preserve immutable nested-history evidence and bounded
|
||||||
|
expansion work.
|
||||||
|
|
||||||
|
See the [configuration contract](../config.md#layered-loading),
|
||||||
|
[inheritance rules](../config.md#sibling-inheritance), and
|
||||||
|
[consumer examples](../consumers/README.md) for current behavior.
|
||||||
|
|
||||||
|
## Compatibility and upgrade
|
||||||
|
|
||||||
|
This is an intentional pre-v1 incompatible minor release for configurations
|
||||||
|
that use `extends` as ordinary data. That key is now reserved wherever an input
|
||||||
|
mapping occurs; it is consumed before decoding. Rename any such YAML key and
|
||||||
|
corresponding Go field, then rebuild. No collection registration or opt-in flag
|
||||||
|
is available. Configurations without `extends` retain their existing meaning.
|
||||||
|
|
||||||
|
Applications adopting the new features may move source-backed defaults into
|
||||||
|
explicit `Underlay` groups, use `SourceContext` for relative resources, or
|
||||||
|
replace application-owned sibling merging with one `extends` relationship per
|
||||||
|
mapping. Adoption is optional. The maintained
|
||||||
|
[named-preset example](../consumers/README.md#named-sibling-inheritance)
|
||||||
|
shows inherited `false` and destination-local nested resolution.
|
||||||
|
|
||||||
|
Select the new module version:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/ConfigKit@v0.5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The minimum Go line remains 1.25. Linux amd64 and arm64 remain the
|
||||||
|
release-validated targets; macOS remains best-effort. The YAML dependency is
|
||||||
|
unchanged at `go.yaml.in/yaml/v4 v4.0.0-rc.6`.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.5.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.5.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.5.0/docs/oss/attribution.md)
|
||||||
63
docs/releases/v0.6.0.md
Normal file
63
docs/releases/v0.6.0.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# ConfigKit v0.6.0
|
||||||
|
|
||||||
|
This minor release reduces avoidable configuration-loading and inspection work,
|
||||||
|
adopts ReleaseKit for source publication, and raises the consumer Go minimum to
|
||||||
|
1.26.0. The public configuration model remains unchanged from v0.5.0.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Reduce temporary path allocations by sizing pointer-formatting buffers,
|
||||||
|
combining final metadata traversal, reusing parsed sensitivity paths, and
|
||||||
|
avoiding path construction when detecting inheritance. Effective and
|
||||||
|
historical metadata indexes retain independently owned paths.
|
||||||
|
- Replace repeated resolution-coverage, metadata-update, CLI-overlap, and
|
||||||
|
sensitivity-propagation scans with private indexes and incremental processing.
|
||||||
|
Preserve diagnostic ordering, history, contextual path shapes, and redaction.
|
||||||
|
- Make presence-only metadata queries avoid materializing provenance. Prepare
|
||||||
|
resolution replay descriptors and immutable mapping and inheritance-history
|
||||||
|
lookups for inspection, and use ordered membership indexes for provenance
|
||||||
|
collections. Returned records remain detached; explanations still replay
|
||||||
|
complete trees and render on demand.
|
||||||
|
- Add a maintained public-API startup construction benchmark. The initial path
|
||||||
|
formatting and metadata traversal experiments reproduced allocation reductions
|
||||||
|
across local and downstream workloads. Later algorithmic improvements were
|
||||||
|
validated behaviorally without a new benchmark campaign. Busy-host timings
|
||||||
|
were inconclusive; no fixed speedup or allocation threshold is promised.
|
||||||
|
- Replace ConfigKit-owned release scripts with a small profile and the
|
||||||
|
module-selected ReleaseKit v0.3.0 tool. The
|
||||||
|
[release procedure](../release.md) documents candidate preparation, tag-only
|
||||||
|
publication, and failed-push recovery. Existing release checks and the
|
||||||
|
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 [performance candidates](../roadmap/future.md#5-performance-experiments).
|
||||||
|
|
||||||
|
## Compatibility and upgrade
|
||||||
|
|
||||||
|
The consumer minimum increases from Go 1.25.0 to **Go 1.26.0**. Install Go 1.26.0
|
||||||
|
or newer before upgrading, and update application build and CI environments that
|
||||||
|
still use Go 1.25. The `go.mod` directive records the consumer minimum; using a
|
||||||
|
newer validation patch does not itself raise that minimum. See the
|
||||||
|
[consumer minimum policy](../policy/release.md#consumer-minimum-and-release-toolchain).
|
||||||
|
|
||||||
|
Select this version and rebuild:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/ConfigKit@v0.6.0
|
||||||
|
```
|
||||||
|
|
||||||
|
No application-code or configuration migration is required from v0.5.0. Public
|
||||||
|
Go signatures, strict within-tier composition, recursive tier precedence,
|
||||||
|
sibling inheritance, presence/null distinctions, provenance, secret handling,
|
||||||
|
and synchronous completion retain their existing contracts. See the
|
||||||
|
[configuration contract](../config.md) and
|
||||||
|
[consumer guide](../consumers/README.md).
|
||||||
|
|
||||||
|
The YAML dependency remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`. ReleaseKit is a
|
||||||
|
declared Go tool dependency for maintainer commands, not a library runtime
|
||||||
|
import; its requirements participate in ConfigKit's module graph. Linux amd64
|
||||||
|
and arm64 remain release-validated targets, and macOS remains best-effort.
|
||||||
|
Maintainers should use the current release procedure instead of the retired
|
||||||
|
`scripts/release.sh` and `scripts/release-check.sh` commands.
|
||||||
|
|
||||||
|
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.6.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.6.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.6.0/docs/oss/attribution.md)
|
||||||
58
docs/releases/v0.7.0.md
Normal file
58
docs/releases/v0.7.0.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# ConfigKit v0.7.0
|
||||||
|
|
||||||
|
This minor release adds structured comparison of completed configurations.
|
||||||
|
Applications can inspect changes between two configuration files or the
|
||||||
|
effective impact of a selected overlay, with ConfigKit handling comparison and
|
||||||
|
safe value disclosure while applications own presentation.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- Add `Diff[T](left, right *Result[T]) ([]Difference, error)` through the public
|
||||||
|
facade. It compares frozen effective inspection values from successful loads,
|
||||||
|
including defaults and recorded application resolution, rather than source
|
||||||
|
text or later mutations of `Result.Config`.
|
||||||
|
- Return detached changed-region records containing a semantic path, safe left
|
||||||
|
and right `ValueSnapshot` values, and `HiddenChanged`. Mappings compare
|
||||||
|
recursively, sequences report atomic changes, and records follow canonical
|
||||||
|
pointer order. Presence distinguishes additions and removals from explicit
|
||||||
|
null; equal effective values produce no records regardless of provenance or
|
||||||
|
loading history.
|
||||||
|
- Protect both snapshots with sensitivity from both loads, including historical
|
||||||
|
and dependency-derived classification. Compatible regions retain public
|
||||||
|
siblings; incompatible structural replacements use complete-region masking
|
||||||
|
where required. `HiddenChanged` identifies actual changes withheld by masking,
|
||||||
|
rather than merely indicating that a snapshot contains redacted content.
|
||||||
|
- Preserve visible public scalar changes when only historical sensitive
|
||||||
|
descendants remain, including additions and removals. Historical explanation
|
||||||
|
redaction and masking of incompatible present shapes remain intact.
|
||||||
|
- Add maintained file-comparison and base-versus-overlay examples with
|
||||||
|
consumer-owned formatting, plus an executable public API example. See the
|
||||||
|
[consumer guide](../consumers/README.md#effective-configuration-diffs) and
|
||||||
|
[configuration contract](../config.md#effective-configuration-diffs).
|
||||||
|
|
||||||
|
## Compatibility and upgrade
|
||||||
|
|
||||||
|
No application-code or configuration migration is required from v0.6.0.
|
||||||
|
Adopting the new API is optional. Loading, composition, inheritance, provenance,
|
||||||
|
existing inspection, and secret-handling contracts remain unchanged.
|
||||||
|
|
||||||
|
Load both configurations successfully before calling `Diff`; nil or
|
||||||
|
uninitialized results return an error with no records. Comparison does not
|
||||||
|
reopen sources, rerun callbacks, or reread environment variables. Typed secret
|
||||||
|
references compare by configured reference name, not resolved credential bytes,
|
||||||
|
so credential rotation behind an unchanged name is not reported. Returned
|
||||||
|
snapshots describe changed regions, not cross-protected full-document context.
|
||||||
|
|
||||||
|
Select this version and rebuild:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/ConfigKit@v0.7.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.3.0. 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.7.0/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.7.0/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.7.0/docs/oss/attribution.md)
|
||||||
22
docs/releases/v0.7.1.md
Normal file
22
docs/releases/v0.7.1.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# ConfigKit v0.7.1
|
||||||
|
|
||||||
|
This patch updates the maintainer release tool to ReleaseKit v0.4.1. ConfigKit's
|
||||||
|
runtime behavior and public API are unchanged.
|
||||||
|
|
||||||
|
## Compatibility and upgrade
|
||||||
|
|
||||||
|
No application-code or configuration migration is required from v0.7.0. The
|
||||||
|
consumer minimum remains Go 1.26.0, and the YAML dependency is unchanged.
|
||||||
|
|
||||||
|
Select this version and rebuild:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/ConfigKit@v0.7.1
|
||||||
|
```
|
||||||
|
|
||||||
|
ReleaseKit v0.4.1 is a maintainer tool dependency and does not change consumer
|
||||||
|
configuration or runtime behavior. 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.7.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.7.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.7.1/docs/oss/attribution.md)
|
||||||
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)
|
||||||
@@ -1,28 +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 initial release deliberately excludes:
|
|
||||||
|
|
||||||
- automatic `conf.d` discovery;
|
|
||||||
- remote imports, arbitrary providers, and additional configuration formats;
|
|
||||||
- custom merge strategies, sequence merging, reset operators, and 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 diffs and 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.
|
|
||||||
93
docs/roadmap/future.md
Normal file
93
docs/roadmap/future.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 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. Separate glob entry preparation from traversal
|
||||||
|
|
||||||
|
Implemented in the [internal-boundary roadmap](internal-boundaries.md#glob-entry-preparation) (C03).
|
||||||
|
|
||||||
|
## 2. Simplify redundant facade-to-engine transport
|
||||||
|
|
||||||
|
Implemented in the [internal-boundary roadmap](internal-boundaries.md#facade-to-engine-transport) (F07).
|
||||||
|
|
||||||
|
## 3. 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.
|
||||||
|
|
||||||
|
## 4. 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.
|
||||||
|
|
||||||
|
## 5. 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?
|
||||||
296
docs/roadmap/implementation.md
Normal file
296
docs/roadmap/implementation.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Import Discovery And Facade Simplification Implementation Plan
|
||||||
|
|
||||||
|
Status: complete.
|
||||||
|
Baseline: `bb5540b62e382106d59678e2edb7655acfd5c960`.
|
||||||
|
Audience: a Luna coding agent implementing the stages sequentially.
|
||||||
|
|
||||||
|
## Scope And Execution
|
||||||
|
|
||||||
|
Implement **C03** and **F07** from the
|
||||||
|
[internal-boundary roadmap](internal-boundaries.md). That document owns the target
|
||||||
|
state and preservation requirements; this plan owns concrete private choices,
|
||||||
|
steps, validation, and progress. The preceding 29-finding remediation sprint is
|
||||||
|
complete. Its implementation record remains at
|
||||||
|
`bb5540b:docs/roadmap/implementation.md`; this plan does not reopen it.
|
||||||
|
|
||||||
|
Read `AGENTS.md`, [agent policy](../policy/agents.md), the [development
|
||||||
|
guide](../development.md), and applicable architecture, testing, and documentation
|
||||||
|
policies. Use graph-first discovery and verify actual callers before deleting
|
||||||
|
helpers. Preserve unrelated work.
|
||||||
|
|
||||||
|
This is one end-to-end sprint. Intermediate stages may fail to compile, especially
|
||||||
|
the coordinated engine/facade cutover in Stages 2–3. Do not create adapters,
|
||||||
|
aliases, duplicate paths, or extra abstractions solely to make intermediate
|
||||||
|
stages green. Run focused checks once their affected surface is buildable; record
|
||||||
|
pending checks and finish them before final acceptance. Commit focused nonempty
|
||||||
|
units under repository policy and record stage status, actual validation, and
|
||||||
|
known commit identifiers here. Completion requires the integrated result, not
|
||||||
|
just checked-off stages.
|
||||||
|
|
||||||
|
### Boundaries
|
||||||
|
|
||||||
|
- Preserve public root-owned declarations and signatures, YAML syntax, loading
|
||||||
|
semantics, ordinary/layered differences, callback order, diagnostics, source
|
||||||
|
identity, inspection, confidentiality, and ownership.
|
||||||
|
- Preserve glob literal-versus-discovered rules and directory/glob differences.
|
||||||
|
Keep completed-link reuse, directory failure phases, containment, and existing
|
||||||
|
accounting. This is not a discovery algorithm or permission-policy change.
|
||||||
|
- Keep `resolvedImportPolicy`, registration compilation, normalized request
|
||||||
|
responsibilities, and conversions with real ownership or domain meaning.
|
||||||
|
Simplification is limited to defaults transport and placement construction.
|
||||||
|
- Add no dependencies, caller controls, caches, limits, background work, release
|
||||||
|
gates, or performance promises. Do not publish a release, update consumers, or
|
||||||
|
revise accepted ADR decisions.
|
||||||
|
|
||||||
|
### Selected Ownership Model
|
||||||
|
|
||||||
|
Use the existing engine `ImportDefaults` name and its exported `Required`,
|
||||||
|
`Follow`, `Hidden`, `Recursive`, and `Pattern` fields as the sole engine
|
||||||
|
input-defaults type. Root `ImportDefaults` stays separately defined. Root
|
||||||
|
conversion maps the fields without cloning them; the public loading wrappers
|
||||||
|
immediately enter engine preflight. `loadSettings` takes the per-load snapshot
|
||||||
|
before acquisition or callbacks, as it already does. The normalized request and
|
||||||
|
acquisition policy then borrow that load-owned read-only value.
|
||||||
|
|
||||||
|
Declaration resolution reads the owned defaults and retains its existing copies
|
||||||
|
of selected pattern/exclusion slices and destinations in `resolvedImportPolicy`.
|
||||||
|
This separates presence-aware load input from independently owned effective
|
||||||
|
declaration policy. No public copy-count or thread-safety guarantee follows.
|
||||||
|
|
||||||
|
For placement, expose the engine's existing enum as `PlacementMode uint8` with
|
||||||
|
`PlacementModeMapping` and `PlacementModeSequenceItem`, and expose placement
|
||||||
|
fields on engine input/options records for direct construction. These remain
|
||||||
|
internal-package implementation surfaces. Root types stay root-owned; direct
|
||||||
|
casts preserve unknown values for existing preflight validation.
|
||||||
|
|
||||||
|
### Validation Conventions
|
||||||
|
|
||||||
|
Run from the repository root with an installed compatible Go toolchain and
|
||||||
|
dependencies already available. Use `GOTOOLCHAIN=local`, `GOPROXY=off`, and
|
||||||
|
`GOTELEMETRY=off`. Keep disposable artifacts outside the repository and do not
|
||||||
|
change dependencies for tooling. Focused expressions identify behavior owners;
|
||||||
|
include a newly named regression explicitly if needed. Reuse unchanged successful
|
||||||
|
checks. No timing, allocation, exact syscall, or helper-shape assertions are
|
||||||
|
acceptance criteria.
|
||||||
|
|
||||||
|
## Stage 1 — Extract Patterned Glob Entry Preparation
|
||||||
|
|
||||||
|
Finding: **C03**. Owners: `internal/engine/glob.go`, existing glob selection,
|
||||||
|
link, and directory failure tests. Status: **complete**. Validation passed:
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Glob|Directory|Link|Filesystem|Resource'` and `git diff --check`.
|
||||||
|
|
||||||
|
1. Add `prepareGlobEntry` beside the selector. Take the filesystem access,
|
||||||
|
base path, parent `selectedImportCandidate`, `fs.DirEntry`, pattern segment,
|
||||||
|
`last` flag, and `resolvedImportPolicy`. Return a candidate, a boolean meaning
|
||||||
|
selected versus ordinary skip, and a nullable `*Diagnostic`. Use the parent's
|
||||||
|
existing `site`, which is propagated from the selector's root candidate.
|
||||||
|
No candidate-set, accounting, or traversal-state mutation belongs here.
|
||||||
|
2. Move only the patterned branch's per-entry body into this helper. Preserve
|
||||||
|
its order: `path.Match`; match/hidden filtering; lexical/access path creation
|
||||||
|
and containment; `importEntryTarget` and existing diagnostic translation;
|
||||||
|
follow filtering; intermediate-directory eligibility; host/native physical
|
||||||
|
path selection and canonicalization fallback; candidate label/site creation.
|
||||||
|
Return existing safe codes, paths, and sites. Do not turn inspection errors
|
||||||
|
into skips or skip validation merely because exclusion will later match.
|
||||||
|
3. Replace that inline body with the helper call and candidate insertion.
|
||||||
|
Leave pattern validation, `explicitGlobCandidate`, directory enumeration,
|
||||||
|
per-segment union/sorting, final-file validation, exclusions, requiredness,
|
||||||
|
and source-admission timing in the selector.
|
||||||
|
4. Keep directory entry preparation separate. Reuse existing filesystem/link
|
||||||
|
primitives; do not introduce a generic entry strategy or unify differing
|
||||||
|
missing-target, literal-link, filtering, or recursion rules. Remove imports
|
||||||
|
only when genuinely unused after extraction.
|
||||||
|
|
||||||
|
Acceptance: the selector now expresses traversal without embedding preparation
|
||||||
|
details. Reuse `glob_selection_test.go`, `glob_test.go`, filesystem-link tests,
|
||||||
|
and the preceding sprint's directory-phase regressions. They must still cover
|
||||||
|
literal hidden/link components versus wildcard filtering, aliases and native
|
||||||
|
spelling, escaping/cyclic links, matched-directory errors before exclusion,
|
||||||
|
selection order, and bounded accounting. Add a behavioral case only for a gap
|
||||||
|
identified during extraction, not a unit test asserting the new helper exists.
|
||||||
|
|
||||||
|
Focused check:
|
||||||
|
`go test ./internal/engine -run 'Glob|Directory|Link|Filesystem|Resource'`.
|
||||||
|
|
||||||
|
## Stage 2 — Consolidate The Engine Defaults Representation
|
||||||
|
|
||||||
|
Finding: **F07**, engine representation. Owners: `internal/engine/import_spec.go`,
|
||||||
|
`load.go`, `layered.go`, `load_request.go`, `acquisition.go`, `options.go`, and
|
||||||
|
their callers/fixtures. Status: **complete**. Validation passed:
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'` and `git diff --check`. Root facade callers remain pending Stage 3.
|
||||||
|
|
||||||
|
1. Retain engine `ImportDefaults` with its existing five exported fields.
|
||||||
|
Remove private `importDefaults`, without a compatibility alias. Update
|
||||||
|
engine option/request/acquisition fields, helper signatures, readers, and
|
||||||
|
fixture literals to use the canonical type and exported default-field names.
|
||||||
|
Private containing fields such as `loadRequest.importDefaults` can keep their
|
||||||
|
names. Do not rename the distinct fields of `importDeclarationSpec` or
|
||||||
|
`resolvedImportPolicy` just because they have similar meanings.
|
||||||
|
2. Expose `ImportDefaults ImportDefaults` on engine `Options[T]` and
|
||||||
|
`LayeredOptions[T]`. Remove `OptionsWithImportDefaults`,
|
||||||
|
`LayeredOptionsWithImportDefaults`, and `privateImportDefaults`. Stage 3 changes
|
||||||
|
root callers; a temporary compile failure is preferable to an interim bridge.
|
||||||
|
3. Adapt `snapshotImportDefaults` to the canonical type, retaining nil boolean
|
||||||
|
pointers and nil versus non-nil empty pattern. Keep `cloneBool` and
|
||||||
|
`cloneStrings` where they still have real consumers. Keep current snapshot
|
||||||
|
call sites until Stage 3 establishes their final ownership placement.
|
||||||
|
4. Keep effective declaration policy resolution and validation semantics intact:
|
||||||
|
explicit false beats defaults, each control resolves independently, and
|
||||||
|
selector-specific applicability and depth checks stay unchanged. Do not
|
||||||
|
combine defaults with resolved policy or registration compilation.
|
||||||
|
|
||||||
|
Acceptance: one engine input-defaults type reaches every production path and
|
||||||
|
fixture; no equivalent private record or alias remains. Inspect ordinary,
|
||||||
|
layered, transitive-import, directory/glob, and test-helper call sites. The root
|
||||||
|
cutover and removal of redundant snapshots are explicitly pending until Stage 3.
|
||||||
|
|
||||||
|
Run the import/default focused tests after Stage 3 if the root cutover prevents
|
||||||
|
building the package here; do not report that deferred check as passed.
|
||||||
|
|
||||||
|
## Stage 3 — Simplify Facade Defaults Construction And Snapshot Ownership
|
||||||
|
|
||||||
|
Finding: **F07**, defaults handoff. Owners: root `conversion.go`, `lifecycle.go`,
|
||||||
|
`layered.go`; engine defaults/preflight/coordinator owners; `facade_import_defaults_test.go`
|
||||||
|
and `internal/engine/import_spec_test.go`. Status: **complete**. Validation passed:
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'ImportDefaults|FacadeImport|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...`, and `git diff --check`.
|
||||||
|
|
||||||
|
1. Set engine `ImportDefaults` fields directly in root `toEngineOptions` and
|
||||||
|
`toEngineLayeredOptions`. Retain `toEngineImportDefaults` as the explicit
|
||||||
|
root-to-engine type mapping, but map its booleans/pattern without copies.
|
||||||
|
Remove root `cloneImportDefaultBool` and `cloneImportDefaultStrings` after
|
||||||
|
confirming no other caller. Keep public root types and declaration contracts.
|
||||||
|
2. Keep `snapshotImportDefaults` in `loadSettings` as the load-entry ownership
|
||||||
|
operation for both APIs. Remove repeated snapshots in engine `LoadLayered`
|
||||||
|
request construction, `executeLoadRequest` acquisition-policy construction,
|
||||||
|
and `resolveImportPolicy`. Carry the owned defaults read-only thereafter.
|
||||||
|
Review all callers of those helpers, including test setup; direct declaration
|
||||||
|
resolution can read its input because it returns its own concrete policy.
|
||||||
|
3. Keep per-policy copies of the selected pattern, exclusions, and destination.
|
||||||
|
Do not borrow caller-owned storage beyond preflight, mutate the shared owned
|
||||||
|
defaults, or remove copying from unrelated option/record paths. Each new load
|
||||||
|
snapshots its own current inputs; add no reuse cache or ownership flag.
|
||||||
|
4. Replace the private conversion-snapshot fragment in
|
||||||
|
`TestImportDefaultsPreserveEmptyPatternAndRequiredness`, which converts options,
|
||||||
|
mutates inputs, and calls engine `Load`. It should no longer demand a snapshot
|
||||||
|
from a private conversion helper. Keep that test's public nil/empty,
|
||||||
|
requiredness, and inspection-evidence checks.
|
||||||
|
5. Protect the public snapshot contract with a compact ordinary/layered table:
|
||||||
|
use a main document with a version and imports, enable
|
||||||
|
`SourceVersionModeIfPresent`, and use a configuration type with version and
|
||||||
|
imported-value fields. In the main version-check callback, mutate the caller's
|
||||||
|
recursive default from true to false and its existing pattern element from
|
||||||
|
`*.cfg` to `*.skip`. This callback runs after preflight but before imported
|
||||||
|
discovery. Verify the original recursive/pattern selection still supplies a
|
||||||
|
value from a nested imported file. Use existing fixture helpers and no
|
||||||
|
concurrent mutation. A subsequent load with those changed defaults must omit
|
||||||
|
that imported contribution rather than reuse the prior snapshot. Keep fixtures
|
||||||
|
reusable or create fresh readers for each load.
|
||||||
|
6. Reuse or extend the small snapshot/policy tests for all four optional booleans,
|
||||||
|
explicit false, pattern mutation, nil, and non-nil empty pattern. Preserve
|
||||||
|
existing invalid-pattern-before-read/hook tests and declaration override,
|
||||||
|
transitive, underlay, and selected-overlay coverage. Do not assert exact copy
|
||||||
|
counts, pointer identities, or the private conversion boundary as public policy.
|
||||||
|
|
||||||
|
Acceptance: the engine/facade cutover builds; public defaults remain detached
|
||||||
|
before acquisition/callbacks, reused loads remain independent, and only genuine
|
||||||
|
ownership copies remain. Confirm the absence of full defaults snapshots at the
|
||||||
|
three removed downstream sites by source review, not production instrumentation.
|
||||||
|
|
||||||
|
Focused checks:
|
||||||
|
`go test ./internal/engine -run 'Import|Glob|Directory|SourceVersion'` and
|
||||||
|
`go test . -run 'ImportDefaults|FacadeImport|SourceVersion'`.
|
||||||
|
|
||||||
|
## Stage 4 — Construct Placement Directly Across The Facade
|
||||||
|
|
||||||
|
Finding: **F07**, placement handoff. Owners: engine `destination.go`, `load.go`,
|
||||||
|
`layered.go`, normalized input and placement consumers; root `lifecycle.go`,
|
||||||
|
`layered.go`, and placement fixtures. Status: **complete**. Validation passed:
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Destination|Placement|Item|SourceVersion'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'Placement|Destination|SourceInfo|SourceVersion|PublicFacadeBoundary|DependencyImportBoundary'`, `env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`.
|
||||||
|
|
||||||
|
1. Rename the existing engine enum and its constants to `PlacementMode`,
|
||||||
|
`PlacementModeMapping`, and `PlacementModeSequenceItem`, preserving underlying
|
||||||
|
`uint8` values and zero semantics. Use the same type throughout engine
|
||||||
|
placement parsing, normalization, acquisition, source evidence, and version
|
||||||
|
projection. Do not add a second raw-mode record or type alias.
|
||||||
|
2. Expose engine `Options[T].Destination` and `.PlacementMode`, and
|
||||||
|
`Input.PlacementMode`. Normalized `loadInput` and other private owner fields
|
||||||
|
can retain their names. Populate the normalized input from those fields.
|
||||||
|
Remove `OptionsWithPlacement` and `InputWithPlacement` and migrate all callers,
|
||||||
|
including the direct engine construction in root `records_test.go`.
|
||||||
|
3. In root `toEngineOptions` and `toEngineInput`, set destination using the existing
|
||||||
|
`toEnginePath` conversion and placement using
|
||||||
|
`engine.PlacementMode(options.PlacementMode)` or its input equivalent.
|
||||||
|
Preserve invalid enum values for `normalizeInput` to diagnose before reads.
|
||||||
|
Do not add validation/defaulting to the facade or a new setter/cast function.
|
||||||
|
4. Preserve normalization of literal/parsed destination keys, root-item rejection,
|
||||||
|
wrapper accounting, mounted/item source coordinates, and version projection.
|
||||||
|
Root `Path` is immutable and `toEnginePath` carries its private engine value;
|
||||||
|
keep normalization and actual mutable ownership copies. Do not redesign path
|
||||||
|
conversion or add redundant clones solely to replace the deleted decorator.
|
||||||
|
5. Leave source-result transport such as `SourceInfoPlacementModeForFacade`,
|
||||||
|
source-version conversion, sequence registration preparation, and opaque
|
||||||
|
records alone. They are not input setters targeted by this step. Adapt tests
|
||||||
|
mechanically to new engine construction without weakening public assertions.
|
||||||
|
|
||||||
|
Acceptance: both public loading APIs retain zero/mapping and sequence-item
|
||||||
|
placement, errors before input consumption, final values, source metadata, and
|
||||||
|
history. Reuse the public placement tests plus destination/accounting,
|
||||||
|
item-import, mounted-version, and invalid-option cases. Root signatures still
|
||||||
|
contain no engine types, and engine exports remain below the internal boundary.
|
||||||
|
|
||||||
|
Focused checks:
|
||||||
|
`go test ./internal/engine -run 'Destination|Placement|Item|SourceVersion'` and
|
||||||
|
`go test . -run 'Placement|Destination|SourceInfo|SourceVersion|PublicFacadeBoundary|DependencyImportBoundary'`.
|
||||||
|
|
||||||
|
## Stage 5 — Integrate, Document, And Verify The Result
|
||||||
|
|
||||||
|
Findings: final acceptance for **C03, F07**. Status: **complete**. Validation
|
||||||
|
passed on Linux/amd64 (Go 1.26.10):
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off ./scripts/check.sh`,
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...`, and
|
||||||
|
`git diff --check`. Existing integration coverage was reviewed; no additional
|
||||||
|
case was needed.
|
||||||
|
|
||||||
|
1. Review both paths together: defaults resolving wildcard discovery in ordinary,
|
||||||
|
layered, and transitive imports; placement carrying the resulting sources
|
||||||
|
through composition and inspection; and failures stopping before later hooks.
|
||||||
|
Existing integration tests own these interactions. Add only a concrete missing
|
||||||
|
case rather than a second copy of the complete matrix or a new fixture framework.
|
||||||
|
2. Inspect the resulting handoffs. Confirm the glob selector no longer embeds
|
||||||
|
per-entry preparation, the engine has one defaults input type, direct fields
|
||||||
|
replaced the four input decorators, and the load snapshot is not repeated at
|
||||||
|
each private representation. Keep domain-specific policy/registration and
|
||||||
|
root conversion boundaries, necessary ownership copies, and existing budgets.
|
||||||
|
3. Update `docs/internal/imports.md` for the selector/preparation split and
|
||||||
|
`docs/internal/overview.md` for defaults conversion, preflight snapshot ownership,
|
||||||
|
and direct placement construction. Keep algorithms private and avoid public
|
||||||
|
performance or copy-count guarantees. No ADR/policy change is needed.
|
||||||
|
4. Replace C03/F07 detail in the first two `docs/roadmap/future.md` entries with
|
||||||
|
short pointers to `internal-boundaries.md`. Retain their heading anchors for
|
||||||
|
existing audit links; leave other future candidates unchanged. Correct old
|
||||||
|
roadmap references that would otherwise mistake this replacement plan for
|
||||||
|
the previous sprint's completed record: use
|
||||||
|
`bb5540b:docs/roadmap/implementation.md` for that historical plan. Do not reopen
|
||||||
|
the prior audit or retire unrelated planning records in this sprint.
|
||||||
|
5. Format changed Go files, finish deferred checks, run the final checks below,
|
||||||
|
and inspect the complete diff for API, dependency, semantics, or scope changes.
|
||||||
|
Record actual results and platform limitations here. Commit focused nonempty
|
||||||
|
units and hand off a clean workspace; do not create empty validation commits.
|
||||||
|
|
||||||
|
Final validation, following the existing development guide:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off ./scripts/check.sh
|
||||||
|
env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./...
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
The everyday check includes consumers, documentation links, and public/dependency
|
||||||
|
boundary tests. Confirm those existing tests ran rather than adding redundant
|
||||||
|
final invocations. Report filesystem/platform skips accurately; express macOS
|
||||||
|
validation, CI completion, release publication, extended fuzz campaigns, and
|
||||||
|
benchmarks are not additional acceptance requirements.
|
||||||
|
|
||||||
|
No product or architectural question remains open. The concrete choices above
|
||||||
|
complete the plan without changing the roadmap's public contracts; private
|
||||||
|
formatting and file organization can follow local idiom.
|
||||||
346
docs/roadmap/interface-remediation.md
Normal file
346
docs/roadmap/interface-remediation.md
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
# Interface Contract Remediation Roadmap
|
||||||
|
|
||||||
|
Status: target settled, including shared history ownership; implementation has
|
||||||
|
not begun.
|
||||||
|
Planning baseline: `1f64673` (v0.10.2 implementation plus documentation changes).
|
||||||
|
|
||||||
|
This roadmap covers the four findings in the [interface audit](interfaces.md).
|
||||||
|
The audit owns reproductions, evidence, and finding numbers. This document owns
|
||||||
|
the combined target, required changes, and general dependency order. A subsequent
|
||||||
|
implementation plan should own concrete stages, private implementation choices,
|
||||||
|
validation commands, and progress.
|
||||||
|
|
||||||
|
## Purpose And Scope
|
||||||
|
|
||||||
|
Restore agreement between ConfigKit's presence, metadata, history, and diagnostic
|
||||||
|
interfaces under their existing contracts. The first three findings concern
|
||||||
|
semantic paths that share a JSON Pointer spelling; the fourth concerns a
|
||||||
|
composition claimant order that is lost during diagnostic canonicalization.
|
||||||
|
|
||||||
|
Build one shared internal history model from which metadata history and
|
||||||
|
explanation are derived. It will own the historical evidence and semantic
|
||||||
|
attribution used by both views, replacing their independent reconstruction of
|
||||||
|
that attribution. This is the selected resolution of OD-1.
|
||||||
|
|
||||||
|
The [Path declarations](../../path.go), [metadata contract](../config.md#layered-metadata),
|
||||||
|
[resolution contract](../config.md#recorded-application-resolution), and
|
||||||
|
[strict-composition contract](../config.md#strict-document-composition) remain
|
||||||
|
authoritative. This work changes the implementation of those contracts without
|
||||||
|
selecting a new public API, path syntax, precedence rule, or configuration
|
||||||
|
restriction. The existing root-owned facade and engine responsibilities remain.
|
||||||
|
|
||||||
|
## Current Baseline And Targeted End State
|
||||||
|
|
||||||
|
An explicit mapping-key path and an explicit sequence-index path can both render
|
||||||
|
as `/shape/0`. `ParsePath` deliberately leaves numeric tokens contextual, whereas
|
||||||
|
`Keys` and `Index` preserve the caller's explicit choice. The engine already has
|
||||||
|
shape-aware effective/historical path indexes and a frozen resolution-entry view,
|
||||||
|
but some queries and metadata updates still use pointer spelling alone.
|
||||||
|
|
||||||
|
History currently reconstructs definition and previous-origin records, while
|
||||||
|
explanation replays source contributions, inheritance, decoding checkpoints, CLI
|
||||||
|
assignments, and grouped resolution snapshots. Neither public representation
|
||||||
|
contains everything needed by the other. The target is shared underlying evidence
|
||||||
|
with distinct projections for each interface, preserving their existing behavior.
|
||||||
|
|
||||||
|
| Finding | Current discrepancy | Target |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 1: resolution presence | `IsExplicit` and `Default` consult string-keyed presence after schema validation, which cannot determine actual shape beneath dynamic values. | Presence decisions use the matching semantic path in the fixed resolution-entry state. |
|
||||||
|
| 2: effective explicitness | Resolution retains old field information when pointer spelling survives a container-shape change. | Input explicitness carries forward for the same semantic path; a newly introduced key or index does not inherit another path's explicitness. |
|
||||||
|
| 3: history attribution | A historical-shape check admits the query, then `History` returns the entire textual-pointer bucket; explanation selects evidence separately. | Both interfaces derive attribution from one shared history model, selecting exact definitions, previous origins, and applicable ancestor operations by semantic path. |
|
||||||
|
| 4: claimant order | Composition constructs related locations in assembly order, then canonicalization sorts them by location. | Structured and rendered composition conflicts preserve the selected claimant order. |
|
||||||
|
|
||||||
|
For example, if input contains `shape: {'0': old}` and recorded application
|
||||||
|
resolution changes `shape` to a sequence, the old key remains an input path in
|
||||||
|
the callback's entry view. The new index is effective in final metadata but was
|
||||||
|
not explicitly supplied by that input. Its history must not present the old key's
|
||||||
|
definition as its own. This distinction concerns container shape, not identity
|
||||||
|
tracking for records that move within a sequence.
|
||||||
|
|
||||||
|
## Required Changes
|
||||||
|
|
||||||
|
### 1. Use Entry-State Shape For Resolution Presence
|
||||||
|
|
||||||
|
In [resolution.go](../../internal/engine/resolution.go), align `IsExplicit` and
|
||||||
|
the explicitness decision in `Default` with the frozen, shape-aware entry metadata
|
||||||
|
already used by `InputInfo` in
|
||||||
|
[load_metadata.go](../../internal/engine/load_metadata.go).
|
||||||
|
|
||||||
|
Reuse the existing semantic-path matching machinery. A shared private lookup can
|
||||||
|
avoid divergent presence decisions without requiring every public query to have
|
||||||
|
identical error behavior. In particular:
|
||||||
|
|
||||||
|
- Preserve `IsExplicit`'s schema-path validation and its existing diagnostics.
|
||||||
|
`InputInfo` remains a query that returns false for missing or mismatched paths
|
||||||
|
without those diagnostics.
|
||||||
|
- Preserve `Default`'s helper-target validation, assignment, recording, and final
|
||||||
|
helper-value checks. Correct its presence gate; do not bypass the other checks.
|
||||||
|
- Use entry-state shape even after application code changes the working value.
|
||||||
|
Explicit false, zero, empty, and null retain their existing presence meaning.
|
||||||
|
- Keep callback lifetime and detached metadata behavior intact.
|
||||||
|
|
||||||
|
A shape mismatch beneath a schema-valid dynamic path must not become explicit
|
||||||
|
merely because its pointer spelling matches an input path. Contextually parsed
|
||||||
|
numeric tokens continue to resolve against the entry state's actual containers.
|
||||||
|
|
||||||
|
### 2. Preserve Explicitness By Semantic Path During Resolution
|
||||||
|
|
||||||
|
Update resolution metadata application and field refresh in
|
||||||
|
[load_metadata.go](../../internal/engine/load_metadata.go) to compare the relevant
|
||||||
|
entry and final shapes before reusing presence information.
|
||||||
|
|
||||||
|
The comparison must account for container kinds along the full path. A mapping
|
||||||
|
key becoming a sequence index at an ancestor can change a descendant's semantic
|
||||||
|
path even when its last segment and full pointer spelling stay unchanged.
|
||||||
|
|
||||||
|
Keep explicitness for matching surviving paths, including descendants covered by
|
||||||
|
an ancestor normalization record. New semantic paths introduced by resolution
|
||||||
|
remain non-explicit, and retain the applicable recorded resolution origin.
|
||||||
|
Removed paths leave effective metadata while remaining available to historical
|
||||||
|
queries under the existing contract. Do not clear every descendant's presence
|
||||||
|
just because its parent was normalized.
|
||||||
|
|
||||||
|
Use the existing before/after projection and entry metadata where possible.
|
||||||
|
This work does not require tracking every transient mutation inside a callback;
|
||||||
|
the existing net-change and coalesced-record model continues to apply.
|
||||||
|
|
||||||
|
### 3. Build A Shared Internal History Model
|
||||||
|
|
||||||
|
#### Evidence And Ownership
|
||||||
|
|
||||||
|
Give historical evidence one private owner in the engine. Composition,
|
||||||
|
inheritance, decoding, CLI assignment, and resolution continue to perform their
|
||||||
|
existing work and supply evidence to that owner. The model does not drive a
|
||||||
|
second configuration load or repeat application callbacks.
|
||||||
|
|
||||||
|
Organize the evidence into ordered groups with the phase boundaries required by
|
||||||
|
the existing inspection contracts:
|
||||||
|
|
||||||
|
| Evidence group | Information to preserve |
|
||||||
|
| --- | --- |
|
||||||
|
| Default baseline | Canonical typed application defaults or Go-zero values and their default attribution. |
|
||||||
|
| Source contributions | Underlay, main-tier, and selected-overlay contributions in their existing order; exact definitions, composite origins, displaced definitions, raw policy-validated values, and sequence-position projections. |
|
||||||
|
| Inheritance | Parent transfers, child contributions including equal-valued replacements, controlling directives and their consumption, identity-field preservation, and contribution origins/dependencies. |
|
||||||
|
| Typed checkpoints | Pre-CLI and post-CLI typed projections and attribution needed to reconcile retained defaults and raw contributions with decoded values. |
|
||||||
|
| CLI contributions | Explicit assignments, including equal-valued assignments, raw supplied representation, source attribution, and affected paths. |
|
||||||
|
| Resolution | Original recorded operations, reasons, dependencies, and before/after evidence, with the grouping needed to explain overlapping records as net subtree changes. |
|
||||||
|
|
||||||
|
The logical order remains baseline, source contributions, inheritance, pre-CLI
|
||||||
|
decoding reconciliation, CLI contributions, post-CLI typed reconciliation, then
|
||||||
|
recorded resolution. The two typed checkpoints belong on their respective sides
|
||||||
|
of the CLI contribution phase.
|
||||||
|
|
||||||
|
The groups describe logical inspection chronology, not a transcript of every
|
||||||
|
physical mutation. Retain the existing net-change and coalescing rules for
|
||||||
|
resolution. Preserve superseded raw input that need not decode into the final
|
||||||
|
application type; typed checkpoints do not replace that evidence.
|
||||||
|
|
||||||
|
Use a small set of ordinary private structures and existing document/path
|
||||||
|
mechanisms. Exact types, indexes, and storage layout belong in the implementation
|
||||||
|
plan. Complete evidence means enough for the existing history, explanation, and
|
||||||
|
internal attribution consumers; it does not add persistence, a public event API,
|
||||||
|
or recording of transient application effects.
|
||||||
|
|
||||||
|
#### Shared Selection And Attribution
|
||||||
|
|
||||||
|
Retain concrete semantic-path evidence when it is available. A query over the
|
||||||
|
model must distinguish exact definitions from operations affecting an ancestor
|
||||||
|
or descendant, determine the relevant previous origins, and apply sequence
|
||||||
|
contribution coordinates at their assigned positions.
|
||||||
|
|
||||||
|
The affected semantic path and the operation's reported path are distinct facts.
|
||||||
|
For example, a replacement at `/shape` can remove a mapping key and introduce a
|
||||||
|
sequence index with the same pointer spelling. Preserve enough before/after shape
|
||||||
|
evidence to answer each explicit query correctly. Merely filtering `Change.Path`
|
||||||
|
after concrete segment kinds have been discarded is insufficient.
|
||||||
|
|
||||||
|
Make this selection and attribution common to both projections, including
|
||||||
|
inheritance integration and `Previous` reconstruction. A false definition must
|
||||||
|
not survive as a previous origin after it has been excluded from the query's
|
||||||
|
history. Preserve genuine ancestor replacement/removal effects and chronology.
|
||||||
|
A path that never existed does not acquire history solely because an ancestor
|
||||||
|
changed.
|
||||||
|
|
||||||
|
Keep the contextual meaning of parsed numeric tokens across historical states
|
||||||
|
and the one-entry-per-canonical-pointer enumeration of `HistoricalPaths`.
|
||||||
|
Indexes and compact derived records may remain, but their attribution must come
|
||||||
|
from the shared evidence. Metadata queries should not require full configuration
|
||||||
|
replay or YAML rendering to obtain definition and origin records.
|
||||||
|
|
||||||
|
#### Distinct Public Projections
|
||||||
|
|
||||||
|
`Metadata.History` produces detached definition, inheritance, and resolution
|
||||||
|
records from the model. `Explain` reconstructs and safely renders values from
|
||||||
|
the same evidence, retaining its existing decoding and grouping behavior. Neither
|
||||||
|
public API should be implemented by translating the other's public result.
|
||||||
|
|
||||||
|
Preserve these intentional differences:
|
||||||
|
|
||||||
|
| Case | Metadata history | Explanation |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Several strict contributions to a mapping | Composite definition/origin information. | Each contribution's effect on the mapping. |
|
||||||
|
| Raw input differs from canonical typed values | Definition and attribution records. | The applicable decoding reconciliation transition. |
|
||||||
|
| Overlapping resolution records | Specific recorded attribution under the existing coverage rules. | One net subtree event with combined reasons and dependencies. |
|
||||||
|
| Equal-valued input or inheritance operations | Explicit definition and operation attribution remains visible. | Relevant operations remain visible even when before and after values are equal. |
|
||||||
|
|
||||||
|
Agreement means consistent semantic identity and attribution, not identical
|
||||||
|
record lists. Retain original resolution records as evidence from which net
|
||||||
|
replay groups are derived; a grouped explanation alone loses information needed
|
||||||
|
by metadata.
|
||||||
|
|
||||||
|
#### Replace Existing Attribution Machinery
|
||||||
|
|
||||||
|
The common model must replace independent historical attribution, rather than
|
||||||
|
become another representation alongside it. The implementation plan should map
|
||||||
|
the following owners to their replacements:
|
||||||
|
|
||||||
|
| Current owner | Required change |
|
||||||
|
| --- | --- |
|
||||||
|
| History-building portions of `metadataFromDecoded`, `applyOverlayMetadata`, and `appendPostDecodeCLIHistory` in [load_metadata.go](../../internal/engine/load_metadata.go) and [override_context.go](../../internal/engine/override_context.go) | Produce or project shared evidence instead of independently reconstructing pointer-keyed histories. Retain effective-field construction where it is still needed. |
|
||||||
|
| Previous-origin reconstruction and inheritance insertion in [inheritance_history.go](../../internal/engine/inheritance_history.go) | Use common semantic selection and attribution while preserving the existing inheritance projection rules. |
|
||||||
|
| Relevance and chronology orchestration in [explain.go](../../internal/engine/explain.go) | Consume the shared model; retain useful value reconstruction and redaction helpers. |
|
||||||
|
| Separate published inheritance evidence for metadata and explanation in [load_result.go](../../internal/engine/load_result.go) | Consolidate under the frozen history owner where independent copies are unnecessary; keep required output detachment. |
|
||||||
|
| Original and coalesced resolution evidence in [history.go](../../internal/engine/history.go) | Own original records in the model and derive the replay grouping without losing specific attribution or maintaining a second independently interpreted history. |
|
||||||
|
|
||||||
|
Existing source, sequence-combination, and inheritance evidence producers may
|
||||||
|
remain useful. Their algorithms need not be rewritten merely to contribute to
|
||||||
|
the model. Likewise, final effective fields, the frozen resolution-entry view,
|
||||||
|
source records, and final configuration snapshots retain their responsibilities.
|
||||||
|
Findings 1 and 2 still require explicit presence corrections.
|
||||||
|
|
||||||
|
#### Other Consumers, Lifetime, And Resource Accounting
|
||||||
|
|
||||||
|
Move historical dependency reads in
|
||||||
|
[secret classification](../../internal/engine/secret.go) and historical location
|
||||||
|
fallbacks in [override_context.go](../../internal/engine/override_context.go) to
|
||||||
|
the shared evidence. Do not retain the old history machinery solely for those
|
||||||
|
readers. Preserve existing sensitivity propagation and historical classification;
|
||||||
|
correcting attribution does not authorize narrowing redaction for paths that
|
||||||
|
share a pointer spelling.
|
||||||
|
|
||||||
|
Evidence must be available at the lifecycle stages where these internal readers
|
||||||
|
need it, including diagnostic attribution before publication. Keep the callback's
|
||||||
|
entry metadata fixed while later resolution evidence is added. Finalize the
|
||||||
|
model for published queries without introducing lazy mutation of shared state.
|
||||||
|
Private frozen evidence may be shared by the two views while their returned
|
||||||
|
records remain detached under the existing ownership contract.
|
||||||
|
|
||||||
|
Reuse retained trees and existing lookup machinery where practical. Review
|
||||||
|
retained-history accounting against the resulting ownership graph, including
|
||||||
|
typed checkpoints and inheritance/resolution evidence. The model does not require
|
||||||
|
a full document copy per event, a new quota, or a new concurrency guarantee.
|
||||||
|
Reduced duplication is a design benefit to evaluate, not a promised memory or
|
||||||
|
speed improvement.
|
||||||
|
|
||||||
|
### 4. Preserve Composition Claimant Order Through Diagnostics
|
||||||
|
|
||||||
|
Keep the order produced by `compositionConflictDiagnostic` in
|
||||||
|
[compose.go](../../internal/engine/compose.go) through accumulation, `LoadError`,
|
||||||
|
facade conversion, and `WriteDiagnostics` in
|
||||||
|
[diagnostic.go](../../internal/engine/diagnostic.go).
|
||||||
|
|
||||||
|
Prefer a narrow internal canonicalization rule based on the diagnostic semantics
|
||||||
|
already carried across these boundaries. Identify the composition diagnostic
|
||||||
|
producers governed by the claimant-order contract rather than changing all
|
||||||
|
related-location lists. Preserve canonical ordering of diagnostic records and
|
||||||
|
the existing sorting of related locations for other diagnostics.
|
||||||
|
|
||||||
|
The root renderer converts public diagnostic records back to engine records and
|
||||||
|
canonicalizes them again. An engine-only marker discarded by facade conversion
|
||||||
|
would therefore be insufficient. No public ordering option is needed for this
|
||||||
|
correction. Keep primary selection, conflict detection, acquisition order, and
|
||||||
|
dependency-ready scheduling unchanged.
|
||||||
|
|
||||||
|
## General Implementation Order
|
||||||
|
|
||||||
|
1. Prepare the concrete shared-evidence design and focused regression cases.
|
||||||
|
Map existing producers and consumers to the new owner, distinguish common
|
||||||
|
attribution from presentation rules, and identify the old paths to remove.
|
||||||
|
Select representations and indexes that preserve existing query and lifetime
|
||||||
|
needs without adding public contracts.
|
||||||
|
2. Repair entry-state presence queries and `Default` (finding 1). This is a local
|
||||||
|
correction that can proceed independently of the larger history work.
|
||||||
|
3. Repair final explicitness preservation (finding 2), retaining the shape evidence
|
||||||
|
needed by metadata updates. Coordinate this work with history construction so
|
||||||
|
the two fixes do not introduce competing definitions of path identity.
|
||||||
|
4. Build the shared model and migrate metadata history, explanation, sensitivity
|
||||||
|
dependencies, and diagnostic-location readers. Preserve phase evidence and
|
||||||
|
derive both public projections through common semantic attribution. Remove
|
||||||
|
the superseded reconstruction paths and reconcile retained-history accounting.
|
||||||
|
Verify finding 3 and its interaction with final presence metadata; repairing
|
||||||
|
pointer buckets alone does not complete the selected architecture.
|
||||||
|
5. Complete composition diagnostic ordering (finding 4) as an independent change;
|
||||||
|
it may proceed alongside the path work. Check rendering as well as the
|
||||||
|
structured error before considering it complete.
|
||||||
|
6. Review the integrated result, run applicable development checks, and update
|
||||||
|
affected implementation documentation and finding status.
|
||||||
|
|
||||||
|
These are high-level dependencies, not a requirement to combine unrelated fixes
|
||||||
|
in one commit. The implementation plan should select concrete edits and test
|
||||||
|
commands while keeping the four findings individually traceable.
|
||||||
|
|
||||||
|
## Validation And Completion
|
||||||
|
|
||||||
|
Follow the [testing policy](../policy/testing.md) and use the
|
||||||
|
[development guide](../development.md#baseline-validation) for checks. Extend
|
||||||
|
existing test owners rather than recreating full internal matrices at the facade.
|
||||||
|
The focused cases should cover:
|
||||||
|
|
||||||
|
- Both mapping-key/index mismatch directions beneath dynamic values, including
|
||||||
|
explicit paths and contextual parsed pointers. Check callback presence and
|
||||||
|
`Default` behavior, including the fixed entry view after working-value changes.
|
||||||
|
- Both container-shape transitions during recorded resolution, a descendant
|
||||||
|
affected by an ancestor shape change, and a same-shape normalization control.
|
||||||
|
Check explicitness and recorded origins through final metadata and explanation.
|
||||||
|
- Shape-changing CLI and resolution history, checking exact definitions,
|
||||||
|
`Previous`, and genuine ancestor replacement/removal. Include contextual
|
||||||
|
enumeration/query coverage and preserve existing inheritance and overlay cases;
|
||||||
|
add another case only where those integrations expose an unprotected risk.
|
||||||
|
- Shared-model projections across composite mapping origins, typed decoding and
|
||||||
|
default reconciliation, equal-valued writes, inheritance controls/transfers,
|
||||||
|
mapped sequence replacements and removed descendants, and overlapping
|
||||||
|
resolution records. Reuse existing cases where they protect those semantics;
|
||||||
|
check the intentional differences between the public projections as well as
|
||||||
|
agreement on attribution.
|
||||||
|
- Historical sensitivity/dependency propagation, diagnostic source-location
|
||||||
|
fallback, detached returned records, and complete retained-history accounting
|
||||||
|
under the new ownership graph. Use small controlled budget cases and existing
|
||||||
|
disclosure fixtures rather than memory or timing thresholds.
|
||||||
|
- A composition conflict whose acquisition and assembly orders differ, using the
|
||||||
|
audit fixture. Check both published diagnostics and rendered related-location
|
||||||
|
order, while retaining coverage for ordinary diagnostic canonicalization.
|
||||||
|
|
||||||
|
Retain existing checks for invalid schema paths, callback lifetime, detached
|
||||||
|
records, safe historical rendering, and default/null presence semantics. Test
|
||||||
|
observable behavior rather than a particular private index or number of copies.
|
||||||
|
No benchmark campaign, broader audit, or new release gate is part of this roadmap.
|
||||||
|
|
||||||
|
Completion requires all four findings to have implementation and regression
|
||||||
|
evidence, appropriate development checks, and both history interfaces deriving
|
||||||
|
semantic attribution from the shared model. Review the final producer/consumer
|
||||||
|
map to confirm superseded independent reconstruction has been removed and
|
||||||
|
internal attribution readers have migrated. Passing the four defect regressions
|
||||||
|
while retaining the old parallel attribution machinery is not completion.
|
||||||
|
|
||||||
|
The audit's previous passing suite is not validation of these future changes.
|
||||||
|
Scope here does not promise identical output from different inspection interfaces
|
||||||
|
or a universal guarantee about unrelated path-processing code.
|
||||||
|
|
||||||
|
## Decision Status
|
||||||
|
|
||||||
|
OD-1 is settled: build one shared internal history model with distinct metadata
|
||||||
|
and explanation projections. The design investigation found that both views
|
||||||
|
require evidence the other's public representation does not preserve. Shared
|
||||||
|
ownership can replace independent attribution and redundant retained evidence,
|
||||||
|
while keeping the current lifecycle and public contracts.
|
||||||
|
|
||||||
|
The narrower repair of stored history events would leave the separate replay
|
||||||
|
attribution owner in place. Promoting current explanation output to authoritative
|
||||||
|
history would lose composite-definition and specific resolution information.
|
||||||
|
The selected common underlying model addresses both concerns. This accepts a
|
||||||
|
larger internal refactor than a local shape-filtering fix.
|
||||||
|
|
||||||
|
No significant open decision remains at roadmap level. Private representation,
|
||||||
|
indexing, and helper choices belong in the implementation plan. This roadmap
|
||||||
|
records future implementation ownership; it does not revise accepted ADRs or
|
||||||
|
declare the model implemented.
|
||||||
202
docs/roadmap/interfaces.md
Normal file
202
docs/roadmap/interfaces.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# API and Interface Contract Audit
|
||||||
|
|
||||||
|
Reviewed baseline: `01b08e76a3fecc22c4414437bfeaa07b4b7a85aa`.
|
||||||
|
|
||||||
|
Status: four open findings. This is an audit record, not an implementation plan.
|
||||||
|
No production code or repository tests were changed.
|
||||||
|
|
||||||
|
The [interface remediation roadmap](interface-remediation.md) describes the
|
||||||
|
settled target, required changes, and dependencies, including a shared internal
|
||||||
|
history model for metadata history and explanation. Implementation remains pending.
|
||||||
|
This audit retains the findings and reproduction evidence.
|
||||||
|
|
||||||
|
## Scope and evidence
|
||||||
|
|
||||||
|
Reviewed the root-owned public facade and its conversions, engine loading and
|
||||||
|
callback boundaries, source/discovery and filesystem ownership, schema and typed
|
||||||
|
value handling, metadata/history, safe rendering and comparison, secrets, and
|
||||||
|
the document/YAML boundary against declaration comments, the
|
||||||
|
[configuration contract](../config.md), and applicable architectural decisions.
|
||||||
|
The root delegates to the synchronous engine lifecycle; parser-independent
|
||||||
|
documents support source assembly and inheritance, and completed results retain
|
||||||
|
detached configuration and frozen inspection state.
|
||||||
|
|
||||||
|
The findings below were reproduced through the public API with a disposable Go
|
||||||
|
program outside the repository, using labeled byte sources and `fstest.MapFS`.
|
||||||
|
The full existing suite passed with Go 1.26.10 on Linux:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GOTOOLCHAIN=local GOTELEMETRY=off GOPROXY=off go test ./... -count=1
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing tests cover many ownership, lifetime, error, and zero-value cases,
|
||||||
|
but do not detect these cross-interface discrepancies. No benchmarks, external
|
||||||
|
service calls, or new test files were needed. The recommendations restore
|
||||||
|
existing contracts rather than introduce new API restrictions or guarantees.
|
||||||
|
|
||||||
|
## 1. Resolution presence queries confuse explicit keys with sequence indexes
|
||||||
|
|
||||||
|
Priority: P2. Status: open.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [`Resolution.IsExplicit`](../../internal/engine/resolution.go), lines 75–82.
|
||||||
|
- `Resolution.InputInfo` and `resolutionInput.lookup` in
|
||||||
|
[resolution.go](../../internal/engine/resolution.go) and
|
||||||
|
[load_metadata.go](../../internal/engine/load_metadata.go).
|
||||||
|
- `resolveSchemaPath` in [schema.go](../../internal/engine/schema.go).
|
||||||
|
|
||||||
|
The public [path contract](../../path.go) distinguishes explicit mapping keys
|
||||||
|
from sequence indexes. The [resolution contract](../config.md#recorded-application-resolution)
|
||||||
|
defines surviving-input presence and a fixed entry-state metadata view;
|
||||||
|
[metadata queries](../config.md#layered-metadata) preserve container shapes.
|
||||||
|
|
||||||
|
`IsExplicit` validates against the schema but then looks up presence using only
|
||||||
|
`path.String()`. In dynamic `any` state, schema validation cannot establish the
|
||||||
|
actual container shape. Consequently these distinct queries collide:
|
||||||
|
|
||||||
|
```go
|
||||||
|
key := configkit.Keys("shape", "0")
|
||||||
|
index := configkit.Keys("shape").Index(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
Load `map[string]any` from `shape: [input]` with a resolver that queries both
|
||||||
|
paths, without changing the configuration. Observed results:
|
||||||
|
|
||||||
|
| Query | `Resolution.IsExplicit` | `Resolution.InputInfo` found | Final `Metadata.IsExplicit` |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Explicit mapping key | true | false | false |
|
||||||
|
| Explicit sequence index | true | true | true |
|
||||||
|
|
||||||
|
The mapping-key path never existed and should not be reported as explicit.
|
||||||
|
Applications branching on presence receive inconsistent answers depending on
|
||||||
|
which public query they use, even during an otherwise unchanged load.
|
||||||
|
|
||||||
|
Potential remediation: use the retained entry-state semantic shape when checking
|
||||||
|
resolution presence, consistently with `InputInfo` and `Metadata.IsExplicit`.
|
||||||
|
Review the same string-only presence lookup in `Default` while preserving its
|
||||||
|
existing assignment and callback-lifetime rules. Add focused public coverage for
|
||||||
|
both mapping-to-index and index-to-mapping mismatches under dynamic values,
|
||||||
|
including contextually parsed pointers. This does not require changing the
|
||||||
|
documented treatment of invalid schema paths.
|
||||||
|
|
||||||
|
## 2. Resolution can transfer explicitness to a different semantic path
|
||||||
|
|
||||||
|
Priority: P2. Status: open.
|
||||||
|
|
||||||
|
Relevant code: `applyResolutionMetadata`, `applyResolutionFieldOrigins`, and
|
||||||
|
`refreshResolutionFields` in
|
||||||
|
[load_metadata.go](../../internal/engine/load_metadata.go), especially lines
|
||||||
|
371–387; final shape checking in
|
||||||
|
[`Metadata.effectiveField`](../../internal/engine/metadata.go).
|
||||||
|
|
||||||
|
The [metadata contract](../config.md#layered-metadata) distinguishes key/index
|
||||||
|
shapes and keeps derived values for absent input non-explicit. Recorded
|
||||||
|
resolution changes origins without creating input explicitness.
|
||||||
|
|
||||||
|
Reproduction:
|
||||||
|
|
||||||
|
1. Load `map[string]any` from `shape: {'0': old}`.
|
||||||
|
2. In resolution, replace `configuration["shape"]` with `[]any{"new"}` and
|
||||||
|
record normalization at `Keys("shape")`.
|
||||||
|
3. Query final metadata using the two paths from finding 1.
|
||||||
|
|
||||||
|
The old explicit mapping-key path correctly stops being effective, but the newly
|
||||||
|
created sequence index reports `Explicit: true`. That sequence index was never
|
||||||
|
supplied by input. `refreshResolutionFields` reuses the old `FieldInfo` whenever
|
||||||
|
its textual pointer survives; it updates null state without checking whether
|
||||||
|
the old and new containers give that pointer different semantic shapes. Final
|
||||||
|
lookup validates only the new shape, so it cannot recover the lost distinction.
|
||||||
|
|
||||||
|
This can mislead consumers inspecting whether a final value was explicitly
|
||||||
|
configured rather than introduced by application resolution. The converse
|
||||||
|
sequence-to-numeric-key transition has the same underlying collision.
|
||||||
|
|
||||||
|
Potential remediation: retain entry/effective shape evidence across resolution
|
||||||
|
and carry input explicitness forward only for the matching semantic path.
|
||||||
|
Preserve existing explicitness for same-shape paths and the recorded resolution
|
||||||
|
origin; do not clear every descendant's presence merely because an ancestor was
|
||||||
|
normalized. Cover both shape transitions and agreement among `Lookup`,
|
||||||
|
`IsExplicit`, and explanation. Fixing finding 1's callback query alone does not
|
||||||
|
repair this publication path.
|
||||||
|
|
||||||
|
## 3. Metadata history mixes numeric-key and sequence-index definitions
|
||||||
|
|
||||||
|
Priority: P2. Status: open.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [`Metadata.History`](../../internal/engine/metadata.go), lines 56–65.
|
||||||
|
- `metadataFromDecoded`, `applyOverlayMetadata`, and history event construction
|
||||||
|
in [load_metadata.go](../../internal/engine/load_metadata.go).
|
||||||
|
- Shape-aware replay in [`Inspection.Explain`](../../internal/engine/explain.go).
|
||||||
|
|
||||||
|
The [metadata contract](../config.md#layered-metadata) promises semantic
|
||||||
|
key/index distinctions for historical as well as effective queries. The
|
||||||
|
historical-path index retains both shapes, but `History` uses that index only to
|
||||||
|
check that the query was ever known. It then retrieves the entire history bucket
|
||||||
|
by textual pointer, without selecting the query's shape.
|
||||||
|
|
||||||
|
Reproduction: load `shape: {'0': old}` into `map[string]any`, then apply a CLI
|
||||||
|
override at `Keys("shape")` with `[]any{"new"}`. Both explicit shapes were
|
||||||
|
observed and share `/shape/0`.
|
||||||
|
|
||||||
|
`History(Keys("shape").Index(0))` incorrectly includes the original base
|
||||||
|
definition of the mapping key, and identifies that base origin as the previous
|
||||||
|
origin of the CLI operation. The old input defined a key, not an index. In the
|
||||||
|
same successful result, `Explain` for the explicit index correctly shows only
|
||||||
|
its CLI introduction. `History` for the key and index returns identical records
|
||||||
|
despite their different lifetimes.
|
||||||
|
|
||||||
|
Consequences include false source attribution and disagreement between the two
|
||||||
|
public history interfaces when consumers inspect removed or introduced paths.
|
||||||
|
|
||||||
|
Potential remediation: retain enough container-shape evidence with stored
|
||||||
|
history events, or select relevant events from existing shape-aware replay
|
||||||
|
evidence. Filter exact definitions and previous origins by the queried semantic
|
||||||
|
shape while retaining genuine ancestor replacement/removal events. Filtering
|
||||||
|
only on the current `Change.Path` is insufficient where construction already
|
||||||
|
replaced concrete segment kinds with parsed tokens. Preserve the intentional
|
||||||
|
one-pointer-per-entry `HistoricalPaths` enumeration and contextual behavior of
|
||||||
|
`ParsePath`; neither public contract needs redesign. Cover both direction changes
|
||||||
|
and compare metadata history with explanation.
|
||||||
|
|
||||||
|
## 4. Diagnostic canonicalization breaks composition claimant ordering
|
||||||
|
|
||||||
|
Priority: P3. Status: open.
|
||||||
|
|
||||||
|
Relevant code: `compositionConflictDiagnostic` in
|
||||||
|
[compose.go](../../internal/engine/compose.go) and `canonicalDiagnostics` in
|
||||||
|
[diagnostic.go](../../internal/engine/diagnostic.go), particularly lines 88–92.
|
||||||
|
The latter runs both while constructing `LoadError` and in `WriteDiagnostics`.
|
||||||
|
|
||||||
|
The [strict-composition contract](../config.md#strict-document-composition)
|
||||||
|
specifies the first claim in scoped assembly order as primary and every remaining
|
||||||
|
claim in the same order as related locations.
|
||||||
|
|
||||||
|
A native-filesystem fixture demonstrates the discrepancy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main.yml imports a.yml, b.yml, c.yml
|
||||||
|
a.yml imports z.yml
|
||||||
|
b.yml, c.yml, and z.yml each define x
|
||||||
|
```
|
||||||
|
|
||||||
|
Acquisition assigns main=source-1, a=source-2, z=source-3, b=source-4,
|
||||||
|
c=source-5. Dependency-ready assembly considers the three conflicting claims
|
||||||
|
in b, c, z order. The composer correctly chooses b as primary and constructs
|
||||||
|
related locations c, z. Generic canonicalization sorts those related locations
|
||||||
|
by acquisition ID, publishing z, c instead. The reproduced public diagnostic
|
||||||
|
has primary source-4 and related source-3, source-5.
|
||||||
|
|
||||||
|
All claimants remain present and the conflict still fails loading, so this is
|
||||||
|
lower impact than the presence/history defects. It nevertheless violates the
|
||||||
|
documented reporting order and gives consumers two different ordering rules
|
||||||
|
within one conflict report.
|
||||||
|
|
||||||
|
Potential remediation: preserve the selected semantic ordering for composition
|
||||||
|
claimant lists through accumulation and rendering, while retaining canonical
|
||||||
|
ordering of diagnostic records and the established sorting of related locations
|
||||||
|
for diagnostics without a semantic-order contract. Add one public failure-path
|
||||||
|
case whose acquisition and claimant orders differ and verify both structured
|
||||||
|
diagnostics and rendered output. No scheduler or precedence change is needed.
|
||||||
186
docs/roadmap/internal-boundaries.md
Normal file
186
docs/roadmap/internal-boundaries.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# Import Discovery And Facade Boundary Simplification
|
||||||
|
|
||||||
|
Status: target defined; execution is tracked in the [implementation plan](implementation.md).
|
||||||
|
Baseline: `80cf23e7102fca2c5d7c7621e75f930a1ef1ecad`.
|
||||||
|
|
||||||
|
This roadmap brings together the two refactors deferred until after the audit
|
||||||
|
remediation sprint: glob entry preparation (**C03**) and facade-to-engine
|
||||||
|
transport simplification (**F07**). Their original motivation is retained in the
|
||||||
|
[glob](future.md#1-separate-glob-entry-preparation-from-traversal) and
|
||||||
|
[transport](future.md#2-simplify-redundant-facade-to-engine-transport) future-work
|
||||||
|
entries and the [consolidated audit](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md).
|
||||||
|
|
||||||
|
The goal is clearer responsibility and ownership with fewer redundant internal
|
||||||
|
representations. Both changes preserve the public API and configuration model.
|
||||||
|
They are independent work areas within one maintenance effort, not a new loading
|
||||||
|
workflow. The [implementation plan](implementation.md) owns concrete edits,
|
||||||
|
stages, validation commands, and execution progress.
|
||||||
|
|
||||||
|
## Baseline And Targeted End State
|
||||||
|
|
||||||
|
The preceding sprint repaired directory-failure classification and shared link
|
||||||
|
dependency evaluation, clarified acquisition handoffs, and removed unused
|
||||||
|
placement bridges. Those changes provide the baseline; this work does not repeat
|
||||||
|
or redesign them.
|
||||||
|
|
||||||
|
| Area | Current baseline | Target |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Glob discovery | `selectGlobImportCandidates` interleaves traversal and set operations with substantial per-entry preparation. | The selector coordinates discovery; a cohesive private operation prepares a wildcard-discovered entry. |
|
||||||
|
| Facade transport | Import defaults pass through root conversion, an exported engine transport record, an equivalent private engine record, and repeated snapshots. Placement construction also uses setter-style bridges. | Root-owned public records convert explicitly to one engine defaults representation, with straightforward engine option/input construction and identifiable ownership copies. |
|
||||||
|
|
||||||
|
The [architecture policy](../policy/architecture.md), [configuration
|
||||||
|
contract](../config.md), and public declarations remain authoritative. [ADR
|
||||||
|
0006](../adr/0006-public-facade.md) continues to govern the facade boundary. These
|
||||||
|
refactors do not require a new architectural decision or changes to its accepted
|
||||||
|
decision. The [component overview](../internal/overview.md) and [import
|
||||||
|
internals](../internal/imports.md) own implemented structure.
|
||||||
|
|
||||||
|
## Glob Entry Preparation
|
||||||
|
|
||||||
|
### Target Responsibility Split
|
||||||
|
|
||||||
|
In [glob.go](../../internal/engine/glob.go), the selector continues to own pattern
|
||||||
|
validation, component advancement, literal-segment handling, directory
|
||||||
|
enumeration and its accounting, candidate union and sorting, final target
|
||||||
|
validation, exclusions, and requiredness.
|
||||||
|
|
||||||
|
A private glob-specific preparation operation owns the patterned branch's work
|
||||||
|
for one directory entry: matching and hidden-entry filtering, lexical/access path
|
||||||
|
construction, containment checks, target inspection and discovered-link policy,
|
||||||
|
intermediate-directory eligibility, host/native access-path selection, labels,
|
||||||
|
and candidate construction. Its result distinguishes a selected candidate, an
|
||||||
|
ordinary skip, and a diagnostic. It receives the facts needed for that entry
|
||||||
|
rather than mutating the selector's candidate set or surrounding traversal state.
|
||||||
|
|
||||||
|
Use the existing filesystem access and link-resolution helpers. Directory
|
||||||
|
selection already has `prepareImportDirectoryEntry`; it remains a separate owner
|
||||||
|
because its recursion, matching, absence handling, and traversal state differ.
|
||||||
|
Share a smaller operation only where it actually expresses the same semantics
|
||||||
|
and reduces duplication. No common visitor or discovery strategy is needed for
|
||||||
|
this extraction.
|
||||||
|
|
||||||
|
### Behavior To Preserve
|
||||||
|
|
||||||
|
Preserve the current distinctions and processing order documented by
|
||||||
|
[explicit imports](../config.md#explicit-imports) and exercised by the selection
|
||||||
|
tests:
|
||||||
|
|
||||||
|
- Literal components remain explicit selections; discovery filters apply at
|
||||||
|
their existing wildcard boundaries. Moving matching or hidden filtering after
|
||||||
|
filesystem inspection would change which errors are observable.
|
||||||
|
- Keep lexical candidate identity separate from resolved access identity.
|
||||||
|
Native source identity and relative imports retain namespace spelling; host
|
||||||
|
access retains its existing canonicalization and containment behavior.
|
||||||
|
- Keep discovered-link follow rules, intermediate-directory eligibility,
|
||||||
|
missing-target handling, and structured diagnostic sites/classifications.
|
||||||
|
Do not make glob and directory rules identical as a side effect of extraction.
|
||||||
|
- Preserve deterministic candidate and error order, union/deduplication,
|
||||||
|
directory-entry accounting, and the existing point of source admission.
|
||||||
|
Final selected-file validation remains before exclusions where it is today;
|
||||||
|
exclusions do not hide discovery or resource errors.
|
||||||
|
- Retain the preceding sprint's distinction between missing-on-open directories
|
||||||
|
and failures after opening, along with operation-local completed-link reuse.
|
||||||
|
|
||||||
|
The intended benefit is that a maintainer can understand traversal separately
|
||||||
|
from entry eligibility and identity preparation. This is a readability change;
|
||||||
|
it does not select a different search algorithm or claim a speedup.
|
||||||
|
|
||||||
|
## Facade-To-Engine Transport
|
||||||
|
|
||||||
|
### Target Representations
|
||||||
|
|
||||||
|
The root keeps its public `ImportDefaults`, `PlacementMode`, `Options`, `Input`,
|
||||||
|
and `LayeredOptions` types. Explicit conversions remain appropriate at this
|
||||||
|
boundary; consumers must not acquire engine type identity, fields, or methods.
|
||||||
|
|
||||||
|
Inside the engine, consolidate the equivalent `ImportDefaults` transport and
|
||||||
|
private `importDefaults` records into one presence-aware input-defaults
|
||||||
|
representation. Carry that representation through the ordinary and layered
|
||||||
|
options, normalized request, and acquisition policy instead of translating the
|
||||||
|
same five fields between equivalent engine records.
|
||||||
|
|
||||||
|
Keep `resolvedImportPolicy` distinct: it represents concrete per-declaration
|
||||||
|
choices, including selector-specific rules, rather than another copy of optional
|
||||||
|
load-wide input. Likewise, registration compilation and the normalized loading
|
||||||
|
request have real responsibilities and are not replacement targets.
|
||||||
|
|
||||||
|
Make placement and defaults straightforward to construct on the existing engine
|
||||||
|
option/input records. Remove decorator-style bridges made unnecessary by that
|
||||||
|
construction surface. Exporting fields or types within `internal/engine` where
|
||||||
|
needed by the root converter does not make them part of ConfigKit's supported
|
||||||
|
public API. Keep explicit enum conversion and preserve unknown values until the
|
||||||
|
existing option validation can report them; conversion must not silently choose
|
||||||
|
a default or move validation into facade setters.
|
||||||
|
|
||||||
|
### Ownership And Copying
|
||||||
|
|
||||||
|
Make the actual mutable ownership boundaries identifiable. Public load calls
|
||||||
|
continue to snapshot pointed-to default booleans and the pattern slice before
|
||||||
|
source acquisition, as specified by the [public
|
||||||
|
declaration](../../lifecycle.go). Preserve omitted, explicit false, nil pattern,
|
||||||
|
and non-nil empty pattern as distinct inputs where they are distinct today.
|
||||||
|
|
||||||
|
Review the entire handoff, not just the duplicate type definitions: root
|
||||||
|
conversion in [conversion.go](../../conversion.go), ordinary construction in
|
||||||
|
[lifecycle.go](../../lifecycle.go), layered construction in
|
||||||
|
[layered.go](../../layered.go), engine options and normalization, acquisition
|
||||||
|
policy construction, and declaration resolution in
|
||||||
|
[import_spec.go](../../internal/engine/import_spec.go).
|
||||||
|
|
||||||
|
Remove snapshots that merely recopy already-owned read-only defaults as they
|
||||||
|
cross those private representations. Keep copies needed when establishing
|
||||||
|
independent mutable ownership, including any independently owned resolved
|
||||||
|
policy. The target is explainable ownership and less redundant work, not an exact
|
||||||
|
copy count or removal of copying throughout the library. Shared defaults within
|
||||||
|
one load can be read-only by implementation discipline; this creates no new
|
||||||
|
caller immutability or concurrency requirement.
|
||||||
|
|
||||||
|
Preserve ordinary and layered option preflight before source reads and application
|
||||||
|
hooks, independent declaration overrides, transitive/underlay propagation,
|
||||||
|
selected-overlay behavior, destination normalization, and invalid-enum
|
||||||
|
diagnostics. Keep callback, source, path, secret, and returned-record conversions
|
||||||
|
whose ownership or domain meaning is not redundant transport.
|
||||||
|
|
||||||
|
The intended benefit is fewer concepts, field mappings, and construction paths
|
||||||
|
to maintain. Reduced copying is useful but does not establish an end-to-end
|
||||||
|
performance guarantee.
|
||||||
|
|
||||||
|
## Documentation Ownership
|
||||||
|
|
||||||
|
Internal documentation describes the resulting discovery and transport
|
||||||
|
boundaries once implemented. Public declarations continue to own the existing
|
||||||
|
snapshot and type contracts; this effort selects no new public behavior.
|
||||||
|
The corresponding C03/F07 future-work entries serve as navigation to this
|
||||||
|
roadmap rather than parallel target or progress inventories. Unrelated future
|
||||||
|
candidates remain separate. The implementation plan owns the documentation
|
||||||
|
updates and retirement of replaced private representations and helpers.
|
||||||
|
|
||||||
|
## Validation And Completion
|
||||||
|
|
||||||
|
Follow the [testing policy](../policy/testing.md) and [development
|
||||||
|
guide](../development.md#baseline-validation). Reuse the existing glob selection,
|
||||||
|
directory/link failure, import-default, placement, public facade, and dependency
|
||||||
|
boundary coverage. Add a focused behavioral case only where a concrete
|
||||||
|
extraction or ownership risk lacks protection.
|
||||||
|
|
||||||
|
For discovery, verify literal versus wildcard filters, host/native identity,
|
||||||
|
contained and escaping links, optional absence versus read/close failures,
|
||||||
|
ordering, exclusions, regular-file checks, and existing resource accounting. For
|
||||||
|
transport, verify nil/empty and explicit-false distinctions, mutation of caller
|
||||||
|
default storage after the load snapshot through a supported callback, invalid
|
||||||
|
options before input consumption, both loading APIs, and transitive/underlay
|
||||||
|
defaults. Existing conversion-focused tests may change with private construction;
|
||||||
|
preserve the useful ownership behavior rather than fossilizing a helper's shape.
|
||||||
|
|
||||||
|
Completion requires an easier-to-follow selector, one engine input-defaults
|
||||||
|
representation, fewer redundant mapping/construction sites, justified remaining
|
||||||
|
copies, and preserved public behavior under the relevant checks. Review source
|
||||||
|
and ownership flow as well as passing tests; moving complexity into a helper or
|
||||||
|
renaming a duplicate record alone is insufficient.
|
||||||
|
|
||||||
|
No benchmark campaign, complexity score, allocation threshold, exact filesystem
|
||||||
|
call trace, new resource limit, or additional release gate is required. This work
|
||||||
|
adds no dependencies, caches, background activity, or new consumer controls, and
|
||||||
|
does not reopen other audit findings or deferred performance experiments. Routine
|
||||||
|
helper signatures, names, and file placement belong to the implementation plan;
|
||||||
|
no significant product or architectural decision remains open for this scope.
|
||||||
276
docs/roadmap/roadmap.md
Normal file
276
docs/roadmap/roadmap.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# Audit Remediation Roadmap
|
||||||
|
|
||||||
|
Status: target accepted; completed execution is recorded in the historical plan
|
||||||
|
at `bb5540b:docs/roadmap/implementation.md`.
|
||||||
|
Prepared: 2026-10-25. Planning baseline: `e1e94ee`.
|
||||||
|
|
||||||
|
This roadmap describes the target state and related work for the 29 entries
|
||||||
|
selected in the [consolidated audit review](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#settled-scope-decisions).
|
||||||
|
It is intended for maintainers reviewing the remediation's scope and architecture.
|
||||||
|
|
||||||
|
## Document Ownership
|
||||||
|
|
||||||
|
- [Archived audit](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md) records finding IDs, original evidence, detailed remedies and
|
||||||
|
acceptance cases, and the settled scope decisions. Its evidence has not been
|
||||||
|
independently reproduced by writing this roadmap.
|
||||||
|
- This document owns the combined target state, work areas, and design intent.
|
||||||
|
Finding IDs below provide traceability without repeating the full audit inventory.
|
||||||
|
- The historical implementation plan at
|
||||||
|
`bb5540b:docs/roadmap/implementation.md` records concrete stages, private
|
||||||
|
implementation choices, edits, validation commands, and completion evidence.
|
||||||
|
|
||||||
|
The [configuration contract](../config.md), public Go declarations, and
|
||||||
|
[architecture policy](../policy/architecture.md) remain the owners of behavior
|
||||||
|
and guarantees. Update their applicable implementation documentation when work
|
||||||
|
lands; this planning document does not redefine their contracts.
|
||||||
|
|
||||||
|
## Current Baseline And Targeted End State
|
||||||
|
|
||||||
|
The existing loading model remains the foundation. Public loading APIs delegate
|
||||||
|
to the shared engine lifecycle; acquisition retains source scopes for composition,
|
||||||
|
then precedence, inheritance, decoding, CLI overrides, application resolution,
|
||||||
|
secret lookup, and validation produce a detached result with inspection metadata.
|
||||||
|
The [component overview](../internal/overview.md) describes the current owners.
|
||||||
|
|
||||||
|
The audits identify failures at particular traversal, lifetime, error, and
|
||||||
|
accounting boundaries, alongside redundant representations and repeated work.
|
||||||
|
The target is to repair those boundaries and simplify their implementation while
|
||||||
|
preserving ConfigKit's public API and configuration model.
|
||||||
|
|
||||||
|
| Current gap | Targeted outcome |
|
||||||
|
| --- | --- |
|
||||||
|
| Recursive descriptors can fail to terminate, failed dynamic builds can leave partial state, and slice aliases can be mistaken for cycles. | Traversal terminates for the reported cases, failed construction leaves reusable state valid, and supported acyclic values remain accepted. |
|
||||||
|
| Callback and owned-root cleanup can be skipped during panic unwinding; directory errors lose their failure phase. | Cleanup follows the lifetime of each owned resource, and selected-directory failures retain the information needed for correct error handling. |
|
||||||
|
| Finite symlink dependencies can be expanded repeatedly. | Completed dependencies are reused within a resolution while active cycles remain detectable. |
|
||||||
|
| Projection reservations and published history can escape existing accounting. | Existing budgets govern the relevant allocations and complete retained document graph. |
|
||||||
|
| Collection-policy work repeatedly scans or copies unrelated state. | Policy preparation, application, and retained evidence have clear ownership, with work localized to affected policy. |
|
||||||
|
| Acquisition has redundant paths and difficult handoffs; several private representations have no production consumer. | Production paths and their tests share the active protocol, with cohesive handoffs and unused representations removed. |
|
||||||
|
| Some lookups repeat full scans, and some tests assert incidental representation or miss meaningful mutations. | Local indexing avoids the identified repeated work, and tests protect observable contracts at useful boundaries. |
|
||||||
|
|
||||||
|
This is a bounded remediation effort, not a redesign of configuration semantics.
|
||||||
|
Correcting erroneous acceptance, rejection, cleanup, or diagnostics in the
|
||||||
|
reported cases restores existing contracts. It does not select new loading APIs,
|
||||||
|
configuration options, limits, background work, or persistent caches.
|
||||||
|
|
||||||
|
## Selected Work
|
||||||
|
|
||||||
|
The following areas account for all 29 selected entries. Detailed examples and
|
||||||
|
acceptance cases remain with their linked audit group.
|
||||||
|
|
||||||
|
### 1. Safe Typed-Value Traversal And Descriptor Reuse
|
||||||
|
|
||||||
|
Selected entries: **A01, A02, A03, A04, A05** in
|
||||||
|
[schema and typed values](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#a-schema-and-typed-values).
|
||||||
|
|
||||||
|
Make pointer-only descriptor traversal terminate in decoding and path resolution,
|
||||||
|
while preserving supported null values and ordinary recursive structures. Make
|
||||||
|
dynamic descriptor construction transactional: a failed build must not leave
|
||||||
|
partial descriptors available to subsequent operations. Staging successful
|
||||||
|
additions or rolling back the complete failed addition set are implementation
|
||||||
|
choices; both must handle recursive references and preserve prior valid entries.
|
||||||
|
|
||||||
|
Correct active-path cycle detection to distinguish finite slice views sharing
|
||||||
|
storage from actual cycles. Keep detachment and nil/empty behavior intact across
|
||||||
|
the callers that share this traversal machinery.
|
||||||
|
|
||||||
|
Enforce existing projection budgets before large container reservations. Include
|
||||||
|
map-key materialization and overflow-safe lower-bound checks in the review; a
|
||||||
|
check after the reservation does not address the reported failure. This work
|
||||||
|
does not introduce a general heap limit or reject large containers independently
|
||||||
|
of existing budgets.
|
||||||
|
|
||||||
|
Dynamic-type eligibility reuse depends on safe descriptor construction and retains
|
||||||
|
completed successful checks within the value operation. Keep failure diagnostics
|
||||||
|
specific to their current path and distinguish completed results from traversal visits.
|
||||||
|
This removes repeated descriptor-graph walks without a cross-load cache.
|
||||||
|
|
||||||
|
### 2. Callback, Filesystem, And Dependency Lifetimes
|
||||||
|
|
||||||
|
Selected entries: **B01, B02** in [callback lifetime](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#b-callback-and-filesystem-lifetime)
|
||||||
|
and **C01, C02** in [filesystem traversal](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#c-import-selection-and-link-resolution).
|
||||||
|
|
||||||
|
Tie callback-context deactivation to the callback invocation with deferred cleanup
|
||||||
|
so it runs on normal return and panic unwinding. Independently give the owned
|
||||||
|
import root a deferred cleanup fallback. Preserve normal close-error handling,
|
||||||
|
caller ownership, and propagation of the original callback panic. Fixing either
|
||||||
|
state lifetime or handle lifetime does not fix the other.
|
||||||
|
|
||||||
|
Preserve the phase of a directory failure. A genuinely absent directory before
|
||||||
|
successful opening remains subject to existing permissiveness rules; a later
|
||||||
|
read or close failure must not be treated as absence. Apply this distinction to
|
||||||
|
both supported directory-reading paths. Strengthen the shared filesystem tests
|
||||||
|
to check each handle's closure and structured, non-disclosing diagnostics,
|
||||||
|
including partial reads and failures masked by another successful directory.
|
||||||
|
Do not change regular-file close-error policy as a side effect.
|
||||||
|
|
||||||
|
Avoid repeatedly expanding completed finite symlink dependencies in both host
|
||||||
|
and native filesystem resolution. Use operation-local memoization of completed
|
||||||
|
targets, or an equivalent dependency evaluation, with keys that respect the
|
||||||
|
resolution namespace. Keep active-path cycle detection separate from completed
|
||||||
|
work. Preserve containment, supported finite reuse, and existing host-path
|
||||||
|
semantics. The fix must work for directory and glob selection without a new
|
||||||
|
arbitrary traversal cap or changed symlink defaults.
|
||||||
|
|
||||||
|
### 3. Retained History And Combination Evidence
|
||||||
|
|
||||||
|
Selected entries: **D01, D02, D03, D04** in
|
||||||
|
[history and retained state](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#d-retained-history-and-unused-evidence).
|
||||||
|
|
||||||
|
Remove the unconsumed override-result pre-CLI copy and the unqueried published
|
||||||
|
structural-input copy. Preserve the separate pre-CLI state used by active replay
|
||||||
|
and the structural input still used before publication for metadata and secret
|
||||||
|
classification. Remove unused combination-evidence members and the work needed
|
||||||
|
solely to produce them, retaining projections, mappings, origins, and snapshots
|
||||||
|
that have active consumers.
|
||||||
|
|
||||||
|
With retained roots clarified, enforce the existing history allowance against
|
||||||
|
the complete graph published through inspection, including replay and inheritance
|
||||||
|
state and publication-created copies. Count shared nodes once by pointer identity;
|
||||||
|
distinct retained copies still consume the allowance. Account for required copies
|
||||||
|
before creating them where practical. Removing an unused tree alone does not
|
||||||
|
resolve the accounting omissions.
|
||||||
|
|
||||||
|
Keep required before/after evidence and existing inspection behavior. Validate
|
||||||
|
the complete publication path with small controlled allowances, including the
|
||||||
|
interaction of CLI, resolution replay, and inheritance, rather than relying only
|
||||||
|
on tests of an isolated counting helper.
|
||||||
|
|
||||||
|
### 4. Collection-Policy Preparation And Application
|
||||||
|
|
||||||
|
Selected entries: **E01, E02, E03** in
|
||||||
|
[collection policy and projection](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#e-collection-processing-and-efficiency).
|
||||||
|
|
||||||
|
Distinguish the existing policy state used for lookup, changes introduced by an
|
||||||
|
incoming tier, and evidence retained by the assembly. Prepare incoming claims and
|
||||||
|
declarations against read-only lower state, then apply the owned changes at the
|
||||||
|
appropriate combination boundary. Avoid copying every unrelated policy for
|
||||||
|
small tiers and scanning all policies for each independent replacement.
|
||||||
|
|
||||||
|
Use a semantic-path index, localized updates, or equivalent bounded lookup to
|
||||||
|
find affected policy. Preserve retirement timing, policy established by the
|
||||||
|
incoming contribution, pending declarations, persistent explicit modes, and
|
||||||
|
nested record ownership. Copying remains appropriate wherever mutable ownership
|
||||||
|
requires it. Removing only one duplicate clone or shortening a function does not
|
||||||
|
address the identified growth patterns.
|
||||||
|
|
||||||
|
Clarify preparation, evidence application, and finalization responsibilities.
|
||||||
|
Share straightforward history-descriptor translation where useful; evaluate
|
||||||
|
shared record-selection logic while keeping overlay and inheritance origin and
|
||||||
|
mode lifetimes distinct. Retain the common sequence-combination owner. This work
|
||||||
|
does not call for a generic merging framework.
|
||||||
|
|
||||||
|
### 5. Active Acquisition Handoffs And Private Cleanup
|
||||||
|
|
||||||
|
Selected entries: **F01, F02, F03, F04, F05, F06** in
|
||||||
|
[acquisition, cleanup, and test quality](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#f-remaining-maintenance-and-test-improvements).
|
||||||
|
|
||||||
|
Remove the test-only eager acquisition mode so acquisition retains scopes through
|
||||||
|
the active production protocol. Tests that need composition should invoke the
|
||||||
|
actual composition boundary explicitly. Retire the parallel test-only import
|
||||||
|
parser and route relevant fixtures through the active directive parser or the
|
||||||
|
appropriate declaration boundary, retaining malformed-input coverage.
|
||||||
|
|
||||||
|
Clarify active acquisition and coordinator handoffs around cohesive tasks:
|
||||||
|
parsed-input directive/version handling, standalone placement, and repeated tier
|
||||||
|
application/projection. Use explicit inputs and results consistent with existing
|
||||||
|
state owners. Extracting helpers that continue to share surrounding mutable state
|
||||||
|
does not establish a clearer handoff. Preserve ordinary/layered differences,
|
||||||
|
overlay and diff behavior, source identity, callback timing, diagnostics, and
|
||||||
|
accounting scopes.
|
||||||
|
|
||||||
|
Remove the disconnected root record-clone family, uncalled engine helpers, and
|
||||||
|
unread private transport/result members identified in the audit. Reconfirm
|
||||||
|
references on the implementation baseline, including function values and relevant
|
||||||
|
build configurations. Preserve live conversion paths and validation side effects;
|
||||||
|
the audit's absence of field reads is not a general rule for deleting state.
|
||||||
|
Replace meaningful ownership coverage through the public boundary before removing
|
||||||
|
tests that only exercise disconnected helpers.
|
||||||
|
|
||||||
|
### 6. Local Lookup Improvements
|
||||||
|
|
||||||
|
Selected entries: **E04, E05** in
|
||||||
|
[collection policy and projection](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#e-collection-processing-and-efficiency)
|
||||||
|
and **F08** in [secret-failure lookup](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#f08-index-secret-failure-location-queries-lazily).
|
||||||
|
|
||||||
|
Reuse a local document lookup or equivalent traversal for tier-history projection
|
||||||
|
instead of rescanning wide mappings for each sequence. Preserve authored
|
||||||
|
coordinates, sparse projections, and evidence ownership.
|
||||||
|
|
||||||
|
Restore compound identity fields using indexed mapping positions and membership
|
||||||
|
checks rather than rescanning child and merged mappings for every field. Preserve
|
||||||
|
child-authored values and locations, mapping order, and detached nodes.
|
||||||
|
|
||||||
|
Build secret-failure location lookup state lazily when failures need it. Preserve
|
||||||
|
all diagnostics, nearest-location fallbacks, and confidentiality; successful
|
||||||
|
loads should not incur an unnecessary index merely to support failure reporting.
|
||||||
|
These changes address specific repeated work and do not promise a measured
|
||||||
|
startup improvement.
|
||||||
|
|
||||||
|
### 7. Tests That Protect The Intended Behavior
|
||||||
|
|
||||||
|
Selected entries: **F09, F10, F11, F12** in
|
||||||
|
[acquisition, cleanup, and test quality](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/453da684d7c7d3843a5f505f5c468b4c889dc6e2/docs/roadmap/audit.md#f-remaining-maintenance-and-test-improvements),
|
||||||
|
in addition to the regression coverage attached to each reported defect.
|
||||||
|
|
||||||
|
Cover historical-path enumeration through the public API, including removed
|
||||||
|
paths, queryability, ordering, and detached returned slices. Use those assertions
|
||||||
|
to replace private representation checks where they address the same risk.
|
||||||
|
|
||||||
|
Strengthen fuzz detachment checks by mutating existing descendants or in-length
|
||||||
|
elements, so spare capacity cannot hide aliases. Replace exact filesystem stat
|
||||||
|
call sequences with observable candidate ordering and meaningful failure
|
||||||
|
selection, retaining relevant assertions such as not opening excluded inputs.
|
||||||
|
|
||||||
|
Consolidate facade scaffolding around public construction, loading, errors,
|
||||||
|
ownership, and lifecycle behavior. Preserve distinct integration risks and
|
||||||
|
reader-byte snapshot coverage. Existing coverage percentages or a desired test
|
||||||
|
count are not acceptance criteria for this work.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
The historical implementation plan at `bb5540b:docs/roadmap/implementation.md`
|
||||||
|
defines the sequential, Luna-scoped stages and their dependencies, including
|
||||||
|
coordinated changes to descriptor state, history ownership/accounting, collection
|
||||||
|
policy, acquisition, and replacement public coverage. It also records validation
|
||||||
|
commands and the complete finding-to-stage map. The audit remains the owner of
|
||||||
|
original evidence and detailed acceptance cases.
|
||||||
|
|
||||||
|
## Validation And Completion
|
||||||
|
|
||||||
|
Validation follows the audit's detailed acceptance cases and the
|
||||||
|
[testing policy](../policy/testing.md). For fatal or hanging regressions, use bounded
|
||||||
|
subprocesses; for budget cases, use small controlled limits. For symlink expansion
|
||||||
|
and repeated lookup work, prefer bounded operation evidence and algorithm review
|
||||||
|
over timing thresholds. Use real filesystem fixtures where filesystem semantics
|
||||||
|
matter and narrow fault injection for read/close failures. State platform limits.
|
||||||
|
|
||||||
|
Applicable development checks come from the [development guide](../development.md),
|
||||||
|
including appropriate race coverage for changed ownership and lifetime boundaries.
|
||||||
|
The historical implementation plan owns their execution. Existing audit results
|
||||||
|
are baseline evidence, not validation of the eventual changes. No performance
|
||||||
|
benchmark campaign is required by this roadmap, and no new benchmark results are
|
||||||
|
claimed.
|
||||||
|
|
||||||
|
Completion means all 29 selected findings have implementation and acceptance
|
||||||
|
evidence, the affected public behavior remains consistent with its contracts,
|
||||||
|
and canonical internal documentation describes the resulting owners. The
|
||||||
|
historical implementation plan records execution status without duplicate
|
||||||
|
progress inventories. Retire completed planning records under the normal
|
||||||
|
documentation lifecycle.
|
||||||
|
|
||||||
|
## Scope Boundaries And Decision Status
|
||||||
|
|
||||||
|
All three audit scope questions are settled. No significant architectural or
|
||||||
|
product decision remains open for this roadmap. The historical implementation
|
||||||
|
plan records private design choices and sequencing, subject to the acceptance
|
||||||
|
cases above.
|
||||||
|
If that work exposes a need to change a public contract, bring the specific
|
||||||
|
decision back for review rather than treating it as an implicit part of remediation.
|
||||||
|
|
||||||
|
The glob preparation refactor (C03) and wider facade transport simplification
|
||||||
|
(F07) have since been selected in the
|
||||||
|
[internal-boundary roadmap](internal-boundaries.md). Other deferred features,
|
||||||
|
clone-traversal redesign, and broader performance experiments remain outside
|
||||||
|
this effort. Keep
|
||||||
|
existing ownership boundaries and loading APIs; no ADR or architecture-policy
|
||||||
|
change is proposed by this roadmap.
|
||||||
@@ -19,6 +19,28 @@ func ExampleLoad() {
|
|||||||
// Output: {true 3} <nil>
|
// 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(`configkit:
|
||||||
|
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() {
|
func ExampleNoSource() {
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Workers int `yaml:"workers"`
|
Workers int `yaml:"workers"`
|
||||||
@@ -30,6 +52,32 @@ func ExampleNoSource() {
|
|||||||
// Output: 4 0 <nil>
|
// Output: 4 0 <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleDiff() {
|
||||||
|
type Config struct {
|
||||||
|
Workers int `yaml:"workers"`
|
||||||
|
}
|
||||||
|
before, err := configkit.Load[Config](configkit.Mapping("before", map[string]any{"workers": 2}))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
after, err := configkit.Load[Config](configkit.Mapping("after", map[string]any{"workers": 4}))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
differences, err := configkit.Diff(before, after)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, difference := range differences {
|
||||||
|
fmt.Printf("%s: %s -> %s\n", difference.Path.String(),
|
||||||
|
strings.TrimSpace(difference.Left.YAML), strings.TrimSpace(difference.Right.YAML))
|
||||||
|
}
|
||||||
|
// Output: /workers: 2 -> 4
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleValidation_Error() {
|
func ExampleValidation_Error() {
|
||||||
type Config struct {
|
type Config struct {
|
||||||
First string `yaml:"first"`
|
First string `yaml:"first"`
|
||||||
@@ -128,8 +176,15 @@ func ExampleOverride() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
result, err := configkit.Load(configkit.NoSource(), configkit.Options[Config]{
|
result, err := configkit.Load(configkit.NoSource(), configkit.Options[Config]{
|
||||||
Definition: configkit.Definition[Config]{Defaults: func() Config { return Config{Workers: 4} }},
|
Definition: configkit.Definition[Config]{
|
||||||
Overrides: overrides,
|
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)
|
fmt.Println(result.Config.Workers, result.Sources()[0].Label, err)
|
||||||
// Output: 0 --workers <nil>
|
// Output: 0 --workers <nil>
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ func Example_seriatim() {
|
|||||||
// Output: visitor true the event occurred <nil> <nil>
|
// Output: visitor true the event occurred <nil> <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Example_inheritedPresets() {
|
||||||
|
result, err := consumers.LoadPresets(configkit.File(fixture("inheritance", "presets.yml")))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := configkit.Keys("presets", "custom", "enabled")
|
||||||
|
info, _ := result.Metadata().Lookup(path)
|
||||||
|
custom := result.Config.Presets["custom"]
|
||||||
|
fmt.Println(custom.Enabled, result.Metadata().IsExplicit(path), custom.Settings["secondary"]["width"])
|
||||||
|
fmt.Println(info.Origin.Locations[0].SourceID, info.Origin.Inheritance[0].From)
|
||||||
|
// Output:
|
||||||
|
// false true 20
|
||||||
|
// source-1 /presets/base/enabled
|
||||||
|
}
|
||||||
|
|
||||||
func Example_weatherStartup() {
|
func Example_weatherStartup() {
|
||||||
const blockedName = "CONFIGKIT_WEATHER_EXAMPLE_BLOCKED"
|
const blockedName = "CONFIGKIT_WEATHER_EXAMPLE_BLOCKED"
|
||||||
const tokenName = "CONFIGKIT_WEATHER_EXAMPLE_TOKEN"
|
const tokenName = "CONFIGKIT_WEATHER_EXAMPLE_TOKEN"
|
||||||
|
|||||||
154
examples/consumers/destinations.go
Normal file
154
examples/consumers/destinations.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CollectionBundleConfig is a representative application-owned collection
|
||||||
|
// shape. Collection and prompt version labels remain ordinary domain fields.
|
||||||
|
type CollectionBundleConfig struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Collections map[string]CollectionDefinition `yaml:"collections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionDefinition struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Prompts map[string]CollectionPrompt `yaml:"prompts"`
|
||||||
|
Profiles map[string]CollectionProfile `yaml:"profiles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionPrompt struct {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
DefaultProfile string `yaml:"default_profile"`
|
||||||
|
ContentFile string `yaml:"content_file"`
|
||||||
|
ResourceText string `yaml:"resource_text"`
|
||||||
|
Messages []CollectionMessage `yaml:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionMessage struct {
|
||||||
|
Role string `yaml:"role"`
|
||||||
|
Content string `yaml:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollectionProfile struct {
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
Temperature float64 `yaml:"temperature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCollectionBundle demonstrates one layered collection load. Its main
|
||||||
|
// 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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundle := configkit.NewFilesystem("collection bundle", files)
|
||||||
|
collectionDestination := configkit.Keys("collections", "weatherreporter")
|
||||||
|
promptDestination := collectionDestination.Key("prompts")
|
||||||
|
return configkit.LoadLayered(
|
||||||
|
configkit.Input{Source: configkit.Bytes("application main", []byte("version: 1\n"))},
|
||||||
|
configkit.LayeredOptions[CollectionBundleConfig]{
|
||||||
|
Definition: configkit.Definition[CollectionBundleConfig]{
|
||||||
|
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(version configkit.Version, _ *configkit.Validation) error {
|
||||||
|
if !version.Present || version.Text != "1" {
|
||||||
|
return errors.New("unsupported application schema version")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
Resolve: resolveCollectionResources,
|
||||||
|
Validate: validateCollectionBundle,
|
||||||
|
},
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{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,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCollectionResources(configuration *CollectionBundleConfig, resolution *configkit.Resolution) error {
|
||||||
|
for collectionID, collection := range configuration.Collections {
|
||||||
|
for promptID, prompt := range collection.Prompts {
|
||||||
|
if prompt.ContentFile == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
||||||
|
info, found := resolution.InputInfo(contentPath)
|
||||||
|
if !found || len(info.Origin.Locations) != 1 {
|
||||||
|
return fmt.Errorf("prompt resource has no single declaring source")
|
||||||
|
}
|
||||||
|
resourceSource, found := resolution.Source(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("prompt resource source is unavailable")
|
||||||
|
}
|
||||||
|
context, found := resolution.SourceContext(resourceSource.ID)
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("prompt resource context is unavailable")
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, prompt.ContentFile))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read prompt resource: %w", err)
|
||||||
|
}
|
||||||
|
prompt.ResourceText = string(data)
|
||||||
|
collection.Prompts[promptID] = prompt
|
||||||
|
resolution.Record(
|
||||||
|
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
|
||||||
|
configkit.ChangeNormalization, "read source-relative prompt content", contentPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
configuration.Collections[collectionID] = collection
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCollectionBundle(configuration *CollectionBundleConfig, validation *configkit.Validation) {
|
||||||
|
for collectionID, collection := range configuration.Collections {
|
||||||
|
for promptID, prompt := range collection.Prompts {
|
||||||
|
if prompt.DefaultProfile == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, found := collection.Profiles[prompt.DefaultProfile]; !found {
|
||||||
|
validation.Error(configkit.Keys("collections", collectionID, "prompts", promptID, "default_profile"), "must name a profile in this collection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCollectionPromptContent follows the effective content field's physical
|
||||||
|
// source, including when it was inherited from a peer prompt.
|
||||||
|
func ReadCollectionPromptContent(result *configkit.Result[CollectionBundleConfig], collectionID, promptID string) (string, error) {
|
||||||
|
if result == nil {
|
||||||
|
return "", errors.New("collection result is nil")
|
||||||
|
}
|
||||||
|
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
||||||
|
info, found := result.Metadata().Lookup(contentPath)
|
||||||
|
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 context is unavailable")
|
||||||
|
}
|
||||||
|
prompt, found := result.Config.Collections[collectionID].Prompts[promptID]
|
||||||
|
if !found {
|
||||||
|
return "", errors.New("prompt is unavailable")
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, prompt.ContentFile))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
137
examples/consumers/destinations_test.go
Normal file
137
examples/consumers/destinations_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_inputDestination() {
|
||||||
|
mounted, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||||
|
Source: configkit.Mapping("weather prompt", map[string]any{
|
||||||
|
"summarize.alerts": map[string]any{"version": "1"},
|
||||||
|
}),
|
||||||
|
Destination: configkit.Keys("collections", "weather.reporter", "prompts"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root, err := configkit.LoadLayered[map[string]any](configkit.Input{
|
||||||
|
Source: configkit.Bytes("standalone", []byte("name: standalone\n")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, found := mounted.Config["collections"]
|
||||||
|
fmt.Println(found, root.Config["name"])
|
||||||
|
// Output: true standalone
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionBundleAssemblesMountedPeersAndResources(t *testing.T) {
|
||||||
|
result, err := consumers.LoadCollectionBundle()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadCollectionBundle: %v", err)
|
||||||
|
}
|
||||||
|
collection := result.Config.Collections["weatherreporter"]
|
||||||
|
if result.Config.Version != "1" || collection.Name != "Weather Reporter" || collection.Version != "2.4.0" {
|
||||||
|
t.Fatalf("main and collection versions = %q, %#v", result.Config.Version, collection)
|
||||||
|
}
|
||||||
|
brief := collection.Prompts["brief"]
|
||||||
|
custom := collection.Prompts["custom"]
|
||||||
|
if brief.Version != "2" || brief.DefaultProfile != "compact" || brief.ContentFile != "welcome.txt" || len(brief.Messages) != 1 {
|
||||||
|
t.Fatalf("version-only override replaced lower prompt fields: %#v", brief)
|
||||||
|
}
|
||||||
|
if custom.Version != "2" || custom.DefaultProfile != "weather" || custom.ContentFile != brief.ContentFile ||
|
||||||
|
len(custom.Messages) != 1 || custom.ResourceText != "Welcome to the weather collection.\n" {
|
||||||
|
t.Fatalf("inherited prompt = %#v", custom)
|
||||||
|
}
|
||||||
|
weatherProfile := collection.Profiles["weather"]
|
||||||
|
if weatherProfile.Format != "text" || weatherProfile.Temperature != 0.4 {
|
||||||
|
t.Fatalf("inherited profile = %#v", weatherProfile)
|
||||||
|
}
|
||||||
|
resource, err := consumers.ReadCollectionPromptContent(result, "weatherreporter", "custom")
|
||||||
|
if err != nil || resource != custom.ResourceText {
|
||||||
|
t.Fatalf("inherited prompt resource = %q, %v", resource, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionInfo, ok := result.Metadata().Lookup(configkit.Keys("version"))
|
||||||
|
if !ok || !versionInfo.Explicit || versionInfo.Origin.Layer != configkit.LayerBase || len(versionInfo.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("main-owned version metadata = %#v, %v", versionInfo, ok)
|
||||||
|
}
|
||||||
|
collectionVersionPath := configkit.Keys("collections", "weatherreporter", "version")
|
||||||
|
collectionVersion, ok := result.Metadata().Lookup(collectionVersionPath)
|
||||||
|
if !ok || !collectionVersion.Explicit || collectionVersion.Origin.Layer != configkit.LayerUnderlay || len(collectionVersion.Origin.Locations) != 1 {
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
contentPath := configkit.Keys("collections", "weatherreporter", "prompts", "custom", "content_file")
|
||||||
|
contentInfo, ok := result.Metadata().Lookup(contentPath)
|
||||||
|
if !ok || !contentInfo.Explicit || len(contentInfo.Origin.Inheritance) != 1 || len(contentInfo.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("inherited content field metadata = %#v, %v", contentInfo, ok)
|
||||||
|
}
|
||||||
|
contentLocation := contentInfo.Origin.Locations[0]
|
||||||
|
if parentPromptSource.ID == "" || contentLocation.SourceID != parentPromptSource.ID || contentLocation.Line != 4 ||
|
||||||
|
parentPromptSource.Destination.String() != "/collections/weatherreporter/prompts" {
|
||||||
|
t.Fatalf("inherited resource source coordinates = %#v, source %#v", contentLocation, parentPromptSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionPeerPromptConflictsRemainStrict(t *testing.T) {
|
||||||
|
destination := configkit.Keys("collections", "weatherreporter", "prompts")
|
||||||
|
_, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.NoSource()}, configkit.LayeredOptions[map[string]any]{
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||||
|
{Source: configkit.Bytes("peer-a", []byte("brief: {version: '1'}\n")), Destination: destination},
|
||||||
|
{Source: configkit.Bytes("peer-b", []byte("brief: {version: '1'}\n")), Destination: destination},
|
||||||
|
}}},
|
||||||
|
})
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != configkit.CodeComposeConflict {
|
||||||
|
t.Fatalf("duplicate peer prompt error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
136
examples/consumers/diff.go
Normal file
136
examples/consumers/diff.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileComparisonConfig is a small application-owned shape for comparing
|
||||||
|
// caller-selected configuration files.
|
||||||
|
type FileComparisonConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Entries map[string]any `yaml:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareFiles loads two caller-selected files with the same application
|
||||||
|
// definition, then formats only the safe records returned by configkit.Diff.
|
||||||
|
func CompareFiles(leftPath, rightPath string) (string, error) {
|
||||||
|
options := configkit.Options[FileComparisonConfig]{
|
||||||
|
Definition: configkit.Definition[FileComparisonConfig]{
|
||||||
|
Defaults: func() FileComparisonConfig {
|
||||||
|
return FileComparisonConfig{Entries: map[string]any{}}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
left, err := configkit.Load(configkit.File(leftPath), options)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("load left configuration: %w", err)
|
||||||
|
}
|
||||||
|
right, err := configkit.Load(configkit.File(rightPath), options)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("load right configuration: %w", err)
|
||||||
|
}
|
||||||
|
differences, err := configkit.Diff(left, right)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return FormatDifferences(differences), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolloutConfig models a service endpoint whose URL is derived by the
|
||||||
|
// application resolver and whose credential reference is classified.
|
||||||
|
type RolloutConfig struct {
|
||||||
|
Service RolloutService `yaml:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolloutService contains endpoint settings and a classified credential.
|
||||||
|
type RolloutService struct {
|
||||||
|
Endpoint RolloutEndpoint `yaml:"endpoint"`
|
||||||
|
Credential RolloutCredential `yaml:"credential"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolloutEndpoint is the effective destination used by the application.
|
||||||
|
type RolloutEndpoint struct {
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolloutCredential holds only a typed secret reference in configuration.
|
||||||
|
type RolloutCredential struct {
|
||||||
|
Token configkit.Secret `yaml:"token" configkit:"secret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRollout loads a base file, optionally with one selected configuration
|
||||||
|
// overlay. Resolution derives the effective URL after the overlay is applied.
|
||||||
|
func LoadRollout(basePath, overlayName string) (*configkit.Result[RolloutConfig], error) {
|
||||||
|
return configkit.Load(configkit.File(basePath), configkit.Options[RolloutConfig]{
|
||||||
|
Overlay: overlayName,
|
||||||
|
Definition: configkit.Definition[RolloutConfig]{
|
||||||
|
Resolve: resolveRollout,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareRolloutOverlay compares the effective base configuration with that
|
||||||
|
// same file plus one selected overlay. It presents only safe diff records.
|
||||||
|
func CompareRolloutOverlay(basePath, overlayName string) (string, error) {
|
||||||
|
base, err := LoadRollout(basePath, "")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("load base configuration: %w", err)
|
||||||
|
}
|
||||||
|
withOverlay, err := LoadRollout(basePath, overlayName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("load overlay configuration: %w", err)
|
||||||
|
}
|
||||||
|
differences, err := configkit.Diff(base, withOverlay)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return FormatDifferences(differences), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resolution) error {
|
||||||
|
endpoint := &configuration.Service.Endpoint
|
||||||
|
if endpoint.Host == "" || endpoint.Port <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
configkit.Keys("service", "endpoint", "host"),
|
||||||
|
configkit.Keys("service", "endpoint", "port"))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatDifferences is consumer-owned presentation. It distinguishes absence
|
||||||
|
// through Present and reports withheld changes through HiddenChanged without
|
||||||
|
// interpreting redaction marker text.
|
||||||
|
func FormatDifferences(differences []configkit.Difference) string {
|
||||||
|
if len(differences) == 0 {
|
||||||
|
return "No effective changes.\n"
|
||||||
|
}
|
||||||
|
var output strings.Builder
|
||||||
|
for _, difference := range differences {
|
||||||
|
fmt.Fprintf(&output, "%s: left=%s right=%s", difference.Path.String(),
|
||||||
|
formatSnapshot(difference.Left), formatSnapshot(difference.Right))
|
||||||
|
if difference.HiddenChanged {
|
||||||
|
output.WriteString(" hidden-change")
|
||||||
|
}
|
||||||
|
output.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return output.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSnapshot(snapshot configkit.ValueSnapshot) string {
|
||||||
|
if !snapshot.Present {
|
||||||
|
return "<absent>"
|
||||||
|
}
|
||||||
|
return strconv.Quote(snapshot.YAML)
|
||||||
|
}
|
||||||
187
examples/consumers/diff_test.go
Normal file
187
examples/consumers/diff_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleCompareFiles() {
|
||||||
|
directory, err := os.MkdirTemp("", "configkit-diff-files-")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(directory)
|
||||||
|
leftPath := filepath.Join(directory, "before.yml")
|
||||||
|
rightPath := filepath.Join(directory, "after.yml")
|
||||||
|
if err := os.WriteFile(leftPath, []byte("name: before\nentries: {}\n"), 0o600); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(rightPath, []byte("name: after\nentries:\n added: null\n"), 0o600); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, err := consumers.CompareFiles(leftPath, rightPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print(output)
|
||||||
|
// Output:
|
||||||
|
// /entries/added: left=<absent> right="null\n"
|
||||||
|
// /name: left="before\n" right="after\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleCompareRolloutOverlay() {
|
||||||
|
const baseToken = "CONFIGKIT_DIFF_EXAMPLE_BASE_TOKEN"
|
||||||
|
const overlayToken = "CONFIGKIT_DIFF_EXAMPLE_OVERLAY_TOKEN"
|
||||||
|
defer preserveEnvironment(baseToken)()
|
||||||
|
defer preserveEnvironment(overlayToken)()
|
||||||
|
_ = os.Setenv(baseToken, "disposable-example-base-credential")
|
||||||
|
_ = os.Setenv(overlayToken, "disposable-example-overlay-credential")
|
||||||
|
|
||||||
|
directory, err := os.MkdirTemp("", "configkit-diff-overlay-")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(directory)
|
||||||
|
basePath := filepath.Join(directory, "config.yml")
|
||||||
|
overlayPath := filepath.Join(directory, "overlays", "canary.yml")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overlayPath), 0o700); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
base := "service:\n endpoint:\n host: base.example\n port: 8443\n credential:\n token: " + baseToken + "\n"
|
||||||
|
overlay := "service:\n endpoint:\n host: canary.example\n credential:\n token: " + overlayToken + "\n"
|
||||||
|
if err := os.WriteFile(basePath, []byte(base), 0o600); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(overlayPath, []byte(overlay), 0o600); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, err := consumers.CompareRolloutOverlay(basePath, "canary")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print(output)
|
||||||
|
// Output:
|
||||||
|
// /service/credential/token: left="'[REDACTED]'\n" right="'[REDACTED]'\n" hidden-change
|
||||||
|
// /service/endpoint/host: left="base.example\n" right="canary.example\n"
|
||||||
|
// /service/endpoint/url: left="https://base.example:8443\n" right="https://canary.example:8443\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareFilesUsesCallerSelectedSourcesAndSafePresence(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
leftPath := filepath.Join(directory, "before.yml")
|
||||||
|
rightPath := filepath.Join(directory, "after.yml")
|
||||||
|
writeConfiguration(t, leftPath, "name: before\nentries: {}\n")
|
||||||
|
writeConfiguration(t, rightPath, "name: after\nentries:\n added: null\n")
|
||||||
|
|
||||||
|
output, err := consumers.CompareFiles(leftPath, rightPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := "/entries/added: left=<absent> right=\"null\\n\"\n" +
|
||||||
|
"/name: left=\"before\\n\" right=\"after\\n\"\n"
|
||||||
|
if output != want {
|
||||||
|
t.Fatalf("comparison output = %q, want %q", output, want)
|
||||||
|
}
|
||||||
|
if again, err := consumers.CompareFiles(leftPath, rightPath); err != nil || again != output {
|
||||||
|
t.Fatalf("repeat output = %q, %v", again, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareFilesReturnsEachLoadError(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
leftPath := filepath.Join(directory, "before.yml")
|
||||||
|
rightPath := filepath.Join(directory, "after.yml")
|
||||||
|
writeConfiguration(t, leftPath, "name: before\n")
|
||||||
|
writeConfiguration(t, rightPath, "unknown: invalid\n")
|
||||||
|
|
||||||
|
output, err := consumers.CompareFiles(leftPath, rightPath)
|
||||||
|
if err == nil || output != "" || !strings.Contains(err.Error(), "load right configuration") {
|
||||||
|
t.Fatalf("right load failure = %q, %v", output, err)
|
||||||
|
}
|
||||||
|
if _, err := consumers.CompareFiles(filepath.Join(directory, "missing.yml"), rightPath); err == nil || !strings.Contains(err.Error(), "load left configuration") {
|
||||||
|
t.Fatalf("left load failure = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareRolloutOverlayShowsResolvedSafeChangesAndRetainsSiblings(t *testing.T) {
|
||||||
|
t.Setenv("CONFIGKIT_DIFF_BASE_TOKEN", "disposable-base-credential")
|
||||||
|
t.Setenv("CONFIGKIT_DIFF_OVERLAY_TOKEN", "disposable-overlay-credential")
|
||||||
|
directory := t.TempDir()
|
||||||
|
basePath := filepath.Join(directory, "config.yml")
|
||||||
|
overlayPath := filepath.Join(directory, "overlays", "canary.yml")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(overlayPath), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeConfiguration(t, basePath, "service:\n endpoint:\n host: base.example\n port: 8443\n credential:\n token: CONFIGKIT_DIFF_BASE_TOKEN\n")
|
||||||
|
writeConfiguration(t, overlayPath, "service:\n endpoint:\n host: canary.example\n credential:\n token: CONFIGKIT_DIFF_OVERLAY_TOKEN\n")
|
||||||
|
|
||||||
|
base, err := consumers.LoadRollout(basePath, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
withOverlay, err := consumers.LoadRollout(basePath, "canary")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if base.Config.Service.Endpoint.Port != 8443 || withOverlay.Config.Service.Endpoint.Port != 8443 {
|
||||||
|
t.Fatalf("lower-priority port was not retained: base=%#v overlay=%#v", base.Config.Service.Endpoint, withOverlay.Config.Service.Endpoint)
|
||||||
|
}
|
||||||
|
urlInfo, found := withOverlay.Metadata().Lookup(configkit.Keys("service", "endpoint", "url"))
|
||||||
|
if !found || urlInfo.Origin.Layer != configkit.LayerResolution || withOverlay.Config.Service.Endpoint.URL != "https://canary.example:8443" {
|
||||||
|
t.Fatalf("resolved endpoint = %#v, metadata=%#v, %v", withOverlay.Config.Service.Endpoint, urlInfo, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
differences, err := configkit.Diff(base, withOverlay)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(differences) != 3 {
|
||||||
|
t.Fatalf("differences = %#v", differences)
|
||||||
|
}
|
||||||
|
wantPaths := []string{"/service/credential/token", "/service/endpoint/host", "/service/endpoint/url"}
|
||||||
|
for index, difference := range differences {
|
||||||
|
if got := difference.Path.String(); got != wantPaths[index] {
|
||||||
|
t.Errorf("path[%d] = %q, want %q", index, got, wantPaths[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secret := differences[0]
|
||||||
|
if !secret.Left.Redacted || !secret.Right.Redacted || !secret.HiddenChanged || secret.Left.YAML != "'[REDACTED]'\n" || secret.Right.YAML != "'[REDACTED]'\n" {
|
||||||
|
t.Fatalf("classified credential difference = %#v", secret)
|
||||||
|
}
|
||||||
|
if strings.Contains(consumers.FormatDifferences(differences), "disposable-") {
|
||||||
|
t.Fatal("formatted differences exposed credential contents")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := consumers.CompareRolloutOverlay(basePath, "canary")
|
||||||
|
if err != nil || output != consumers.FormatDifferences(differences) {
|
||||||
|
t.Fatalf("formatted base/overlay comparison = %q, %v", output, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatDifferencesUsesRecordMembershipForNoChange(t *testing.T) {
|
||||||
|
if got := consumers.FormatDifferences(nil); got != "No effective changes.\n" {
|
||||||
|
t.Fatalf("no-change output = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfiguration(t *testing.T, filename, contents string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filename, []byte(contents), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
45
examples/consumers/discovery_test.go
Normal file
45
examples/consumers/discovery_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/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
|
||||||
|
}
|
||||||
|
fmt.Println(result.Config.Name, filepath.Base(result.Sources()[0].Path))
|
||||||
|
// Output: example service.yml
|
||||||
|
}
|
||||||
20
examples/consumers/inheritance.go
Normal file
20
examples/consumers/inheritance.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
|
||||||
|
// PresetCatalog is ordinary application data; no inheritance fields or
|
||||||
|
// registration hooks are required in the typed configuration.
|
||||||
|
type PresetCatalog struct {
|
||||||
|
Presets map[string]Preset `yaml:"presets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Preset struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Settings map[string]map[string]int `yaml:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPresets performs one load; callers may inspect inherited presence and
|
||||||
|
// source origins on the returned result before choosing a preset to execute.
|
||||||
|
func LoadPresets(source configkit.Source) (*configkit.Result[PresetCatalog], error) {
|
||||||
|
return configkit.Load[PresetCatalog](source)
|
||||||
|
}
|
||||||
35
examples/consumers/inheritance_test.go
Normal file
35
examples/consumers/inheritance_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPresetCatalogOneLoadAndDerivation(t *testing.T) {
|
||||||
|
result, err := consumers.LoadPresets(configkit.File(fixture("inheritance", "presets.yml")))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Presets["base"].Settings["secondary"]["width"] != 10 ||
|
||||||
|
result.Config.Presets["custom"].Settings["secondary"]["width"] != 20 ||
|
||||||
|
result.Config.Presets["custom"].Enabled {
|
||||||
|
t.Fatalf("expanded presets = %#v", result.Config.Presets)
|
||||||
|
}
|
||||||
|
enabled := configkit.Keys("presets", "custom", "enabled")
|
||||||
|
if !result.Metadata().IsExplicit(enabled) {
|
||||||
|
t.Fatal("inherited false lost explicit presence")
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(enabled)
|
||||||
|
if !found || info.Origin.Locations[0].SourceID != "source-1" ||
|
||||||
|
len(info.Origin.Inheritance) != 1 || info.Origin.Inheritance[0].From.String() != "/presets/base/enabled" {
|
||||||
|
t.Fatalf("origin and derivation = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
nested := configkit.Keys("presets", "custom", "settings", "secondary", "width")
|
||||||
|
info, found = result.Metadata().Lookup(nested)
|
||||||
|
if !found || len(info.Origin.Inheritance) != 1 ||
|
||||||
|
info.Origin.Inheritance[0].From.String() != "/presets/custom/settings/primary/width" {
|
||||||
|
t.Fatalf("destination-local derivation = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
119
examples/consumers/layered.go
Normal file
119
examples/consumers/layered.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package consumers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
//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.
|
||||||
|
type BundledConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
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")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configkit.Load[BundledConfig](configkit.NewFilesystem("bundled", files).File("config.yml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogConfig keeps resource names and domain rules in the application.
|
||||||
|
type CatalogConfig struct {
|
||||||
|
Mode string `yaml:"mode"`
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Workers int `yaml:"workers"`
|
||||||
|
Templates map[string]string `yaml:"templates"`
|
||||||
|
Labels map[string]string `yaml:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCatalog combines two underlay tiers beneath an independently located
|
||||||
|
// host main file. ConfigKit assembles and validates the complete result once.
|
||||||
|
func LoadCatalog(mainPath string) (*configkit.Result[CatalogConfig], error) {
|
||||||
|
files, err := fs.Sub(bundledFiles, "testdata/layered-bundle")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundle := configkit.NewFilesystem("catalog bundle", files)
|
||||||
|
return configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)},
|
||||||
|
configkit.LayeredOptions[CatalogConfig]{
|
||||||
|
Underlays: []configkit.Underlay{
|
||||||
|
{Inputs: []configkit.Input{{Source: bundle.File("defaults/config.yml"), Imports: true}}},
|
||||||
|
{Inputs: []configkit.Input{{Source: configkit.Mapping("local policy", map[string]any{
|
||||||
|
"workers": 2,
|
||||||
|
"labels": map[string]any{"tenant": "example"},
|
||||||
|
})}}},
|
||||||
|
},
|
||||||
|
Definition: configkit.Definition[CatalogConfig]{
|
||||||
|
Defaults: func() CatalogConfig { return CatalogConfig{Mode: "demo"} },
|
||||||
|
Validate: validateCatalog,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCatalog(configuration *CatalogConfig, validation *configkit.Validation) {
|
||||||
|
if configuration.Mode != "demo" && configuration.Mode != "production" {
|
||||||
|
validation.Error(configkit.Keys("mode"), "must be demo or production")
|
||||||
|
}
|
||||||
|
if configuration.Workers < 0 {
|
||||||
|
validation.Error(configkit.Keys("workers"), "must be nonnegative")
|
||||||
|
}
|
||||||
|
if name := configuration.Templates["welcome"]; name == "." || !fs.ValidPath(name) {
|
||||||
|
validation.Error(configkit.Keys("templates", "welcome"), "must be a relative resource name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadWelcome follows the effective template field's one declaring source to
|
||||||
|
// its retained resource context. Opening and interpreting the resource remain
|
||||||
|
// application work; this does not reopen configuration or change metadata.
|
||||||
|
func ReadWelcome(result *configkit.Result[CatalogConfig]) (string, error) {
|
||||||
|
if result == nil {
|
||||||
|
return "", errors.New("catalog result is nil")
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys("templates", "welcome"))
|
||||||
|
if !found || len(info.Origin.Locations) != 1 {
|
||||||
|
return "", errors.New("welcome template has no single declaring source")
|
||||||
|
}
|
||||||
|
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found {
|
||||||
|
return "", errors.New("welcome template has no resource context")
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, result.Config.Templates["welcome"]))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
151
examples/consumers/layered_test.go
Normal file
151
examples/consumers/layered_test.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package consumers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_nativeFilesystem() {
|
||||||
|
result, err := consumers.LoadBundled()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(result.Config.Name, result.Config.Enabled)
|
||||||
|
// Output: embedded false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_rootRelativeImports() {
|
||||||
|
result, err := consumers.LoadPromptBundle()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source := result.Sources()[2]
|
||||||
|
fmt.Println(result.Config.Name, result.Config.Version, result.Config.Prompts["brief"].Summary, source.Destination.String(), err)
|
||||||
|
// Output: Root import example 1.0 A root-loaded prompt. /prompts <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPromptBundleUsesRelativeImportDestinations(t *testing.T) {
|
||||||
|
result, err := consumers.LoadPromptBundle()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Name != "Root import example" || result.Config.Version != "1.0" || result.Config.Prompts["brief"].Summary != "A root-loaded prompt." {
|
||||||
|
t.Fatalf("root import result = %#v", result.Config)
|
||||||
|
}
|
||||||
|
sources := result.Sources()
|
||||||
|
if len(sources) != 3 || sources[0].Destination.String() != configkit.Keys().String() ||
|
||||||
|
sources[1].Path != "metadata.yml" || sources[1].Destination.String() != configkit.Keys().String() ||
|
||||||
|
sources[2].Path != "prompts/brief.yml" || sources[2].Destination.String() != "/prompts" ||
|
||||||
|
sources[2].ImportedFrom == nil || sources[2].ImportedFrom.SourceID != sources[0].ID {
|
||||||
|
t.Fatalf("root import sources = %#v", sources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_layeredCatalog() {
|
||||||
|
result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
welcome, err := consumers.ReadWelcome(result)
|
||||||
|
fmt.Println(result.Config.Mode, result.Config.Enabled, result.Config.Workers,
|
||||||
|
result.Config.Labels["origin"], result.Config.Labels["tenant"], strings.TrimSpace(welcome), err)
|
||||||
|
// 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.VersionCheck; 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.VersionCheck
|
||||||
|
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 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"enabled", "workers"} {
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys(key))
|
||||||
|
if !found || !info.Explicit || info.Origin.Layer != configkit.LayerBase {
|
||||||
|
t.Fatalf("%s presence = %#v, %v", key, info, found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(configkit.Keys("templates", "welcome"))
|
||||||
|
if !found || info.Origin.Layer != configkit.LayerUnderlay || len(info.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("welcome origin = %#v, %v", info, found)
|
||||||
|
}
|
||||||
|
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found || context.BaseDir != "defaults" {
|
||||||
|
t.Fatalf("welcome context = %#v, %v", context, found)
|
||||||
|
}
|
||||||
|
welcome, err := consumers.ReadWelcome(result)
|
||||||
|
if err != nil || strings.TrimSpace(welcome) != "Welcome from the bundle." {
|
||||||
|
t.Fatalf("welcome = %q, %v", welcome, err)
|
||||||
|
}
|
||||||
|
if result.Config.Labels["tenant"] != "example" || result.Config.Labels["origin"] != "bundle" {
|
||||||
|
t.Fatalf("recursive underlay labels = %#v", result.Config.Labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogValidationRejectsInvalidMode(t *testing.T) {
|
||||||
|
_, err := consumers.LoadCatalog(fixture("layered-main", "invalid.yml"))
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) == 0 ||
|
||||||
|
loadError.Diagnostics()[0].Code != configkit.CodeValidateError ||
|
||||||
|
loadError.Diagnostics()[0].Path.String() != "/mode" {
|
||||||
|
t.Fatalf("invalid mode error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
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 "gitea.maximumdirect.net/eric/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 "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/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.ImportSelections {
|
||||||
|
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
|
||||||
|
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 "gitea.maximumdirect.net/eric/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.Paths(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.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
|
||||||
|
"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 "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
"gitea.maximumdirect.net/eric/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(`
|
||||||
|
configkit:
|
||||||
|
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(`
|
||||||
|
configkit:
|
||||||
|
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 "gitea.maximumdirect.net/eric/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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Secret: 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"),
|
||||||
|
}}}},
|
||||||
|
})
|
||||||
|
}
|
||||||
9
examples/consumers/testdata/collection-bundle/collections/weather/collection.yml
vendored
Normal file
9
examples/consumers/testdata/collection-bundle/collections/weather/collection.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: metadata.yml
|
||||||
|
destination: []
|
||||||
|
- path: prompts/index.yml
|
||||||
|
destination: [prompts]
|
||||||
|
- path: profiles/index.yml
|
||||||
|
destination: [profiles]
|
||||||
|
name: Weather Reporter
|
||||||
4
examples/consumers/testdata/collection-bundle/collections/weather/metadata.yml
vendored
Normal file
4
examples/consumers/testdata/collection-bundle/collections/weather/metadata.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: version.yml
|
||||||
|
description: Imported collection metadata.
|
||||||
3
examples/consumers/testdata/collection-bundle/collections/weather/profiles/compact.yml
vendored
Normal file
3
examples/consumers/testdata/collection-bundle/collections/weather/profiles/compact.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
compact:
|
||||||
|
format: text
|
||||||
|
temperature: 0.2
|
||||||
4
examples/consumers/testdata/collection-bundle/collections/weather/profiles/index.yml
vendored
Normal file
4
examples/consumers/testdata/collection-bundle/collections/weather/profiles/index.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: compact.yml
|
||||||
|
- path: weather.yml
|
||||||
3
examples/consumers/testdata/collection-bundle/collections/weather/profiles/weather.yml
vendored
Normal file
3
examples/consumers/testdata/collection-bundle/collections/weather/profiles/weather.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
weather:
|
||||||
|
extends: compact
|
||||||
|
temperature: 0.4
|
||||||
7
examples/consumers/testdata/collection-bundle/collections/weather/prompts/brief.yml
vendored
Normal file
7
examples/consumers/testdata/collection-bundle/collections/weather/prompts/brief.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
brief:
|
||||||
|
version: "1"
|
||||||
|
default_profile: compact
|
||||||
|
content_file: welcome.txt
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: Write a concise weather summary.
|
||||||
3
examples/consumers/testdata/collection-bundle/collections/weather/prompts/custom.yml
vendored
Normal file
3
examples/consumers/testdata/collection-bundle/collections/weather/prompts/custom.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
custom:
|
||||||
|
extends: brief
|
||||||
|
default_profile: weather
|
||||||
4
examples/consumers/testdata/collection-bundle/collections/weather/prompts/index.yml
vendored
Normal file
4
examples/consumers/testdata/collection-bundle/collections/weather/prompts/index.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: brief.yml
|
||||||
|
- path: custom.yml
|
||||||
1
examples/consumers/testdata/collection-bundle/collections/weather/prompts/welcome.txt
vendored
Normal file
1
examples/consumers/testdata/collection-bundle/collections/weather/prompts/welcome.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Welcome to the weather collection.
|
||||||
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"
|
||||||
2
examples/consumers/testdata/collection-bundle/operators/weather-prompt-version.yml
vendored
Normal file
2
examples/consumers/testdata/collection-bundle/operators/weather-prompt-version.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
brief:
|
||||||
|
version: "2"
|
||||||
10
examples/consumers/testdata/inheritance/presets.yml
vendored
Normal file
10
examples/consumers/testdata/inheritance/presets.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
presets:
|
||||||
|
base:
|
||||||
|
enabled: false
|
||||||
|
settings:
|
||||||
|
primary: {width: 10}
|
||||||
|
secondary: {extends: primary}
|
||||||
|
custom:
|
||||||
|
extends: base
|
||||||
|
settings:
|
||||||
|
primary: {width: 20}
|
||||||
7
examples/consumers/testdata/layered-bundle/defaults/config.yml
vendored
Normal file
7
examples/consumers/testdata/layered-bundle/defaults/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: ../shared.yml
|
||||||
|
enabled: true
|
||||||
|
workers: 4
|
||||||
|
templates:
|
||||||
|
welcome: messages/welcome.txt
|
||||||
1
examples/consumers/testdata/layered-bundle/defaults/messages/welcome.txt
vendored
Normal file
1
examples/consumers/testdata/layered-bundle/defaults/messages/welcome.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Welcome from the bundle.
|
||||||
2
examples/consumers/testdata/layered-bundle/shared.yml
vendored
Normal file
2
examples/consumers/testdata/layered-bundle/shared.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
labels:
|
||||||
|
origin: bundle
|
||||||
3
examples/consumers/testdata/layered-main/config.yml
vendored
Normal file
3
examples/consumers/testdata/layered-main/config.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mode: production
|
||||||
|
enabled: false
|
||||||
|
workers: 0
|
||||||
1
examples/consumers/testdata/layered-main/invalid.yml
vendored
Normal file
1
examples/consumers/testdata/layered-main/invalid.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mode: unsupported
|
||||||
2
examples/consumers/testdata/native-source/config.yml
vendored
Normal file
2
examples/consumers/testdata/native-source/config.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
name: embedded
|
||||||
|
enabled: false
|
||||||
6
examples/consumers/testdata/ordinary-imports/config.yml
vendored
Normal file
6
examples/consumers/testdata/ordinary-imports/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: metadata.yml
|
||||||
|
- path: prompts/brief.yml
|
||||||
|
destination: [prompts]
|
||||||
|
name: Root import example
|
||||||
2
examples/consumers/testdata/ordinary-imports/metadata.yml
vendored
Normal file
2
examples/consumers/testdata/ordinary-imports/metadata.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
version: "1.0"
|
||||||
|
description: Metadata imported into the root document.
|
||||||
2
examples/consumers/testdata/ordinary-imports/prompts/brief.yml
vendored
Normal file
2
examples/consumers/testdata/ordinary-imports/prompts/brief.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
brief:
|
||||||
|
summary: A root-loaded prompt.
|
||||||
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 @@
|
|||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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
|
||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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 @@
|
|||||||
|
configkit:
|
||||||
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user