438 Commits

Author SHA1 Message Date
5959537d1a Prepare the ConfigKit v0.10.0 release 2026-10-24 19:45:33 +00:00
0369111d30 Resolve Windows rooted search paths with Go 2026-10-24 19:24:30 +00:00
2fedb0307f Use Go rules for Windows filename components 2026-10-24 19:20:54 +00:00
087050fa25 Isolate user configuration discovery tests by platform 2026-10-24 19:18:13 +00:00
1ac1f8bc9e Preserve failing search candidate diagnostics 2026-10-24 19:16:50 +00:00
4dc3e70877 Validate search filesystem context after selection 2026-10-24 19:13:35 +00:00
f64497cb13 Record discovery algorithm audit findings 2026-10-24 19:03:04 +00:00
68cf5c489b Record configuration discovery review findings 2026-10-24 17:52:20 +00:00
e50fee2ca5 Complete the configuration discovery implementation plan 2026-10-24 16:26:09 +00:00
eec214944c Align discovery ownership and contributor documentation 2026-10-24 16:24:57 +00:00
4f2c113237 Extend discovery coverage across consumer workflows 2026-10-24 16:20:53 +00:00
2d93a5c703 Expose configuration search through the public facade 2026-10-24 16:16:07 +00:00
2f6c1eca00 Integrate configuration searches into engine loading 2026-10-24 16:11:03 +00:00
8642337966 Load ordinary overlays without a main source 2026-10-24 15:59:39 +00:00
53ce6c63d8 Allow absent inputs to use import contexts 2026-10-24 15:55:10 +00:00
2f0900535d Select configuration files through host rules 2026-10-24 15:46:36 +00:00
4748313105 Expand ordered configuration search candidates 2026-10-24 15:43:10 +00:00
e2634c8815 Define immutable configuration searches 2026-10-24 15:39:59 +00:00
424c89780e Plan configuration discovery implementation 2026-10-24 15:04:02 +00:00
92309ddcc3 Update the discovery roadmap baseline 2026-10-24 14:54:56 +00:00
7ff204bf3d Clarify v0.9.0 import and collection migration guidance 2026-10-24 14:48:21 +00:00
b3ec860fac Document the ConfigKit v0.9.0 release 2026-10-24 14:44:58 +00:00
435dea102f Retire the completed remediation record 2026-10-24 14:41:56 +00:00
ea46001f78 Reuse document indexes for collection policy checks 2026-10-24 14:17:09 +00:00
a0e5693d07 Share indexed fallback locations during effective projection 2026-10-24 14:13:52 +00:00
5c5f2af6cc Limit inheritance policy work to the relevant scope 2026-10-24 14:10:44 +00:00
8253993005 Delete CLI metadata through the replaced subtree 2026-10-24 14:05:42 +00:00
d0848290da Keep sequence projection storage proportional to assigned items 2026-10-24 14:04:26 +00:00
e94a0642ff Keep global registrations out of item-local composition 2026-10-24 13:59:34 +00:00
571d063675 Retire the completed sequence roadmap and implementation plan 2026-10-24 13:56:19 +00:00
d0a605afbb Document the internal complexity refactor roadmap 2026-10-24 02:02:43 +00:00
7a83704f43 Clarify discovery contexts and filename selection boundaries 2026-10-24 02:00:17 +00:00
ac5bf29ddc Settle discovery slug and custom path rules 2026-10-24 01:58:48 +00:00
cce8356b8e Settle empty discovery searches and filename selections 2026-10-24 01:55:02 +00:00
b746dcc609 Settle discovery behavior when no main file is found 2026-10-24 01:51:08 +00:00
9c32b8d3b5 Record sequence algorithm audit findings 2026-10-24 01:46:55 +00:00
c73daf891d Settle discovery directory resolution errors 2026-10-24 01:39:25 +00:00
ae038a5165 Settle discovery presets and directory helpers 2026-10-24 01:33:53 +00:00
367bbaaa1b Clarify sequence operation history in the configuration contract 2026-10-24 01:21:11 +00:00
fafc28421f Build item policies without repeated accumulated copies 2026-10-24 01:20:12 +00:00
1a673753b9 Preserve child identity in inheritance history 2026-10-24 01:17:00 +00:00
988e49bb09 Define discovery filename groups and search presets 2026-10-24 01:16:42 +00:00
5bb4f364b7 Track the main version through sequence movement 2026-10-24 01:11:28 +00:00
af84107905 Document composable configuration discovery 2026-10-24 01:00:08 +00:00
f94dd9b961 Order CLI metadata history after inheritance 2026-10-24 00:55:21 +00:00
146e7e56d9 Retire obsolete collection identity before replacement agreement 2026-10-24 00:51:10 +00:00
d2e20a304a Record sequence feature integration audit findings 2026-10-24 00:38:04 +00:00
8d40983271 Avoid repeated prefix scans when projecting item policies 2026-10-24 00:19:14 +00:00
25cf7ce05c Clarify final identity validation and overlay semantics 2026-10-24 00:16:53 +00:00
57edb8ec19 Identify conflicting sequence registration positions 2026-10-24 00:15:23 +00:00
42663cf07c Retain main version warnings in layered loads 2026-10-24 00:13:54 +00:00
085b57eeb6 Validate final identity fields as application payload 2026-10-24 00:12:31 +00:00
dcf0379e02 Apply nested collection operations when copying mappings 2026-10-24 00:11:21 +00:00
8ecfedc91b Reset inherited YAML identity on actual replacement 2026-10-24 00:07:22 +00:00
f7624ae92a Project incoming source destinations through tier operations 2026-10-24 00:05:24 +00:00
a035df9845 Preserve tier sequence operations in source history 2026-10-24 00:01:46 +00:00
e6598810e0 Project sequence origins from incoming to final item paths 2026-10-23 23:51:33 +00:00
f8cbb2ed87 Preserve retained input presence in nested CLI overrides 2026-10-23 23:47:21 +00:00
af80ccb90c Record sequence implementation review findings 2026-10-23 23:30:27 +00:00
0be7cdb00e Audit sequence identity scaling and lifecycle 2026-10-23 22:03:16 +00:00
e01aeca761 Reconcile sequence identity documentation 2026-10-23 21:57:35 +00:00
d4133f23f5 Add integrated sequence catalog examples 2026-10-23 21:50:10 +00:00
5af71a7533 Validate final registered sequence identities 2026-10-23 21:43:54 +00:00
41b5dff69b Apply CLI overrides after typed decoding 2026-10-23 21:37:13 +00:00
ecfb78d84d Prepare post-decode CLI inspection context 2026-10-23 21:20:19 +00:00
041c4cff02 Apply CLI overrides to decoded configuration 2026-10-23 21:11:54 +00:00
23f22d8982 Prepare reusable CLI override batches 2026-10-23 21:02:46 +00:00
0023617a07 Expose sequence identity registrations through the facade 2026-10-23 20:59:23 +00:00
33a51606da Resolve inheritance between identified records 2026-10-23 20:48:46 +00:00
268fd03579 Prepare reusable inheritance scope execution 2026-10-23 20:32:10 +00:00
9d4d2e2a17 Build identified record inheritance graphs 2026-10-23 20:23:09 +00:00
36ff9170b7 Apply collection policies during sibling inheritance 2026-10-23 20:19:05 +00:00
9e75496664 Prepare collection policy transport through inheritance 2026-10-23 20:04:06 +00:00
80d8c1dc82 Preserve nested collection policy ownership 2026-10-23 19:56:06 +00:00
acd9e9a9fa Integrate collection policies across layered assembly 2026-10-23 19:49:12 +00:00
89a9684f0d Activate selected overlay collection policies 2026-10-23 19:30:48 +00:00
9a7027a04c Activate ordinary source collection declarations 2026-10-23 19:12:39 +00:00
dfeaf90320 Integrate policy-aware item scopes 2026-10-23 18:55:03 +00:00
d03e56398b Resolve collection identity during strict composition 2026-10-23 18:39:04 +00:00
628fc53d2c Separate underlay acquisition from composition 2026-10-23 18:22:37 +00:00
5fec16a768 Separate input acquisition from composition 2026-10-23 18:15:01 +00:00
f6b02dec03 Project sequence evidence through item mappings 2026-10-23 18:07:15 +00:00
7b04dc6a50 Extend history replay for mapped sequence items 2026-10-23 17:55:45 +00:00
12fdf0d331 Implement shared sequence operations 2026-10-23 17:49:05 +00:00
b202dd187f Track collection policy ownership 2026-10-23 14:16:22 +00:00
0314e587b6 Reconcile collection declarations and modes 2026-10-23 14:07:07 +00:00
9d7635d1e4 Parse collection sequence declarations 2026-10-23 14:01:39 +00:00
298e70b552 Add dynamic sequence identity selectors 2026-10-23 13:55:25 +00:00
64c51647f0 Add shared sequence identity primitives 2026-10-23 13:49:22 +00:00
bc6b977585 Split the sequence implementation into smaller stages 2026-10-23 13:40:29 +00:00
d06aae5461 Plan sequence identity and combination implementation 2026-10-23 12:56:23 +00:00
4f941f8ba4 Clarify sequence policy interactions and rename the roadmap 2026-10-23 12:20:18 +00:00
9a49833339 Remove the completed operator convention audit 2026-10-23 12:17:29 +00:00
749cf6868c Settle the sequence identity registration API 2026-10-23 12:09:44 +00:00
a5f0b92ee9 Add dynamic identity constraints to the sequence roadmap 2026-10-23 01:56:00 +00:00
6ec23b2de1 Extend collection inheritance architecture to identified sequences 2026-10-23 01:42:32 +00:00
3298eb916c Settle how sequence inheritance references compose 2026-10-23 01:40:37 +00:00
40430e787c Add identified record inheritance to the sequence roadmap 2026-10-23 01:31:22 +00:00
f434fa159f Align architecture policy with collection and CLI decisions 2026-10-23 01:12:11 +00:00
8ddf73ede3 Accept collection policy and CLI lifecycle decisions 2026-10-23 01:09:24 +00:00
844a15a3cb Consolidate the sequence roadmap around the settled design 2026-10-23 00:11:50 +00:00
58f4c39d6f Settle sequence identity declarations without values 2026-10-23 00:04:55 +00:00
8a54cecd1d Settle nested sequence policy ownership and record review findings 2026-10-23 00:02:26 +00:00
0e430e8077 Allow lower-tier identity during strict composition 2026-10-22 23:53:48 +00:00
26caa6024e Settle persistent sequence operation policy 2026-10-22 23:47:50 +00:00
8691a93ebf Settle CLI ordering before application resolution 2026-10-22 23:41:06 +00:00
c563f80934 Record remaining sequence lifecycle decisions 2026-10-22 15:45:21 +00:00
27797d2392 Settle sequence inheritance and lifecycle coverage 2026-10-22 15:16:42 +00:00
6a4e57e5d3 Settle sequence validation and absent value behavior 2026-10-22 14:47:00 +00:00
29f94d89e1 Settle sequence mode names and identity-based defaults 2026-10-22 14:39:40 +00:00
12f6045da0 Settle sequence combination scope and ordering 2026-10-22 14:08:55 +00:00
6eb80b06e3 Settle sequence identity definitions and policy lifetime 2026-10-22 13:28:18 +00:00
86feb6d755 Settle collection declaration syntax in the sequence roadmap 2026-10-21 14:00:02 +00:00
8bf49f6ea6 Include overlay author controls in the sequence proposal 2026-10-20 14:08:57 +00:00
f3d0fe6411 Record final identity guarantees for application registrations 2026-10-20 14:03:26 +00:00
cba3a0efb0 Record whole-record sequence replacement and application identity constraints 2026-10-20 13:57:19 +00:00
b58cd75c98 Draft sequence identity and layered append proposal 2026-10-20 13:48:54 +00:00
c530a989df Audit operator conventions against Ansible 2026-10-20 13:28:21 +00:00
2baf14d9a5 Retire completed import integration remediation 2026-10-20 04:11:54 +00:00
6903c9e51a Clarify empty path import inspection evidence 2026-10-20 01:06:15 +00:00
31e10af9cb Clarify scheduled provenance and history ordering 2026-10-20 01:05:34 +00:00
0d841d9d73 Resolve contained symlinks in import directory prefixes 2026-10-20 01:04:31 +00:00
4bcefbc9d4 Record import integration audit findings 2026-10-20 00:38:04 +00:00
91db99b967 Retire completed import discovery plans 2026-10-20 00:32:11 +00:00
5747ec49c6 Count import depth only for selected sources 2026-10-20 00:30:35 +00:00
d3a714a1cb Validate complete import depth numbers 2026-10-20 00:27:55 +00:00
7f8ad81065 Track active symlink expansions precisely 2026-10-20 00:26:30 +00:00
75da54150f Resolve native imports beneath nested roots 2026-10-20 00:23:55 +00:00
38fb315415 Check item version ownership at effective paths 2026-10-20 00:22:43 +00:00
1c79ec3aa1 Preserve native import alias identity 2026-10-20 00:20:38 +00:00
01e1b2fc2d Record import discovery review findings 2026-10-20 00:03:23 +00:00
15943e233c Record completed import roadmap 2026-10-19 21:23:54 +00:00
f6a6b0184b Finalize import discovery documentation and audits 2026-10-19 21:23:47 +00:00
898035d83a Record prompt catalog example completion 2026-10-19 21:15:53 +00:00
de3666aed3 Expand prompt catalog discovery example 2026-10-19 21:15:47 +00:00
c00811da2e Record lifecycle audit completion 2026-10-19 21:13:29 +00:00
55abcf8340 Audit scheduled import and inheritance lifecycles 2026-10-19 21:13:21 +00:00
9d7c79cf0b Record public import API completion 2026-10-19 21:06:39 +00:00
24131617c7 Expose import defaults and selection evidence 2026-10-19 21:06:31 +00:00
05b67cbfcf Record empty import evidence completion 2026-10-19 20:57:53 +00:00
e433988962 Retain empty optional import selection evidence 2026-10-19 20:57:41 +00:00
05a28d1178 Record final import wiring completion 2026-10-19 20:51:01 +00:00
1e5bffc0e6 Wire final import declarations through loading 2026-10-19 20:50:47 +00:00
f5a689b578 Record glob discovery completion 2026-10-19 20:38:48 +00:00
e4abff4164 Filter and account final glob candidates 2026-10-19 20:38:42 +00:00
1a8f1cc3a5 Record directory link discovery completion 2026-10-19 20:29:36 +00:00
9b99463187 Follow links safely during directory discovery 2026-10-19 20:29:20 +00:00
f69fa24b40 Record recursive directory search completion 2026-10-19 20:18:44 +00:00
77d97296ea Add recursive directory search with depth limits 2026-10-19 20:18:39 +00:00
032679bf34 Record shallow directory selection completion 2026-10-19 20:13:39 +00:00
55a59de6db Select shallow directory import sources 2026-10-19 20:13:33 +00:00
a0d13962bd Record named import selection completion 2026-10-19 20:08:05 +00:00
5f33e1e90c Select named imports before source admission 2026-10-19 20:07:54 +00:00
b750b0fec6 Record private import parser completion 2026-10-19 20:02:12 +00:00
b498e5bee5 Define private import defaults and declaration parsing 2026-10-19 20:02:05 +00:00
ba577429e6 Record underlay group scheduling completion 2026-10-19 19:53:08 +00:00
014b99504b Schedule underlay groups as dependency forests 2026-10-19 19:53:01 +00:00
263dd2f1bd Record item scope scheduling completion 2026-10-19 19:39:30 +00:00
af43d6f438 Schedule item-local import scopes 2026-10-19 19:39:19 +00:00
a4cbd6390e Record ordinary import scheduling completion 2026-10-19 19:29:52 +00:00
5ed0f55970 Schedule ordinary import contributions by dependency 2026-10-19 19:29:35 +00:00
91ab37a39d Record import graph completion 2026-10-19 17:34:45 +00:00
fbe9ae6d8b Retain import inclusion graph during acquisition 2026-10-19 17:34:21 +00:00
1aa121df71 Record scheduled inheritance completion 2026-10-19 17:25:32 +00:00
6e40c166d6 Schedule sibling inheritance by dependency readiness 2026-10-19 17:25:27 +00:00
a56d4691ff Record inheritance graph groundwork 2026-10-19 15:18:37 +00:00
2718fbb8fe Separate inheritance graph analysis and application 2026-10-19 15:18:25 +00:00
2d52421919 Record deterministic scheduler completion 2026-10-19 15:11:42 +00:00
167fcd475f Add deterministic dependency scheduler 2026-10-19 15:11:28 +00:00
aa93aeda3f Plan import discovery and shared dependency ordering 2026-10-19 14:33:01 +00:00
94381a67fd Record accepted import architecture decisions 2026-10-19 14:24:19 +00:00
c26b34147b Clarify import candidate identity, accounting, and inspection 2026-10-19 14:15:01 +00:00
c9f9c98fbe Settle import validation and require recursion for depth 2026-10-19 14:05:18 +00:00
f6bf4454b0 Settle import selector lists and exclusions 2026-10-19 13:43:56 +00:00
a7ca2c59a2 Settle import defaults, discovery boundaries, and inspection decisions 2026-10-19 01:54:01 +00:00
10c14aa794 Add architecture principles for simplicity and permissive defaults 2026-10-18 21:13:28 +00:00
e9774b252b Adopt shared dependency-ready ordering for imports and inheritance 2026-10-18 21:09:05 +00:00
19fe621df1 Apply permissive defaults and remove import transition support 2026-10-18 16:37:07 +00:00
82b289f4fe Clarify import roadmap decisions using Ansible conventions 2026-10-18 16:28:38 +00:00
137769f3f1 Document the ConfigKit v0.8.0 release 2026-10-17 04:58:01 +00:00
ef28eabaf8 Retire completed sequence composition plans 2026-10-17 04:56:51 +00:00
008d2ba9c8 Clarify when sequence identities are validated 2026-10-17 04:55:32 +00:00
8e3a0892d4 Project item conflict diagnostics through enclosing scopes 2026-10-17 04:54:01 +00:00
023e428b9c Index sequence assembly and history lookups 2026-10-17 04:52:20 +00:00
527a38daf1 Preserve defaults-only input absence 2026-10-17 04:49:33 +00:00
30bcd8eeb6 Protect the main version inside item scopes 2026-10-17 04:48:25 +00:00
884215c403 Honor parsed sequence indexes in version protections 2026-10-17 04:46:53 +00:00
8a89806e44 Explain descendants removed by sequence replacement 2026-10-17 04:45:49 +00:00
26720b98f0 Preserve item helper history in source order 2026-10-17 04:44:29 +00:00
4633a1b8a2 Project consumed version evidence through sequence appends 2026-10-17 04:42:23 +00:00
87986a12d2 Record sequence composition review findings 2026-10-17 01:50:02 +00:00
0858af1029 Report malformed sequence items at assigned paths 2026-10-16 19:41:48 +00:00
acddce8e2f Document sequence composition for consumers 2026-10-16 19:37:59 +00:00
77c5d9482a Close cross-cutting sequence regression gaps 2026-10-16 19:32:31 +00:00
adef16ccdd Expose sequence placement controls in the public API 2026-10-16 19:23:43 +00:00
c19825620c Project version protections through item placement 2026-10-16 19:11:08 +00:00
190431740a Apply sequence policy across loading tiers 2026-10-16 18:59:52 +00:00
25cf6d32b6 Assemble imported sequence-item scopes 2026-10-16 16:20:14 +00:00
1a04bb3835 Add private sequence-item placement 2026-10-16 15:55:29 +00:00
1cf3b0d78f Retain ordered acquisition scopes 2026-10-16 15:48:36 +00:00
66dced0823 Project history and sensitivity paths 2026-10-16 15:44:44 +00:00
b1b5163cc3 Preserve append history across tiers 2026-10-16 15:36:46 +00:00
bb9bde799a Project sequence origins onto appended indexes 2026-10-16 15:29:27 +00:00
dc7a70811a Compose registered sequences by identity 2026-10-16 15:25:06 +00:00
ff212885c4 Prepare sequence registration preflight 2026-10-16 15:19:32 +00:00
90c2d7e409 Clarify unresolved source destination evidence in implementation plan 2026-10-16 14:49:23 +00:00
60e6e2697f Plan sequence placement and composition 2026-10-16 14:36:46 +00:00
20511773fc Settle sequence placement and composition roadmap decisions 2026-10-16 14:22:59 +00:00
31fb6a9bd1 Extend sequence roadmap with item placement and open decisions 2026-10-16 14:18:36 +00:00
103e10639a Plan identity-based sequence composition 2026-10-16 14:05:43 +00:00
97c5a0390a Retire deferred scope and preserve future performance experiments 2026-10-16 13:53:34 +00:00
8b547765f0 Record selective source version requirements as future work 2026-10-16 13:42:37 +00:00
5215cf5c4f Catalog identified record sequences as future work 2026-10-16 12:07:49 +00:00
e66c37161c Retire completed roadmaps before the next release 2026-10-16 11:36:21 +00:00
e4270c9b14 Allow absent version headers below structural controls 2026-10-15 22:22:56 +00:00
e69ad0f32d Keep complete paths in missing version evidence 2026-10-15 22:20:20 +00:00
0a2246bdbe Check import opt-in before processing source headers 2026-10-15 22:18:57 +00:00
e5a6df7af7 Preserve header sensitivity across colliding path shapes 2026-10-15 22:17:49 +00:00
25cb3c47e7 Record source version review findings 2026-10-15 22:11:44 +00:00
020d72084d Document shared source version checks 2026-10-15 16:23:39 +00:00
358b28c421 Add source version adoption example 2026-10-15 16:19:24 +00:00
b0550af6a3 Audit source version lifecycle agreement 2026-10-15 16:17:21 +00:00
43c994a32a Publish detached sanitized source evidence 2026-10-15 16:12:21 +00:00
1e47ee21cd Propagate source evidence sensitivity through history 2026-10-15 16:00:01 +00:00
fed591183b Classify source version evidence at original paths 2026-10-15 15:55:16 +00:00
1f95cf905a Expose source version modes in load options 2026-10-15 15:48:59 +00:00
15f5b591b9 Check source versions in selected overlays 2026-10-15 15:44:04 +00:00
184f29c7af Check source versions in direct underlays 2026-10-15 15:40:11 +00:00
813d6a38c9 Validate source versions in recursive imports 2026-10-15 15:33:40 +00:00
23ae700194 Integrate source version checks for main inputs 2026-10-15 15:24:53 +00:00
49218a836b Add source-local version extraction 2026-10-15 15:15:05 +00:00
2fcf892324 Separate version interpretation from reporting 2026-10-15 15:04:57 +00:00
8645cab79a Record shared source version policy 2026-10-15 15:02:50 +00:00
70c95bf660 Plan shared source version checks 2026-10-15 14:24:51 +00:00
d9f08ff719 Settle source version edge-case decisions 2026-10-15 14:05:25 +00:00
2c91e41d10 Record source version decisions and remaining edge cases 2026-10-14 20:30:58 +00:00
e3860036f8 Plan shared source version checks 2026-10-14 19:03:25 +00:00
b3fc6a5950 Restore the original input destination decision 2026-10-14 18:05:48 +00:00
a858a066e4 Record relative import destination review findings 2026-10-14 17:52:14 +00:00
10edd95d4d Complete relative import destination documentation audit 2026-10-14 17:45:31 +00:00
08e03f6a38 Demonstrate entry-declared import destinations 2026-10-14 17:41:35 +00:00
a20ba0af27 Verify relative imports through the application lifecycle 2026-10-14 17:39:28 +00:00
6458dbd3aa Preserve import provenance and resource contexts 2026-10-14 17:34:52 +00:00
43e55dfa85 Verify accumulated import resource accounting 2026-10-14 17:29:54 +00:00
430985c629 Verify relative import composition and version ownership 2026-10-14 17:27:49 +00:00
4635ef08d5 Add relative destinations to import declarations 2026-10-14 17:24:57 +00:00
49c0c6d908 Pass effective destinations through import traversal 2026-10-14 17:21:07 +00:00
a8beb3e620 Record relative import destination decision and baseline 2026-10-14 17:17:50 +00:00
934c1022e0 Record PromptKit feature request decisions 2026-10-14 17:09:23 +00:00
7248a8bd95 Plan relative import destination implementation 2026-10-14 16:53:50 +00:00
b2db3233b2 Settle relative import destination syntax 2026-10-14 16:44:11 +00:00
edb6d05ec1 Plan relative import destinations 2026-10-14 16:39:10 +00:00
315f464dfc Correct mounted mapping diagnostic paths 2026-10-14 15:49:51 +00:00
a029a2a754 Record input destination review findings 2026-10-14 14:12:18 +00:00
945e111c4e Complete input destination implementation audit 2026-10-14 14:06:38 +00:00
2d8df22dd1 Add mounted collection consumer examples 2026-10-14 13:59:37 +00:00
53601e68ed Expose input destinations through the public facade 2026-10-14 13:56:22 +00:00
751787ec24 Verify mounted inheritance and sensitivity integration 2026-10-14 13:50:08 +00:00
f50122d9f8 Verify mounted provenance and resource contexts 2026-10-14 13:38:56 +00:00
c71faf2065 Count destination wrappers in shared input limits 2026-10-14 13:31:32 +00:00
2f5d57ce1d Verify mounted diagnostic paths and locations 2026-10-14 13:25:55 +00:00
a1c557603f Verify mounted composition and version ownership 2026-10-14 13:24:06 +00:00
a2fab66aba Carry destinations through imported configuration 2026-10-14 13:19:31 +00:00
2f1f7f4b8a Integrate private destinations for layered inputs 2026-10-14 13:14:06 +00:00
667a536ae7 Add private input destination placement helpers 2026-10-14 13:09:06 +00:00
ea55ab43ce Record input destination decision and baseline 2026-10-14 13:05:09 +00:00
d3d468a8b4 Plan configuration input destination implementation 2026-10-14 12:48:08 +00:00
6353598bd6 Settle input destination roadmap decisions 2026-10-14 12:40:06 +00:00
3c2d7facb3 Plan configuration input destinations 2026-10-14 03:27:34 +00:00
6fffde4af8 Prepare ConfigKit v0.7.1 release 2026-10-11 01:14:23 +00:00
9e04a397c4 Upgrade ReleaseKit to v0.4.1 2026-10-11 01:10:26 +00:00
c54a9bf51e Upgrade ReleaseKit to v0.4.0 2026-10-10 23:51:44 +00:00
8f3396d6f0 Retire completed diff plans for release preparation 2026-10-09 14:23:20 +00:00
1c2610c2c0 Prepare release notes for v0.7.0 2026-10-09 14:21:37 +00:00
92025d65cb Correct diff masking for historical descendants 2026-10-09 13:22:40 +00:00
a4b5a6840a Record effective diff review findings 2026-10-09 12:57:40 +00:00
2ebdfb539b Document and audit effective configuration diffs 2026-10-09 12:42:17 +00:00
a37e6895e1 Add effective diff consumer examples 2026-10-09 12:37:18 +00:00
8ae542b3a3 Expose effective diffs through the root facade 2026-10-09 12:34:11 +00:00
174efb65f6 Assemble the safe engine diff operation 2026-10-09 12:30:20 +00:00
756149beaa Track hidden changes behind redaction 2026-10-09 12:24:01 +00:00
e5dddef204 Mask sensitive structural replacements 2026-10-09 12:22:36 +00:00
ade5d6a273 Prepare paired safe snapshots for diffs 2026-10-09 12:20:15 +00:00
c20ea4b916 Index combined diff sensitivity 2026-10-09 12:16:13 +00:00
2e17e365a1 Prepare canonical configuration diff regions 2026-10-09 12:14:47 +00:00
fcf914a2ab Record effective diff baseline evidence 2026-10-09 12:12:12 +00:00
3697a51124 Accept the effective configuration diff decision 2026-10-09 12:10:56 +00:00
9194c5b52a Plan effective configuration diff implementation 2026-10-09 05:09:25 +00:00
e82b1c9054 Finalize the effective configuration diff roadmap 2026-10-09 05:02:49 +00:00
6f59cdd237 Plan structured effective configuration diffs 2026-10-09 04:55:29 +00:00
1a885934c2 Prepare release notes for v0.6.0 2026-10-09 04:29:32 +00:00
5917cebf50 Retire completed roadmaps for release preparation 2026-10-09 04:28:16 +00:00
8e91a2b16b Upgrade ReleaseKit to v0.3.0 2026-10-09 04:26:30 +00:00
08402d098b Adopt updated release toolchain policy 2026-10-09 04:23:15 +00:00
53b79528d9 Keep reference algorithms in tests 2026-10-09 04:14:45 +00:00
f849e06b77 Correct the unindexed sensitivity fixture 2026-10-09 04:11:50 +00:00
b37948cfea Reuse parsed sensitivity paths 2026-10-09 04:10:41 +00:00
9323f89ad1 Record performance implementation review findings 2026-10-09 04:07:46 +00:00
6c62226838 Record performance audit completion 2026-10-09 04:01:14 +00:00
799ebab5a6 Index resolution and origin membership 2026-10-09 03:59:16 +00:00
c8546ae599 Index inherited step membership 2026-10-09 03:56:29 +00:00
d32c94eca3 Reuse frozen inspection mapping lookups 2026-10-09 03:54:14 +00:00
344f4f73d7 Index inheritance history lookups 2026-10-09 03:52:07 +00:00
5fedc11c8f Prepare coalesced resolution replay 2026-10-09 03:49:43 +00:00
5b8b7a2415 Index CLI override validation 2026-10-09 03:45:43 +00:00
e591e97f22 Integrate indexed sensitivity propagation 2026-10-09 03:42:29 +00:00
71e142afb4 Add indexed secret dependency closure 2026-10-09 03:41:27 +00:00
ca1ba04676 Collect sensitivity directly from document trees 2026-10-09 03:39:12 +00:00
528495c75a Index resolution metadata updates 2026-10-09 03:36:29 +00:00
aa3b4a5881 Index resolution coverage and document reads 2026-10-09 03:32:30 +00:00
db2d8ed217 Add private canonical path prefix index 2026-10-09 03:30:01 +00:00
d34a98d34b Precompute canonical paths before sorting 2026-10-09 03:27:42 +00:00
9ac65ccc08 Compare canonical paths by token spelling 2026-10-09 03:23:08 +00:00
edc6660654 Detect inheritance directives without paths 2026-10-09 03:20:24 +00:00
a2084df11a Make explicit metadata queries lightweight 2026-10-09 03:18:50 +00:00
b59f002314 Record performance audit baseline evidence 2026-10-09 03:15:39 +00:00
52793301ba Plan the performance improvements 2026-10-09 03:13:22 +00:00
0c77deed05 Record performance audit and optimization candidates 2026-10-09 02:09:25 +00:00
48619de024 Verify startup loading improvements 2026-10-09 01:19:13 +00:00
c546124029 Combine final metadata traversal 2026-10-09 00:52:55 +00:00
673faea097 Reduce path formatting allocations 2026-10-09 00:34:21 +00:00
91ae4fa7ef Measure startup configuration loading 2026-10-09 00:14:19 +00:00
f1722274c0 Add an ordered roadmap for startup loading performance 2026-10-08 23:58:35 +00:00
d53021ea2c Request more efficient configuration loading for PromptKit 2026-10-08 23:50:02 +00:00
a22aaa5dfa Mark ReleaseKit adoption complete 2026-10-08 17:08:53 +00:00
b1ecd9a078 Clarify release command exit statuses 2026-10-08 17:08:35 +00:00
84b59c690d Correct release recovery endpoint checks 2026-10-08 17:08:00 +00:00
ac90b9835b Record ReleaseKit adoption review findings 2026-10-08 17:01:58 +00:00
f887fac809 Complete ReleaseKit endpoint and recovery validation 2026-10-08 16:55:10 +00:00
c03457c814 Verify ReleaseKit against a disposable remote 2026-10-08 16:51:09 +00:00
e9b6b63e5e Retire duplicated release scripts and tests 2026-10-08 16:46:28 +00:00
1dc50d8cdf Replace the release procedure with ReleaseKit 2026-10-08 16:42:56 +00:00
0e92085002 Align release policy with ReleaseKit 2026-10-08 16:37:10 +00:00
f96f2b8295 Add the ReleaseKit source profile 2026-10-08 16:35:58 +00:00
1917dcf921 Declare ReleaseKit as a Go tool dependency 2026-10-08 16:34:58 +00:00
0ac4731903 Separate Go minimum from release toolchain 2026-10-08 16:23:25 +00:00
c42930c6d5 Plan ReleaseKit migration stages 2026-10-08 13:48:36 +00:00
2d3ea588f1 Resolve ReleaseKit adoption decisions 2026-10-08 13:40:50 +00:00
5aafd943b4 Plan Go toolchain updates for releases 2026-10-08 13:33:43 +00:00
c0605fab44 Plan ReleaseKit adoption 2026-10-08 13:12:18 +00:00
dc09959cca Prepare ConfigKit v0.5.0 release 2026-09-30 00:18:40 +00:00
83c59ea7d3 Fix nested inheritance history and budget accounting 2026-09-30 00:04:02 +00:00
b45f7a0410 Record sibling inheritance review findings 2026-09-30 00:00:09 +00:00
2ca1f80094 Document and benchmark sibling inheritance 2026-09-29 23:13:50 +00:00
df9a02539c Activate sibling inheritance in configuration loading 2026-09-29 23:04:13 +00:00
501d59f21b Preserve secret sensitivity across sibling inheritance 2026-09-29 22:56:10 +00:00
f916e7d1f9 Preserve sibling inheritance in configuration history and explanations 2026-09-29 22:52:51 +00:00
462e57df3e Preserve inherited source origins and transfer evidence 2026-09-29 22:40:49 +00:00
4207110e2f Resolve nested inheritance in destination scopes 2026-09-29 22:31:07 +00:00
fdbc306cfb Resolve sibling inheritance with memoized parent templates 2026-09-29 22:23:48 +00:00
78d4b59bbe Share recursive mapping merge across configuration assembly 2026-09-29 22:20:10 +00:00
f564fef84c Plan sibling inheritance implementation 2026-09-29 21:37:38 +00:00
01e38b5840 Record agreed inheritance reference and presence rules 2026-09-29 21:18:48 +00:00
ac1eec2e30 Revise inheritance roadmap around built-in sibling references 2026-09-29 21:10:04 +00:00
0a4b774bfe Retire completed plans and outline named inheritance 2026-09-29 20:11:42 +00:00
b604aa167b Fix underlay provenance and resource path handling 2026-09-29 20:03:39 +00:00
15f5083282 Record underlay implementation review findings 2026-09-29 19:16:18 +00:00
cbf031aedc Document and demonstrate native and layered configuration loading 2026-09-29 18:22:45 +00:00
34e18c84c1 Validate source equivalence and layered scaling 2026-09-29 18:15:10 +00:00
36362ee6af Provide passive per-source resource contexts 2026-09-29 18:08:27 +00:00
5a77728a81 Expose layered configuration loading and inspection 2026-09-29 17:58:15 +00:00
1cdf90d831 Compose grouped underlays in shared load lifecycle 2026-09-29 17:52:20 +00:00
90c6e8ed7e Add snapshotted mapping configuration sources 2026-09-29 17:43:19 +00:00
1ff3c812a7 Expose native filesystem sources through public loading API 2026-09-29 17:36:20 +00:00
6111cb23a1 Unify host and native filesystem acquisition 2026-09-29 17:29:01 +00:00
de28718941 Reuse owned assembly across configuration layers 2026-09-29 17:13:14 +00:00
8d9a3a8dfa Index mapping assembly for composition and overlays 2026-09-29 17:03:02 +00:00
27df838513 Plan filesystem sources and underlays 2026-09-29 16:50:11 +00:00
36172ed20e Clarify ConfigKit design goals and priorities 2026-09-29 16:10:16 +00:00
e1ce24aabb Focus the source roadmap on recursive underlays 2026-09-29 16:06:31 +00:00
e4bbedc695 Preserve version semantics in the layering roadmap 2026-09-29 15:52:20 +00:00
798091b276 Align layering roadmap with consistent version semantics 2026-09-29 15:45:21 +00:00
5c1aa1814c Document filesystem sources and layered loading roadmap 2026-09-29 15:25:23 +00:00
ec26591b27 Request named configuration inheritance for PromptKit 2026-09-29 15:07:21 +00:00
99338baabb Record PromptKit filesystem and layered loading requests 2026-09-29 14:54:44 +00:00
67c0369018 Prepare ConfigKit v0.4.2 release 2026-09-28 01:44:54 +00:00
a36ed3deba Fix release destination handling and verification example 2026-09-28 01:42:06 +00:00
d750e7325c Record release workflow review findings 2026-09-28 01:31:38 +00:00
3d0093e1fd Align release policy and operational documentation 2026-09-28 01:13:09 +00:00
299ebc21a9 Add guarded single-command release publication 2026-09-28 01:07:30 +00:00
ad2f07db5f Simplify release validation gates 2026-09-28 00:57:25 +00:00
d6fe498652 Plan the single-command release workflow 2026-09-28 00:52:16 +00:00
faf5ff8fa1 Correct policy references and restore upstream wording 2026-09-28 00:45:13 +00:00
897203dd8f Align release policy and complete policy adoption 2026-09-28 00:32:01 +00:00
b5a919b8c5 Align agent architecture and testing policies 2026-09-28 00:28:55 +00:00
5f4a8f1919 Align policy provenance and licensing documentation 2026-09-28 00:27:03 +00:00
12a03dbb9a Plan staged policy documentation alignment 2026-09-28 00:21:11 +00:00
6f372e0dfe Document policy alignment and release workflow roadmaps 2026-09-28 00:12:25 +00:00
0adb94f59e Prepare ConfigKit v0.4.1 2026-09-26 21:22:33 +00:00
ee2bcbeed5 Remove author-owned adaptation notices 2026-09-26 21:19:58 +00:00
3883a5042d Align licensing policy and attribution with the house standard 2026-09-26 21:15:13 +00:00
c034b7e709 Align COPYRIGHT and LICENSE files to the house standard format 2026-09-26 14:56:14 -05:00
a803799502 Align licensing documentation to LGPL-3.0-or-later 2026-09-26 14:06:25 -05:00
01b437e6ed Align licensing documentation to LGPL-3.0-or-later 2026-09-26 14:02:44 -05:00
e6ced0d3d1 Prepare ConfigKit v0.4.0 2026-09-23 22:34:50 +00:00
73ce6e9e31 Add overlay migration regression coverage 2026-09-23 22:31:32 +00:00
ae91f57bbe Rename selected configuration profiles to overlays 2026-09-23 18:27:29 +00:00
66e080ed34 Plan the staged overlay rename 2026-09-23 18:20:25 +00:00
d8d9dbfe7f Plan the breaking rename from profiles to overlays 2026-09-23 18:16:10 +00:00
aead20fae4 Simplify local release validation and publication 2026-09-23 18:10:37 +00:00
c5a2ddee7a Document the simplified release workflow 2026-09-23 18:03:25 +00:00
418fefdeef Align repository policies with current standards 2026-09-23 17:58:49 +00:00
21ae2159f7 Prepare ConfigKit v0.3.1 2026-09-21 13:27:48 +00:00
b9425c9ae2 Ignore hidden entries during secrets bootstrap 2026-09-21 13:26:01 +00:00
fbe7b61c79 Stop tracking codebase memory artifacts 2026-09-12 19:20:25 +00:00
34380f62a5 Prepare ConfigKit v0.3.0 2026-09-12 18:35:51 +00:00
f2c650b5eb Preserve closed callback failures through the facade 2026-09-12 18:33:11 +00:00
5760614834 Make macOS support best effort 2026-09-12 18:30:59 +00:00
88e86de03e Repair development guide formatting 2026-09-12 17:26:12 +00:00
c98435ce8e Restore public API reference documentation 2026-09-12 17:24:44 +00:00
6102b36adf Check resolved types at the public boundary 2026-09-12 17:23:47 +00:00
7395f54513 Preserve missing version checker validation 2026-09-12 17:20:40 +00:00
5dc7e18d83 Audit the public facade boundary 2026-09-12 16:26:44 +00:00
7f46c947dd Move facade to the root package 2026-09-12 16:21:17 +00:00
550f92d7c6 Complete facade inspection and secrets bootstrap 2026-09-12 16:19:22 +00:00
da307c9f49 Add facade lifecycle wrappers 2026-09-12 16:16:11 +00:00
b8935e77bd Separate engine loading from inspection state 2026-09-12 16:12:01 +00:00
05812a6ad3 Add public facade value wrappers 2026-09-12 16:07:38 +00:00
da02a19f2c Bind engine secret handling to facade types 2026-09-12 16:04:31 +00:00
9b4f9ac50e Add internal engine relocation candidate 2026-09-12 15:58:06 +00:00
f81def9365 Record public facade boundary 2026-09-12 15:56:11 +00:00
eb7de8e40a Plan the public facade refactor 2026-09-12 15:16:52 +00:00
28e7fc5f69 Avoid copying unused resolution history 2026-09-12 13:51:04 +00:00
715364b079 Complete resolution provenance readiness audit 2026-09-12 13:25:38 +00:00
b9e4dfac92 Demonstrate source-aware path normalization 2026-09-12 13:21:21 +00:00
cc98be8f0a Verify layered resolution provenance 2026-09-12 13:17:22 +00:00
e20a0a0998 Expose resolution input provenance 2026-09-12 13:13:07 +00:00
88f21d09c7 Prepare resolution entry provenance view 2026-09-12 13:10:48 +00:00
818d36f836 Clarify implementation discipline and provenance feature scope 2026-09-12 13:05:00 +00:00
381430a24b Plan source origins during resolution 2026-09-12 12:52:32 +00:00
8a62b42503 Prepare ConfigKit v0.2.0 2026-09-12 01:33:17 +00:00
244104f84f Unify canonical sequence index semantics for version decoding 2026-09-12 01:31:54 +00:00
fd8a68d268 Complete integer schema version consumer readiness 2026-09-12 00:33:49 +00:00
8702121fa4 Verify integer version provenance and protections 2026-09-12 00:30:32 +00:00
6eb40c5492 Cover integer version decoding boundaries 2026-09-12 00:28:35 +00:00
f5bbc2be5d Support integer schema versions through typed decoding 2026-09-12 00:25:54 +00:00
b3e7b62119 Prepare ConfigKit v0.1.2 2026-09-07 13:02:22 +00:00
cd730d5c99 Prepare ConfigKit v0.1.1 2026-09-07 03:06:14 +00:00
387 changed files with 57597 additions and 6845 deletions

4
.gitignore vendored
View File

@@ -25,6 +25,9 @@ go.work.sum
# env file
.env
# Local codebase-memory tooling artifacts
/.codebase-memory/
# ---> Linux
*~
@@ -81,4 +84,3 @@ Temporary Items
.history/
# Built Visual Studio Code Extensions
*.vsix

View File

@@ -1,2 +1,25 @@
Please review `docs/development.md` for initial orientation in this repository
and follow its task-specific reading guide.
<!-- codebase-memory-mcp:start -->
# Codebase Knowledge Graph (codebase-memory-mcp)
This project uses codebase-memory-mcp to maintain a knowledge graph of the codebase.
ALWAYS prefer MCP graph tools over grep/glob/file-search for code discovery.
## Priority Order
1. `search_graph` — find functions, classes, routes, variables by pattern
2. `trace_path` — trace who calls a function or what it calls
3. `get_code_snippet` — read specific function/class source code
4. `query_graph` — run Cypher queries for complex patterns
5. `get_architecture` — high-level project summary
## When to fall back to grep/glob
- Searching for string literals, error messages, config values
- Searching non-code files (Dockerfiles, shell scripts, configs)
- When MCP tools return insufficient results
<!-- codebase-memory-mcp:end -->
Start with `docs/development.md` for orientation and use its policy reading guide
to select the policies applicable to the task. Do not read the entire suite by
default.
Read and follow `docs/policy/agents.md` before recommending, planning, or
implementing repository changes.

25
COPYRIGHT Normal file
View File

@@ -0,0 +1,25 @@
ConfigKit
Copyright (C) 2026 Eric Rakestraw
SPDX-License-Identifier: LGPL-3.0-or-later
This library is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
The GNU Lesser General Public License version 3 and its incorporated
GNU General Public License version 3 text are provided in LICENSE.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
Third-party code and assets are subject to their respective license terms.
Policy documents adapted from Standard Repository Policies, the root AGENTS.md
orientation, and the Policy Reading Guide in docs/development.md remain licensed
under CC-BY-SA-4.0. See docs/oss/attribution.md for the affected material,
upstream credit, and license links.

1118
LICENSE

File diff suppressed because it is too large Load Diff

102
README.md
View File

@@ -2,18 +2,53 @@
An opinionated Go library for applications that use YAML configuration files.
ConfigKit's initial loader accepts one strict YAML
source (or explicit defaults-only input), optionally traverses explicit path and glob
imports, strictly composes their disjoint definitions, applies typed defaults, and
can overlay one selected fixed profile and check an application schema version. It
returns detached metadata and safe diagnostics.
Linux is release-validated; macOS support is best-effort. See the
[platform support policy](docs/policy/release.md#platform-support) for details.
ConfigKit's simple `Load` accepts one strict YAML source (or explicit
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
source groups with an independently selected main input. Sources can be host
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
Enabled 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,
then report domain errors and warnings through read-only semantic validation. Secret
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
references resolve environment names before validation, and explicit
secrets-directory bootstrap is available as a separate startup operation. Explicit
CLI override records apply above profiles. Value explanation is available through
detached structured records and deterministic redacted output.
CLI override records apply after inheritance and typed decoding, before application
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
type Config struct {
@@ -30,10 +65,49 @@ func ReadConfig(path string) (Config, error) {
}
```
The [configuration contract](docs/config.md) describes implemented behavior. The
[consumer adoption guide](docs/consumers/README.md) provides maintained Narratio-,
Notarius-, WeatherReporter-, and Seriatim-shaped examples and migration guidance.
The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions. See the
[development guide](docs/development.md) for repository orientation and the
[release procedure](docs/release.md) for maintainer-only candidate and publication
steps.
## Design philosophy
ConfigKit favors one coherent, predictable configuration model over flexibility
for reproducing each consumer's conventions. Its goal is to let applications
load once, inspect where final values came from, and distinguish explicit values
from omissions with minimal configuration bookkeeping. Applications retain their
domain rules and execution responsibilities.
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).
[Open Source attribution notices](docs/oss/attribution.md) are available.

View File

@@ -1,190 +0,0 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// composedDocument is the private handoff between source assembly and later
// precedence layers. Claims remain ordered by source and declaration traversal;
// origins retain every source that contributes to a composite mapping.
type composedDocument struct {
root *document.Node
claims []compositionClaim
origins map[string]Origin
overlays []overlayRecord
applications []historyApplication
inputBytes int64
inputNodes int
}
// cloneComposedDocument detaches mutable composition and provenance state;
// history application trees are immutable and retain their existing sharing.
func cloneComposedDocument(base *composedDocument) *composedDocument {
return &composedDocument{
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
applications: cloneHistoryApplications(base.applications),
inputBytes: base.inputBytes, inputNodes: base.inputNodes,
}
}
type compositionClaim struct {
path Path
kind document.Kind
location Location
}
type compositionClaims struct {
ordered []compositionClaim
byPath map[string][]compositionClaim
}
// composeDocuments recursively joins disjoint mapping entries. Source order
// controls only output traversal and diagnostics: every non-mapping overlap is
// rejected before a result is assembled.
func composeDocuments(inputs ...*document.Node) (*composedDocument, []Diagnostic) {
if len(inputs) == 0 {
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires at least one document", nil)}
}
collected := compositionClaims{byPath: make(map[string][]compositionClaim)}
for _, input := range inputs {
if input == nil || input.Kind != document.Mapping {
var location *Location
if input != nil {
converted := publicLocation(input.Location)
location = &converted
}
return nil, []Diagnostic{compositionInvalidDiagnostic("strict composition requires mapping document roots", location)}
}
collected.append(input, Path{})
}
for _, claim := range collected.ordered {
claims := collected.byPath[claim.path.String()]
if len(claims) < 2 || allMappingClaims(claims) {
continue
}
return nil, []Diagnostic{compositionConflictDiagnostic(claim.path, claims)}
}
root := document.Clone(inputs[0])
for _, input := range inputs[1:] {
mergeDisjointMappings(root, input)
}
applications := make([]historyApplication, 0, len(inputs))
for _, input := range inputs {
applications = append(applications, historyApplication{layer: LayerBase, value: input})
}
return &composedDocument{
root: root,
claims: cloneCompositionClaims(collected.ordered),
origins: compositionOrigins(collected.ordered),
applications: applications,
}, nil
}
func (c *compositionClaims) append(node *document.Node, path Path) {
claim := compositionClaim{
path: path.clone(),
kind: node.Kind,
location: publicLocation(node.Location),
}
c.ordered = append(c.ordered, claim)
pointer := path.String()
c.byPath[pointer] = append(c.byPath[pointer], claim)
if node.Kind != document.Mapping {
return
}
for _, entry := range node.Mapping {
c.append(entry.Value, path.Key(entry.Key))
}
}
func allMappingClaims(claims []compositionClaim) bool {
for _, claim := range claims {
if claim.kind != document.Mapping {
return false
}
}
return true
}
func mergeDisjointMappings(target, incoming *document.Node) {
for _, incomingEntry := range incoming.Mapping {
index := documentEntryIndex(target.Mapping, incomingEntry.Key)
if index < 0 {
cloned := incomingEntry
cloned.Value = document.Clone(incomingEntry.Value)
target.Mapping = append(target.Mapping, cloned)
continue
}
// Claim validation guarantees that the only remaining overlap is a
// mapping pair. Empty mappings naturally contribute no child entries.
mergeDisjointMappings(target.Mapping[index].Value, incomingEntry.Value)
}
}
func documentEntryIndex(entries []document.Entry, key string) int {
for index := range entries {
if entries[index].Key == key {
return index
}
}
return -1
}
func compositionOrigins(claims []compositionClaim) map[string]Origin {
origins := make(map[string]Origin, len(claims))
for _, claim := range claims {
pointer := claim.path.String()
origin := origins[pointer]
origin.Layer = LayerBase
origin.Locations = append(origin.Locations, claim.location)
origins[pointer] = origin
}
return origins
}
func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) {
for pointer, origin := range origins {
if _, present := decoded.presence[pointer]; present {
decoded.origins[pointer] = cloneOrigin(origin)
}
}
}
func cloneCompositionClaims(claims []compositionClaim) []compositionClaim {
cloned := make([]compositionClaim, len(claims))
for index, claim := range claims {
cloned[index] = claim
cloned[index].path = claim.path.clone()
}
return cloned
}
func compositionInvalidDiagnostic(message string, location *Location) Diagnostic {
return Diagnostic{
Code: CodeComposeInvalid, Severity: SeverityError, Message: message,
Primary: cloneLocation(location),
}
}
func compositionConflictDiagnostic(path Path, claims []compositionClaim) Diagnostic {
primary := claims[0].location
related := make([]Location, 0, len(claims)-1)
for _, claim := range claims[1:] {
related = append(related, claim.location)
}
return Diagnostic{
Code: CodeComposeConflict,
Severity: SeverityError,
Path: path.clone(),
Message: "configuration path is defined by multiple sources",
Primary: &primary,
Related: related,
}
}
func publicLocation(location document.Location) Location {
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}
}

View File

@@ -1,194 +0,0 @@
package configkit
import (
"reflect"
"testing"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
func TestComposeDocumentsJoinsDisjointMappingsAndTracksClaims(t *testing.T) {
base := parseCompositionDocument(t, "source-1", `services:
api:
enabled: true
empty: {}
`)
fragment := parseCompositionDocument(t, "source-2", `services:
worker:
enabled: false
values: [one, two]
`)
baseBefore := document.Clone(base)
fragmentBefore := document.Clone(fragment)
composed, diagnostics := composeDocuments(base, fragment)
if len(diagnostics) != 0 {
t.Fatalf("composeDocuments() diagnostics = %#v", diagnostics)
}
for _, path := range []Path{
Keys("services", "api", "enabled"),
Keys("services", "worker", "enabled"),
Keys("empty"),
Keys("values"),
} {
if _, ok := documentAtPath(composed.root, path); !ok {
t.Errorf("composed document is missing %s", path.String())
}
}
serviceOrigin := composed.origins[Keys("services").String()]
wantLocations := []Location{
{SourceID: "source-1", Line: 2, Column: 3},
{SourceID: "source-2", Line: 2, Column: 3},
}
if !reflect.DeepEqual(serviceOrigin, Origin{Layer: LayerBase, Locations: wantLocations}) {
t.Fatalf("services origin = %#v, want composite origin", serviceOrigin)
}
wantClaimPaths := []string{"", "/services", "/services/api", "/services/api/enabled", "/empty", "", "/services", "/services/worker", "/services/worker/enabled", "/values"}
gotClaimPaths := make([]string, len(composed.claims))
for index, claim := range composed.claims {
gotClaimPaths[index] = claim.path.String()
}
if !reflect.DeepEqual(gotClaimPaths, wantClaimPaths) {
t.Fatalf("claim paths = %#v, want %#v", gotClaimPaths, wantClaimPaths)
}
reversed, reversedDiagnostics := composeDocuments(fragment, base)
if len(reversedDiagnostics) != 0 {
t.Fatalf("reversed composeDocuments() diagnostics = %#v", reversedDiagnostics)
}
if deltas := compareConfigurationValues(composed.root, reversed.root); len(deltas) != 0 {
t.Fatalf("input order changed effective values: %#v", deltas)
}
if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(fragment, fragmentBefore) {
t.Fatal("composition mutated an input document")
}
composed.root.Mapping[0].Value.Mapping[0].Value.Mapping[0].Value.Text = "changed"
if !reflect.DeepEqual(base, baseBefore) || !reflect.DeepEqual(fragment, fragmentBefore) {
t.Fatal("composed document aliases an input document")
}
}
func TestComposeDocumentsRejectsEveryPresentNonMappingOverlap(t *testing.T) {
tests := []struct {
name string
left string
right string
path string
}{
{name: "equal scalar", left: "value: true\n", right: "value: true\n", path: "/value"},
{name: "different scalar", left: "value: true\n", right: "value: false\n", path: "/value"},
{name: "null", left: "value: null\n", right: "value: null\n", path: "/value"},
{name: "sequence", left: "value: [one]\n", right: "value: [two]\n", path: "/value"},
{name: "mapping and scalar", left: "value:\n nested: true\n", right: "value: text\n", path: "/value"},
{name: "scalar and mapping", left: "value: text\n", right: "value:\n nested: true\n", path: "/value"},
{name: "nested keyed entry", left: "items:\n shared:\n left: 1\n", right: "items:\n shared:\n left: 2\n", path: "/items/shared/left"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
left := parseCompositionDocument(t, "source-1", test.left)
right := parseCompositionDocument(t, "source-2", test.right)
_, diagnostics := composeDocuments(left, right)
assertCompositionConflict(t, diagnostics, test.path, []string{"source-1", "source-2"})
})
}
}
func TestComposeDocumentsReportsAllClaimantsAtExactLocations(t *testing.T) {
first := parseCompositionDocument(t, "source-1", "group:\n value: 1\n")
second := parseCompositionDocument(t, "source-2", "group:\n value: 2\n")
third := parseCompositionDocument(t, "source-3", "group:\n value: 3\n")
_, diagnostics := composeDocuments(first, second, third)
assertCompositionConflict(t, diagnostics, "/group/value", []string{"source-1", "source-2", "source-3"})
locations := append([]Location{*diagnostics[0].Primary}, diagnostics[0].Related...)
for _, location := range locations {
if location.Line != 2 || location.Column != 10 {
t.Fatalf("claim location = %#v, want line 2 column 10", location)
}
}
_, diagnostics = composeDocuments(
parseCompositionDocument(t, "source-1", "group:\n left: 1\n"),
parseCompositionDocument(t, "source-2", "group:\n right: 2\n"),
parseCompositionDocument(t, "source-3", "group: blocked\n"),
)
assertCompositionConflict(t, diagnostics, "/group", []string{"source-1", "source-2", "source-3"})
}
func TestComposeDocumentsTreatsEmptyMappingsAsChildlessContributions(t *testing.T) {
empty, diagnostics := composeDocuments(
parseCompositionDocument(t, "source-1", "{}\n"),
parseCompositionDocument(t, "source-2", "{}\n"),
)
if len(diagnostics) != 0 || len(empty.root.Mapping) != 0 || len(empty.origins[""].Locations) != 2 {
t.Fatalf("empty root composition = (%#v, %#v)", empty, diagnostics)
}
tests := []struct {
name string
left string
right string
}{
{name: "two empty mappings", left: "value: {}\n", right: "value: {}\n"},
{name: "empty then populated", left: "value: {}\n", right: "value: {nested: true}\n"},
{name: "populated then empty", left: "value: {nested: true}\n", right: "value: {}\n"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
composed, diagnostics := composeDocuments(
parseCompositionDocument(t, "source-1", test.left),
parseCompositionDocument(t, "source-2", test.right),
)
if len(diagnostics) != 0 {
t.Fatalf("composeDocuments() diagnostics = %#v", diagnostics)
}
if origin := composed.origins["/value"]; len(origin.Locations) != 2 {
t.Fatalf("value origin = %#v, want both mapping locations", origin)
}
if test.name != "two empty mappings" {
if _, ok := documentAtPath(composed.root, Keys("value", "nested")); !ok {
t.Fatal("populated mapping contribution was not retained")
}
}
})
}
}
func TestComposeDocumentsRejectsInvalidBoundaries(t *testing.T) {
if composed, diagnostics := composeDocuments(); composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid {
t.Fatalf("empty composition = (%#v, %#v)", composed, diagnostics)
}
nonMapping := &document.Node{Kind: document.String, Location: document.Location{SourceID: "source-1", Line: 4, Column: 2}}
if composed, diagnostics := composeDocuments(nonMapping); composed != nil || len(diagnostics) != 1 || diagnostics[0].Primary == nil || diagnostics[0].Primary.Line != 4 {
t.Fatalf("non-mapping composition = (%#v, %#v)", composed, diagnostics)
}
}
func parseCompositionDocument(t *testing.T, sourceID, input string) *document.Node {
t.Helper()
parsed, diagnostics := parseSource(Bytes(sourceID, []byte(input)), sourceID, defaultParseLimits())
if len(diagnostics) != 0 {
t.Fatalf("parse diagnostics = %#v", diagnostics)
}
return parsed.document
}
func assertCompositionConflict(t *testing.T, diagnostics []Diagnostic, path string, sources []string) {
t.Helper()
if len(diagnostics) != 1 {
t.Fatalf("diagnostics = %#v, want one conflict", diagnostics)
}
diagnostic := diagnostics[0]
if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != path || diagnostic.Primary == nil {
t.Fatalf("diagnostic = %#v, want compose conflict at %s", diagnostic, path)
}
locations := append([]Location{*diagnostic.Primary}, diagnostic.Related...)
gotSources := make([]string, len(locations))
for index, location := range locations {
gotSources[index] = location.SourceID
}
if !reflect.DeepEqual(gotSources, sources) {
t.Fatalf("claim sources = %#v, want %#v", gotSources, sources)
}
}

242
conversion.go Normal file
View File

@@ -0,0 +1,242 @@
package configkit
import "gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
func fromEngineLocation(value engine.Location) Location {
return Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
}
func toEngineLocation(value Location) engine.Location {
return engine.Location{SourceID: value.SourceID, Line: value.Line, Column: value.Column}
}
func fromEngineLocationPointer(value *engine.Location) *Location {
if value == nil {
return nil
}
result := fromEngineLocation(*value)
return &result
}
func toEngineLocationPointer(value *Location) *engine.Location {
if value == nil {
return nil
}
result := toEngineLocation(*value)
return &result
}
func fromEngineLocations(values []engine.Location) []Location {
if values == nil {
return nil
}
result := make([]Location, len(values))
for i := range values {
result[i] = fromEngineLocation(values[i])
}
return result
}
func toEngineLocations(values []Location) []engine.Location {
if values == nil {
return nil
}
result := make([]engine.Location, len(values))
for i := range values {
result[i] = toEngineLocation(values[i])
}
return result
}
func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
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 {
result := engine.SourceInfo{
ID: value.ID, Label: value.Label, Path: value.Path, Destination: toEnginePath(value.Destination),
Layer: engine.Layer(value.Layer), Kind: engine.SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
ImportedFrom: toEngineLocationPointer(value.ImportedFrom), VersionCheck: toEngineSourceVersionCheck(value.VersionCheck),
}
result = engine.SourceInfoWithImportSelections(result, toEngineImportSelections(value.ImportSelections))
return engine.SourceInfoWithPlacement(result, uint8(value.PlacementMode), value.DestinationUnresolved)
}
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
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 toEngineImportSelections(values []ImportSelection) []engine.ImportSelectionForFacade {
if values == nil {
return nil
}
result := make([]engine.ImportSelectionForFacade, len(values))
for index, value := range values {
result[index] = engine.ImportSelectionForFacade{
Path: toEnginePath(value.Path), Location: toEngineLocation(value.Location),
Selector: string(value.Selector), Outcome: string(value.Outcome),
}
}
return result
}
func toEngineImportDefaults(value ImportDefaults) engine.ImportDefaults {
return engine.ImportDefaults{
Required: cloneImportDefaultBool(value.Required),
Follow: cloneImportDefaultBool(value.Follow),
Hidden: cloneImportDefaultBool(value.Hidden),
Recursive: cloneImportDefaultBool(value.Recursive),
Pattern: cloneImportDefaultStrings(value.Pattern),
}
}
func cloneImportDefaultBool(value *bool) *bool {
if value == nil {
return nil
}
cloned := *value
return &cloned
}
func cloneImportDefaultStrings(values []string) []string {
if values == nil {
return nil
}
cloned := make([]string, len(values))
copy(cloned, values)
return cloned
}
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 toEngineSourceVersionCheck(value *SourceVersionCheck) *engine.SourceVersionCheck {
if value == nil {
return nil
}
return &engine.SourceVersionCheck{
Path: toEnginePath(value.Path),
Version: engine.Version{Present: value.Version.Present, Null: value.Version.Null, Text: value.Version.Text},
Location: toEngineLocationPointer(value.Location), Redacted: value.Redacted,
}
}
func fromEngineOrigin(value engine.Origin) Origin {
result := Origin{Layer: Layer(value.Layer), Locations: fromEngineLocations(value.Locations), Operation: ChangeKind(value.Operation)}
if value.Inheritance != nil {
result.Inheritance = make([]InheritanceStep, len(value.Inheritance))
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 toEngineOrigin(value Origin) engine.Origin {
result := engine.Origin{Layer: engine.Layer(value.Layer), Locations: toEngineLocations(value.Locations), Operation: engine.ChangeKind(value.Operation)}
if value.Inheritance != nil {
result.Inheritance = make([]engine.InheritanceStep, len(value.Inheritance))
for index, step := range value.Inheritance {
result.Inheritance[index] = engine.InheritanceStep{
From: toEnginePath(step.From), To: toEnginePath(step.To), Reference: toEnginePath(step.Reference),
Locations: toEngineLocations(step.Locations),
}
}
}
return result
}
func fromEngineDiagnostic(value engine.Diagnostic) Diagnostic {
return Diagnostic{Code: Code(value.Code), Severity: Severity(value.Severity), Path: fromEnginePath(value.Path), Message: value.Message, Primary: fromEngineLocationPointer(value.Primary), Related: fromEngineLocations(value.Related)}
}
func toEngineDiagnostic(value Diagnostic) engine.Diagnostic {
return engine.Diagnostic{Code: engine.Code(value.Code), Severity: engine.Severity(value.Severity), Path: toEnginePath(value.Path), Message: value.Message, Primary: toEngineLocationPointer(value.Primary), Related: toEngineLocations(value.Related)}
}
func toEngineDiagnostics(values []Diagnostic) []engine.Diagnostic {
if values == nil {
return nil
}
result := make([]engine.Diagnostic, len(values))
for i := range values {
result[i] = toEngineDiagnostic(values[i])
}
return result
}
func fromEngineFieldInfo(value engine.FieldInfo) FieldInfo {
return FieldInfo{Explicit: value.Explicit, Null: value.Null, Origin: fromEngineOrigin(value.Origin)}
}
func fromEngineChange(value engine.Change) Change {
result := Change{Path: fromEnginePath(value.Path), Origin: fromEngineOrigin(value.Origin), Reasons: append([]string(nil), value.Reasons...)}
if value.Previous != nil {
result.Previous = make([]Origin, len(value.Previous))
for i := range value.Previous {
result.Previous[i] = fromEngineOrigin(value.Previous[i])
}
}
if value.Dependencies != nil {
result.Dependencies = make([]Path, len(value.Dependencies))
for i := range value.Dependencies {
result.Dependencies[i] = fromEnginePath(value.Dependencies[i])
}
}
return result
}
func fromEngineValueSnapshot(value engine.ValueSnapshot) ValueSnapshot {
return ValueSnapshot{Present: value.Present, Redacted: value.Redacted, YAML: value.YAML}
}
func fromEngineValueChange(value engine.ValueChange) ValueChange {
result := ValueChange{
Before: fromEngineValueSnapshot(value.Before),
After: fromEngineValueSnapshot(value.After),
Origin: fromEngineOrigin(value.Origin),
Reasons: append([]string(nil), value.Reasons...),
}
if value.Dependencies != nil {
result.Dependencies = make([]Path, len(value.Dependencies))
for index := range value.Dependencies {
result.Dependencies[index] = fromEnginePath(value.Dependencies[index])
}
}
return result
}
func fromEngineExplanation(value engine.Explanation) Explanation {
result := Explanation{
Path: fromEnginePath(value.Path),
Effective: fromEngineValueSnapshot(value.Effective),
Origin: fromEngineOrigin(value.Origin),
}
if value.History != nil {
result.History = make([]ValueChange, len(value.History))
for index := range value.History {
result.History[index] = fromEngineValueChange(value.History[index])
}
}
if value.SecretReference != nil {
result.SecretReference = &SecretReferenceInfo{Name: value.SecretReference.Name}
}
return result
}
func fromEngineSecretFileResult(value engine.SecretFileResult) SecretFileResult {
return SecretFileResult{Name: value.Name, Path: value.Path, Status: SecretFileStatus(value.Status), ExistingEmpty: value.ExistingEmpty}
}

View File

@@ -9,8 +9,8 @@ import (
"testing"
)
func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
for _, scriptName := range []string{"release-check.sh", "check.sh"} {
func TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure(t *testing.T) {
for _, scriptName := range []string{"check.sh"} {
t.Run(scriptName, func(t *testing.T) {
script, err := os.ReadFile("scripts/" + scriptName)
if err != nil {
@@ -47,7 +47,7 @@ func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
}
// Isolate expensive toolchain commands: this test owns shell orchestration,
// not Go's compiler or the production tests exercised by the real checker.
write(filepath.Join(bin, "go"), []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$CONFIGKIT_TEST_GO_LOG\"\nif [ \"${CONFIGKIT_TEST_FAIL-}\" = yes ] && [ \"${1-}\" = test ]; then exit 37; fi\n"), 0700)
write(filepath.Join(bin, "go"), []byte("#!/bin/sh\nprintf '%s/%s %s\\n' \"${GOOS-native}\" \"${GOARCH-native}\" \"$*\" >> \"$CONFIGKIT_TEST_GO_LOG\"\nif [ \"${CONFIGKIT_TEST_FAIL-}\" = yes ] && [ \"${1-}\" = test ]; then exit 37; fi\n"), 0700)
write(filepath.Join(bin, "gofmt"), []byte("#!/bin/sh\nprintf '%s\\000' \"$@\" > \"$CONFIGKIT_TEST_FMT_LOG\"\n"), 0700)
init := exec.Command("git", "init", "-q")
init.Dir = root
@@ -66,7 +66,9 @@ func TestCheckScriptsHandleFilenamesAndStopOnFailure(t *testing.T) {
t.Fatal(err)
}
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=test-newer", "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.
cmd.Env = append(cmd.Env, "GOOS=", "GOARCH=")
if name == "failed-go-check" {
cmd.Env = append(cmd.Env, "CONFIGKIT_TEST_FAIL=yes")
}

View File

@@ -1,248 +1,57 @@
package configkit
import (
"cmp"
"fmt"
"io"
"slices"
"strconv"
"strings"
"unicode"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
)
// LoadError reports safe structured diagnostics for a failed configuration load.
// It intentionally does not expose or unwrap parser and application errors.
type LoadError struct {
diagnostics []Diagnostic
sources []SourceInfo
}
type LoadError struct{ value *engine.LoadError }
// Error implements error without rendering diagnostic messages or input values.
func (e *LoadError) Error() string {
if e == nil {
if e == nil || e.value == nil {
return "configkit: configuration load failed"
}
count := 0
for _, diagnostic := range e.diagnostics {
if diagnostic.Severity == SeverityError {
count++
}
}
if count == 1 {
return "configkit: configuration load failed with 1 error"
}
return fmt.Sprintf("configkit: configuration load failed with %d errors", count)
return e.value.Error()
}
// Diagnostics returns a deep copy of the error's canonical diagnostics.
func (e *LoadError) Diagnostics() []Diagnostic {
if e == nil {
if e == nil || e.value == nil {
return nil
}
return cloneDiagnostics(e.diagnostics)
values := e.value.Diagnostics()
if values == nil {
return nil
}
result := make([]Diagnostic, len(values))
for i := range values {
result[i] = fromEngineDiagnostic(values[i])
}
return result
}
// Sources returns a deep copy of sources discovered before loading failed.
func (e *LoadError) Sources() []SourceInfo {
if e == nil {
if e == nil || e.value == nil {
return nil
}
return cloneSourceInfos(e.sources)
values := e.value.Sources()
if values == nil {
return nil
}
result := make([]SourceInfo, len(values))
for i := range values {
result[i] = fromEngineSourceInfo(values[i])
}
return result
}
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
// control characters and propagates writer errors, including short writes.
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {
canonical := canonicalDiagnostics(diagnostics)
for _, diagnostic := range canonical {
if err := writeDiagnostic(output, diagnostic); err != nil {
return err
}
}
return nil
}
type diagnosticAccumulator struct {
diagnostics []Diagnostic
sources []SourceInfo
}
func (a *diagnosticAccumulator) add(diagnostics ...Diagnostic) {
for _, diagnostic := range diagnostics {
a.diagnostics = append(a.diagnostics, cloneDiagnostic(diagnostic))
}
}
func (a *diagnosticAccumulator) loadError() *LoadError {
return &LoadError{
diagnostics: canonicalDiagnostics(a.diagnostics),
sources: cloneSourceInfos(a.sources),
}
}
func canonicalDiagnostics(diagnostics []Diagnostic) []Diagnostic {
canonical := cloneDiagnostics(diagnostics)
for i := range canonical {
slices.SortFunc(canonical[i].Related, compareLocation)
}
slices.SortFunc(canonical, compareDiagnostic)
return canonical
}
func compareDiagnostic(left, right Diagnostic) int {
if order := compareOptionalLocation(left.Primary, right.Primary); order != 0 {
return order
}
if order := cmp.Compare(left.Path.String(), right.Path.String()); order != 0 {
return order
}
if order := cmp.Compare(left.Code, right.Code); order != 0 {
return order
}
if order := cmp.Compare(left.Severity, right.Severity); order != 0 {
return order
}
if order := cmp.Compare(left.Message, right.Message); order != 0 {
return order
}
return compareLocations(left.Related, right.Related)
}
func compareOptionalLocation(left, right *Location) int {
if left == nil && right == nil {
return 0
}
if left == nil {
return -1
}
if right == nil {
return 1
}
return compareLocation(*left, *right)
}
func compareLocations(left, right []Location) int {
for i := 0; i < min(len(left), len(right)); i++ {
if order := compareLocation(left[i], right[i]); order != 0 {
return order
}
}
return cmp.Compare(len(left), len(right))
}
func compareLocation(left, right Location) int {
if order := compareSourceID(left.SourceID, right.SourceID); order != 0 {
return order
}
if order := cmp.Compare(left.Line, right.Line); order != 0 {
return order
}
return cmp.Compare(left.Column, right.Column)
}
func compareSourceID(left, right string) int {
leftOrder, leftOK := sourceSequence(left)
rightOrder, rightOK := sourceSequence(right)
if leftOK && rightOK {
return cmp.Compare(leftOrder, rightOrder)
}
return cmp.Compare(left, right)
}
func sourceSequence(sourceID string) (int, bool) {
const prefix = "source-"
if !strings.HasPrefix(sourceID, prefix) {
return 0, false
}
value, err := strconv.Atoi(strings.TrimPrefix(sourceID, prefix))
if err != nil || value < 1 {
return 0, false
}
return value, true
}
func writeDiagnostic(output io.Writer, diagnostic Diagnostic) error {
severity := diagnostic.Severity
if severity == "" {
severity = SeverityError
}
path := diagnostic.Path.String()
if path == "" {
path = "<root>"
}
var b strings.Builder
b.WriteString(escapeControls(string(severity)))
if diagnostic.Code != "" {
b.WriteString(" [")
b.WriteString(escapeControls(string(diagnostic.Code)))
b.WriteByte(']')
}
b.WriteByte(' ')
b.WriteString(escapeControls(path))
if diagnostic.Primary != nil {
b.WriteString(" (")
writeLocationText(&b, *diagnostic.Primary)
b.WriteByte(')')
}
if diagnostic.Message != "" {
b.WriteString(": ")
b.WriteString(escapeControls(diagnostic.Message))
}
b.WriteByte('\n')
for _, related := range diagnostic.Related {
b.WriteString(" related: ")
writeLocationText(&b, related)
b.WriteByte('\n')
}
return writeString(output, b.String())
}
func writeLocationText(output *strings.Builder, location Location) {
label := location.SourceID
if label == "" {
label = "<synthetic>"
}
output.WriteString(escapeControls(label))
if location.Line > 0 {
output.WriteByte(':')
output.WriteString(strconv.Itoa(location.Line))
if location.Column > 0 {
output.WriteByte(':')
output.WriteString(strconv.Itoa(location.Column))
}
}
}
func writeString(output io.Writer, value string) error {
written, err := io.WriteString(output, value)
if err != nil {
return err
}
if written != len(value) {
return io.ErrShortWrite
}
return nil
}
func escapeControls(value string) string {
var b strings.Builder
for _, r := range value {
if unicode.IsControl(r) {
quoted := strconv.QuoteRune(r)
b.WriteString(quoted[1 : len(quoted)-1])
continue
}
b.WriteRune(r)
}
return b.String()
}
func hasErrors(diagnostics []Diagnostic) bool {
for _, diagnostic := range diagnostics {
if diagnostic.Severity == SeverityError {
return true
}
}
return false
return engine.WriteDiagnostics(output, toEngineDiagnostics(diagnostics))
}

53
diff.go Normal file
View 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
}

View File

@@ -1,227 +0,0 @@
package configkit
import (
"path"
"strings"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type importDeclaration struct {
path string
glob string
optional bool
location Location
}
func splitImportDirectives(root *document.Node) (*document.Node, []importDeclaration, []Diagnostic) {
body := document.Clone(root)
var declarations []importDeclaration
directiveIndex := -1
for index, entry := range body.Mapping {
if entry.Key != "_configkit" {
continue
}
directiveIndex = index
parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit"))
if len(diagnostics) != 0 {
return nil, nil, diagnostics
}
declarations = parsed
break
}
if directiveIndex >= 0 {
body.Mapping = append(body.Mapping[:directiveIndex], body.Mapping[directiveIndex+1:]...)
}
if diagnostics := nestedDirectiveDiagnostics(body, Path{}); len(diagnostics) != 0 {
return nil, nil, diagnostics
}
return body, declarations, nil
}
func parseImportDirective(node *document.Node, directivePath Path) ([]importDeclaration, []Diagnostic) {
if node.Kind != document.Mapping {
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, directivePath, "configuration directive must be a mapping", node)}
}
var declarations []importDeclaration
for _, entry := range node.Mapping {
fieldPath := directivePath.Key(entry.Key)
if entry.Key != "imports" {
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "configuration directive field is not recognized", entry.Value)}
}
if entry.Value.Kind != document.Sequence {
return nil, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "imports must be a sequence", entry.Value)}
}
for index, item := range entry.Value.Sequence {
declaration, diagnostics := parseImportDeclaration(item, fieldPath.Index(index))
if len(diagnostics) != 0 {
return nil, diagnostics
}
declarations = append(declarations, declaration)
}
}
return declarations, nil
}
func parseImportDeclaration(node *document.Node, declarationPath Path) (importDeclaration, []Diagnostic) {
if node.Kind != document.Mapping {
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration must be a mapping", node)}
}
declaration := importDeclaration{location: publicLocation(node.Location)}
seenTarget := false
for _, entry := range node.Mapping {
fieldPath := declarationPath.Key(entry.Key)
switch entry.Key {
case "path", "glob":
if seenTarget || entry.Value.Kind != document.String || entry.Value.Text == "" || strings.TrimSpace(entry.Value.Text) != entry.Value.Text {
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration requires exactly one nonempty path or glob string", entry.Value)}
}
seenTarget = true
declaration.location = publicLocation(entry.Value.Location)
if entry.Key == "path" {
declaration.path = entry.Value.Text
} else {
declaration.glob = entry.Value.Text
}
case "optional":
if entry.Value.Kind != document.Boolean {
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "optional must be a boolean", entry.Value)}
}
declaration.optional = entry.Value.Text == "true"
default:
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, fieldPath, "import declaration field is not recognized", entry.Value)}
}
}
if !seenTarget {
return importDeclaration{}, []Diagnostic{nodeImportDiagnostic(CodeImportInvalid, declarationPath, "import declaration requires exactly one path or glob", node)}
}
return declaration, nil
}
func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := current.Key(entry.Key)
if entry.Key == "_configkit" {
location := publicLocation(entry.KeyLocation)
return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}}
}
if diagnostics := nestedDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
return diagnostics
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if diagnostics := nestedDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
return diagnostics
}
}
}
return nil
}
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
if rule == nil {
return nil
}
current := root
if len(rule.Path.segments) == 0 {
return current
}
for _, segment := range rule.Path.segments {
if current.Kind != document.Mapping {
return current
}
if segment.kind == pathSegmentIndex {
return nil
}
key := segment.key
index := documentEntryIndex(current.Mapping, key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
}
return current
}
func validImportPath(value string) bool {
if value == "" || strings.TrimSpace(value) != value || path.IsAbs(value) || strings.HasPrefix(value, "//") {
return false
}
return !hasImportScheme(value)
}
func hasImportScheme(value string) bool {
colon := strings.IndexByte(value, ':')
slash := strings.IndexByte(value, '/')
if colon <= 0 || (slash >= 0 && slash < colon) {
return false
}
for index := 0; index < colon; index++ {
character := value[index]
if index == 0 {
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') {
return false
}
continue
}
if (character < 'a' || character > 'z') && (character < 'A' || character > 'Z') &&
(character < '0' || character > '9') && character != '+' && character != '-' && character != '.' {
return false
}
}
return true
}
func nodeImportDiagnostic(code Code, path Path, message string, node *document.Node) Diagnostic {
location := publicLocation(node.Location)
return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location}
}
func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic {
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := current.Key(entry.Key)
if entry.Key == "_configkit" {
location := publicLocation(entry.KeyLocation)
return []Diagnostic{{
Code: CodeProfileDirective, Severity: SeverityError, Path: childPath,
Message: "profiles cannot contain configuration directives", Primary: &location,
}}
}
if diagnostics := profileDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 {
return diagnostics
}
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
if diagnostics := profileDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 {
return diagnostics
}
}
}
return nil
}
func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic {
var diagnostics []Diagnostic
if node.Kind == document.Mapping {
for _, entry := range node.Mapping {
childPath := path.Key(entry.Key)
if entry.Key == "_configkit" {
location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column}
diagnostics = append(diagnostics, Diagnostic{
Code: CodeImportDisabled, Severity: SeverityError, Path: childPath,
Message: "configuration directives require imports to be enabled", Primary: &location,
})
}
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(entry.Value, childPath)...)
}
} else if node.Kind == document.Sequence {
for index, child := range node.Sequence {
diagnostics = append(diagnostics, disabledDirectiveDiagnostics(child, path.Index(index))...)
}
}
return diagnostics
}

90
discovery.go Normal file
View 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...)}
}

11
doc.go
View File

@@ -1,10 +1,9 @@
// Package configkit provides deterministic, application-owned configuration
// loading for Go programs.
//
// Load currently supports one file, byte, reader, or explicit no-input source with
// strict YAML, typed defaults, schema-version checks, detached metadata, safe
// diagnostics, recorded application resolution, read-only semantic validation, and
// structured load errors. Composition, profiles, overrides, secrets, and effective
// rendering are not available yet. The configuration contract is documented in
// docs/config.md.
// Load supports file, byte, reader, and explicit no-input sources with strict YAML,
// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached
// metadata, recorded application resolution, typed secret references, safe effective
// rendering, read-only semantic validation, and structured load errors. The
// configuration contract is documented in docs/config.md.
package configkit

View File

@@ -1,6 +1,8 @@
# ADR 0005: Record explicit CLI overrides and safe value explanation
Status: accepted
Status: Partially superseded by
[ADR 0018](0018-cli-after-structural-resolution.md) for override-before-decoding
ordering. The remaining ownership and safe explanation decisions remain accepted.
Date: 2026-09-06

View File

@@ -0,0 +1,61 @@
# ADR 0006: Keep the public facade root-owned
## Status
Accepted and implemented on 2026-09-12.
## Date
2026-09-12
## Context
The root `configkit` package formerly combined the supported consumer API with
loading, decoding, inspection, and rendering machinery. That made ordinary
consumer navigation harder and risked exposing implementation details through
future public declarations. Moving files alone would not preserve the identity,
method sets, callbacks, ownership rules, or reflection-sensitive behavior of the
root's public types.
## Decision
Keep `configkit` as the sole supported consumer import path and keep all public
named types defined there. Lifecycle machinery lives in a cohesive
`internal/engine`; the root facade holds private engine state and forwards
deliberately selected methods. The dependency direction is root to engine and
its supporting internal packages. Internal production packages do not import the
root package.
The facade will use root-owned wrappers and explicit private conversions. It
will not use public aliases to engine types, anonymous embedding that promotes
engine methods, public engine types in signatures or fields, or public bridge
methods. Existing document and YAML boundaries remain intact.
The migration used a temporary internal candidate facade while the unchanged root
remained the only production entry point. That duplication was a bounded
relocation mechanism, not a second supported package, feature path, or runtime
selection mechanism; the final cutover removed it and the old root implementation.
The [architecture policy](../policy/architecture.md#public-api-and-dependency-direction)
owns the binding boundary. The [component overview](../internal/overview.md)
describes the implemented layout; the [development guide](../development.md)
locates the completed roadmap and implementation history.
## Alternatives considered
- Alias root types to engine types: reduces forwarding, but makes the engine's
exported method set and type ownership part of the consumer API.
- Keep all machinery in the root package: avoids migration work, but leaves the
public boundary difficult to navigate and easy to expand accidentally.
- Introduce a generic adapter or registration framework: could reduce some
handwritten conversions, but adds machinery and obscures the ownership rules
this boundary needs to preserve.
## Consequences
The refactor carries explicit forwarding and conversion work, especially for
callbacks, detached records, source ownership, frozen inspection, and the
exact-type secret bridge. Public compatibility must be verified by behavior and
external-package evidence rather than inferred from package names. Internal
exports remain implementation surfaces, and the completed architecture will
have one engine and one root facade.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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.

View 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/`.

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,37 @@
# Consumer Adoption Guide
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and
profile traversal, precedence, provenance, secret lookup, and safe inspection from
selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from
applications. Consumers still own their configuration structs, defaults, semantic
validation, schema-version policy, CLI parsing, runtime matching and conversion,
resource checks, remote acquisition, and service initialization.
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 |
| --- | --- | --- |
| Narratio-like | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
| Notarius-like | Canonical object bindings, a string schema version, 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 |
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
| Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
executable examples and ordinary tests. It contains no private application data,
credentials, network calls, or dependency on a sibling checkout.
The Notarius-shaped fixtures deliberately load both `version: 4` and
`version: "4"` into the same string-backed application field. Its version checker
still owns the supported identifier; fractional numeric input and unsupported
integer identifiers remain negative examples.
## Happy path
An application defines ordinary types and passes one explicit source. Optional
@@ -29,7 +41,7 @@ features appear only when the application needs them:
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{
Definition: rules,
Imports: useDeclaredImports,
Profile: selectedConfigProfile,
Overlay: selectedConfigOverlay,
Overrides: explicitlySetFlags,
})
if err != nil {
@@ -38,11 +50,185 @@ if err != nil {
return start(result.Config)
```
Simple consumers use neither imports nor profiles. A complex consumer can opt into
both without redefining their syntax. ConfigKit profiles are configuration overlays;
Simple consumers use neither imports nor selected overlays. A complex consumer can opt into
both without redefining their syntax. ConfigKit overlays are configuration overlays;
they do not select or replace an application's LLM, model, pipeline, report, or
execution profile concepts.
## 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
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-
@@ -51,6 +237,24 @@ validate keys and ranges and convert only at their runtime boundary. Neither sha
needs YAML reserialization, a parser hook, a ConfigKit registry, or a generic subtree
conversion API.
## Source-aware application path policy
[`LoadNotariusPaths`](../../examples/consumers/consumers.go) is the maintained
path-bearing example. Its application resolver uses `Resolution.InputInfo` and
`Resolution.Source` before normalizing: relative CLI input uses an explicitly
supplied invocation directory, retained defaults use the canonical main source,
and a file-authored scalar uses its one declaring source. Absolute paths remain
unchanged. Its executable test covers the matching effective YAML and explanation
history, source-file ownership, and a symlink opened through an alias; canonical
target-file directories, rather than the alias directory or process working
directory, are the selected bases.
The selected example policy leaves a relative value unchanged and emits a warning
when a CLI base is absent or file provenance is missing or composite. That is an
application choice, not a ConfigKit fallback: consumers can instead reject it or
choose another explicit base. The example intentionally performs neither target
existence checks nor confinement checks.
## Intentional migration changes
The examples preserve domain meaning while adopting one configuration convention:
@@ -61,8 +265,9 @@ The examples preserve domain meaning while adopting one configuration convention
domain rule. Loaded and constructed values use the same validator.
- Canonical module and object bindings replace string shorthand. Unknown fields and
incompatible option shapes fail at their semantic paths.
- Schema versions are strings. Consumers with integer versions migrate their files
instead of enabling another scalar convention.
- Schema versions remain string-backed in application types. A configured
`VersionRule` accepts string or strict decimal-integer YAML at its exact path and
supplies the spelling to both the checker and a plain string destination.
- Integers inside dynamic options are portable `int64` values. Runtime `int`
conversion is explicit and range checked.
- Strict YAML intentionally rejects duplicate keys, aliases, merge keys, unsupported
@@ -76,6 +281,16 @@ The examples preserve domain meaning while adopting one configuration convention
blocked empty variables remain blocked, and ordinary `Load` never changes the
process environment.
### Selected-overlay migration
To adopt the current selected-overlay API, rename `Options.Profile`,
`LayerProfile`, and `CodeProfile*` references to their `Overlay` equivalents; move
selected files from `profiles/<name>.yml` to `overlays/<name>.yml`; update any
handling of the `overlay` layer and `overlay.*` diagnostic strings; then rebuild.
The [configuration contract](../config.md#selected-overlays) is the canonical
behavioral reference. Application-owned profile concepts, such as `model_profile`,
are unchanged, and any CLI flag rename remains the application's decision.
These examples are migration evidence, not completed changes to Narratio, Notarius,
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
wiring, runtime catalogs, and operational documentation remains separate downstream
@@ -89,9 +304,13 @@ focused behavioral owner:
| Roadmap surface | Maintained owner |
| --- | --- |
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
| Source/import/profile 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 |
| `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 |
The package-level import-boundary test covers the consumer examples too: supported

View File

@@ -8,28 +8,62 @@ public behavior.
## Initial orientation
1. Inspect the working tree and preserve unrelated changes.
2. Read the architecture policy for code or design work.
3. Read the policy, contract, and internal documents applicable to the task.
2. Use the Policy Reading Guide below to select applicable policies.
3. Read the contract and internal documents applicable to the task.
4. Inspect the relevant implementation and behavioral tests before changing them.
## Policy Reading Guide
| Policy | Read before… |
| --- | --- |
| [Policy set provenance](policy/meta.md) | Adopting policies, checking upstream revisions, or updating the adopted policy set. |
| [Agents](policy/agents.md) | Recommending, planning, or implementing repository changes. |
| [Architecture](policy/architecture.md) | Planning or changing Go library code or its structure. |
| [Testing](policy/testing.md) | Selecting validation for code changes, or adding, changing, removing, or evaluating tests. |
| [Documentation](policy/documentation.md) | Writing, revising, or reorganizing documentation. |
| [OSS](policy/oss.md) | Adding or updating dependencies or incorporated third-party material, or changing licensing or attribution. |
| [Release](policy/release.md) | Preparing or publishing a release, changing release procedures, or making versioning and compatibility decisions. |
- Read applicable policies before making decisions they govern; multiple policies
may apply.
- Select policies using the task triggers above. Follow cross-references to
relevant sections when needed to resolve a concrete question.
- Reuse policies already available in context; reread when changed or no longer
available.
- Expand reading when the task's scope expands.
- When revising a policy itself, read that policy and any provisions elsewhere
affected by the revision.
## Task-specific reading guide
| Task | Read |
| --- | --- |
| Repository orientation or component responsibility | [Architecture policy](policy/architecture.md) and [component overview](internal/overview.md) |
| Documentation changes | [Documentation policy](policy/documentation.md) and the owning document |
| Architecture decisions or future work | [Documentation policy](policy/documentation.md), [deferred scope](roadmap/deferred.md), and relevant ADRs listed below |
| Code and test changes | [Architecture policy](policy/architecture.md), [testing policy](policy/testing.md), and the owning contract and tests from the map below |
| Repository orientation or component responsibility | [Component overview](internal/overview.md) |
| Input destinations | [Configuration contract](config.md#input-destinations), [ADR 0011](adr/0011-input-destinations.md), and the destination tests below |
| Shared source-version checks | [Configuration contract](config.md#shared-source-version-checks), [ADR 0013](adr/0013-shared-source-version-checks.md), and the version/source-version tests below |
| Documentation changes | The owning document |
| Licensing, dependencies, attribution, or release-document changes | [Project grant](../COPYRIGHT) and [attribution](oss/attribution.md); release changes also follow the release procedure |
| Architecture decisions or future work | [Future features](roadmap/future.md) and relevant ADRs listed below |
| Code and test changes | The owning contract and tests from the map below |
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
| Release preparation | [Release policy](policy/release.md) and [release procedure](release.md) |
| Release preparation | [Release procedure](release.md) |
Accepted decisions:
- [Separate composition and overlay](adr/0001-separate-composition-and-overlay.md)
- [Check source versions with one shared policy](adr/0013-shared-source-version-checks.md)
- [Own YAML semantics behind a private parser boundary](adr/0002-own-yaml-semantics.md)
- [Preserve presence and record application resolution](adr/0003-preserve-presence-and-resolution-history.md)
- [Secret references and explicit bootstrap](adr/0004-secret-references-and-explicit-bootstrap.md)
- [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.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
and remediation plans are preserved in Git history at `cb5b435`; use current
@@ -40,13 +74,126 @@ git show cb5b435:docs/roadmap/remediation.md
```
Release-candidate, publication, and verification work follows the operational
[release procedure](release.md).
[release procedure](release.md). Its module-selected ReleaseKit `publish`
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
records are preserved in Git history at `244104f`. The
[configuration contract](config.md#application-schema-versions) owns current behavior.
The completed source-origins and public-facade roadmaps, implementation plan, and
resolved review findings are preserved in Git history at `5760614`, for example
`git show 5760614:docs/roadmap/remediation.md`. The earlier source-origins staged
plan remains at `28e7fc5:docs/roadmap/implementation.md`. Current resolution
behavior belongs to the [configuration contract](config.md#recorded-application-resolution),
and current facade ownership belongs to the [component overview](internal/overview.md).
## Baseline validation
Install Go 1.25.0 or a supported patched/newer Go toolchain. Acquire declared
dependencies explicitly before entering an offline development or validation
environment:
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
preserved in Git history at `73ce6e9` under `docs/roadmap/`. Current overlay
behavior belongs to the [configuration contract](config.md#selected-overlays),
and release operations belong to the [release procedure](release.md).
The completed ReleaseKit adoption, startup performance request and experiments,
benchmark evidence, performance audit, implementation plan, and remediation
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#3-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.
Potential enhancements are tracked in [future features](roadmap/future.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
go mod download
@@ -76,8 +223,10 @@ Use focused checks while iterating, with validation proportionate to the change
under the testing policy. Documentation-only work normally needs the local-link
check below and whitespace review; behavior-bearing examples and commands also
need execution. Use `go test -race ./...` for ownership or concurrency changes.
The separate [release checker](release.md#4-validate-the-source-candidate) adds
two-toolchain, module-hygiene, race, and cross-build gates.
The [release procedure](release.md#4-publish-the-committed-candidate) performs
its selected validation through one `publish` invocation. The everyday checker
remains a development gate and is not a separate release prerequisite. CI never
gates a release.
## Task-to-test map
@@ -87,16 +236,30 @@ code changes. Use the component overview to find the corresponding implementatio
| Task | Behavioral test owners | Focused command |
| --- | --- | --- |
| YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../parse_test.go) | `go test ./internal/yaml` and `go test . -run '^TestParseSource'` |
| File/reader access, imports, globs, containment | [Sources](../source_test.go), [FIFO](../source_fifo_test.go), [imports](../imports_test.go), [globs](../glob_test.go) | `go test . -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` |
| Composition, profiles, CLI precedence | [Composition](../compose_test.go), [overlay](../overlay_test.go), [profiles](../profile_test.go), [overrides](../override_test.go) | `go test . -run 'Compose\|Overlay\|Profile\|Override'` |
| Schema, defaults, projection, cloning | [Schema](../schema_test.go), [decoding](../decode_test.go), [projection](../projection_test.go) | `go test . -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` |
| Lifecycle, resolution, validation, callback ownership | [Loading](../load_test.go), [resolution](../resolution_test.go), [validation](../validation_test.go), [ownership](../callback_ownership_test.go), [independence](../independence_test.go) | `go test . -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
| Paths, metadata, provenance | [Paths](../path_test.go), [metadata](../metadata_test.go), [provenance](../provenance_test.go) | `go test . -run 'Path\|Metadata\|Provenance\|Queries'` |
| Secrets, bootstrap, redaction, explanation | [References](../secret_reference_test.go), [bootstrap](../secrets_test.go), [rendering](../render_test.go), [explanation](../explain_test.go) | `go test . -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'` |
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go) | `go test . -run '^(TestLocalDocumentationLinks\|TestDependencyImportBoundary)$'` |
| Check-command behavior | [Shell regression](../release_check_test.go) | `go test . -run '^TestCheckScripts'` |
| 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), [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'` |
| 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 | [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 | [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'` |
| Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` |
| Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` |
| Passive resource contexts | [Engine contexts](../internal/engine/resource_context_test.go), [public contexts](../facade_resource_context_test.go) | `go test ./internal/engine -run 'Resource'` and `go test . -run 'SourceContext'` |
| Secrets, bootstrap, redaction, explanation | [References](../internal/engine/secret_reference_test.go), [bootstrap](../internal/engine/secrets_test.go), [rendering](../internal/engine/render_test.go), [explanation](../internal/engine/explain_test.go) | `go test ./internal/engine -run 'Secret\|Bootstrap\|Effective\|Explain\|Explanation\|History\|Sensitivity'` |
| Effective configuration diff | [Engine comparison and masking tests](../internal/engine/diff_test.go), [engine operation tests](../internal/engine/diff_operation_test.go), [root facade tests](../facade_diff_test.go), and [consumer examples](../examples/consumers/diff_test.go) | `go test ./internal/engine -run 'Diff|Explain|History|Secret|Independent'`, `go test . -run 'Diff|FacadeInspection|PublicFacadeBoundary|DependencyImportBoundary'`, `go test ./examples/consumers`, and `go test . -run '^ExampleDiff$'` |
| Consumer examples and migration fixtures | [Consumer tests](../examples/consumers/consumers_test.go), [native, layered, and ordinary relative-import examples](../examples/consumers/layered_test.go), [mounted collection examples](../examples/consumers/destinations_test.go), [API examples](../example_test.go) | `go test ./examples/consumers` and `go test . -run '^Example'` |
| Assembly and layered scaling evidence | [Assembly benchmarks](../internal/engine/assembly_bench_test.go), [layered benchmarks](../internal/engine/underlay_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkUnderlay' -benchmem`; repeat with `^BenchmarkCompose` and `^BenchmarkOverlay` |
| Identified-sequence scaling evidence | [Mixed-upsert benchmark](../internal/engine/sequence_combine_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkSequenceMixedUpsert$' -benchmem` |
| Inheritance scaling evidence | [Expansion benchmarks](../internal/engine/inheritance_bench_test.go) | `go test ./internal/engine -run '^$' -bench '^BenchmarkInheritance' -benchmem` |
| Documentation links and dependency isolation | [Link checks](../documentation_test.go), [import boundary](../import_boundary_test.go), [public boundary](../public_boundary_test.go) | `go test . -run 'TestLocalDocumentationLinks\|TestDependencyImportBoundary\|TestPublicFacadeBoundary'` |
| Development check behavior | [Development checker test](../development_check_test.go) | `go test . -run '^TestDevelopmentCheckHandlesFilenamesAndStopsOnFailure$'` |
| Release profile wiring | [Release profile test](../releasekit_profile_test.go) | `go test . -run '^TestReleaseKitProfile$'` |
## Fuzz validation
@@ -104,7 +267,7 @@ Run the bounded fuzz targets independently from the repository root. These exact
commands keep each campaign short and constrain concurrent workers:
```sh
go test . -run='^$' -fuzz='^FuzzParsePath$' -fuzztime=30s -parallel=2
go test ./internal/engine -run='^$' -fuzz='^FuzzParsePath$' -fuzztime=30s -parallel=2
go test ./internal/yaml -run='^$' -fuzz='^FuzzParseDocument$' -fuzztime=30s -parallel=2
go test . -run='^$' -fuzz='^FuzzCompositionAndOverlay$' -fuzztime=30s -parallel=2
go test ./internal/engine -run='^$' -fuzz='^FuzzCompositionAndOverlay$' -fuzztime=30s -parallel=2
```

View File

@@ -137,9 +137,11 @@ permission failures, explicit `conf.d` use, and entry/candidate budgets. This wo
has no Narratio production equivalent; it uses the selected standard-library
`path.Match` and bounded `File.ReadDir` adaptation rather than its flat loader.
### Profiles and overlay
### Upstream profiles and ConfigKit selected overlays
Assigned to selected-profile overlay:
The following upstream symbols and case names retain their original profile
terminology. ConfigKit adapts their selected-overlay behavior without renaming the
upstream evidence:
- Production: `mergeOverlayComposition`, `mergeOverlayNodes`,
`selectPipelineProfile`, `loadPipelineProfileOverlays`, and
@@ -153,19 +155,21 @@ Assigned to selected-profile overlay:
`TestLoadPipelineProfilePairResolvesIndependentEffectivePipelines`.
- Preserve: recursive mapping overlay, atomic scalar/sequence replacement,
deterministic selection failures, source ownership, and independent results.
- Adapt: load only the selected fixed profile path; lowercase `default` means no
overlay. ConfigKit permits null and cross-kind replacement and does not support
declared defaults, inheritance, arbitrary profile paths, or validating every
- Adapt: load only the selected fixed overlay path; lowercase `default` means no
selection. ConfigKit permits null and cross-kind replacement and does not support
declared defaults, inheritance, arbitrary overlay paths, or validating every
unselected overlay.
Implementation record (2026-09-06): `overlayDocuments`, `mergeOverlayMappings`,
`loadProfileDocument`, and `profileDirectiveDiagnostics` adapt the assigned merge,
selection, loading, and ownership mechanics. `TestOverlayDocumentsRecursesMappingsAndReplacesAtomicSubtrees`,
`TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults`, profile selection/error,
directive/version, containment, and budget cases adapt the assigned tests. ConfigKit
Implementation record (2026-09-06, updated 2026-09-23): `overlayDocuments`,
`mergeOverlayMappings`, `loadSelectedOverlayDocument`, and
`overlayDirectiveDiagnostics` adapt the assigned merge, selection, loading, and
ownership mechanics. `TestOverlayDocumentsRecursesMappingsAndReplacesAtomicSubtrees`,
`TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults`, selected-overlay
selection/error, directive/version, containment, and budget cases adapt the assigned
tests. ConfigKit
uses exactly one fixed selected `.yml` source, permits null and cross-kind replacement,
retains displaced descendant history, rejects any profile directive or version-path
contribution, and counts the profile in aggregate budgets. Narratio's declared
retains displaced descendant history, rejects any overlay directive or version-path
contribution, and counts the overlay in aggregate budgets. Narratio's declared
default, multi-overlay validation, inheritance-like application conventions, and
null/kind restrictions were not retained.
@@ -196,7 +200,7 @@ previously extracted source claims, overlay displacement records, typed baseline
and audited resolution snapshots in `load_metadata.go`. The representative
`TestLayeredProvenanceAgreesWithResolvedSnapshot` adapts Narratio's stable ownership,
profile ownership, deterministic effective projection, and side-effect-free
inspection cases across one default/import/profile/resolution workflow. ConfigKit
inspection cases across one default/import/overlay/resolution workflow. ConfigKit
adds chronological descendant history for parent and whole-sequence operations,
composite empty-mapping origins, canonical resolution reasons/dependencies, private
snapshot agreement, defensive nested copies, and concurrent load isolation. Value

87
docs/internal/imports.md Normal file
View File

@@ -0,0 +1,87 @@
# 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. Candidate selection 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. 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.
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 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 main owners are [`import_selection.go`](../../internal/engine/import_selection.go),
[`directory_walk.go`](../../internal/engine/directory_walk.go),
[`import_graph.go`](../../internal/engine/import_graph.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, scheduler, and scheduling
integration tests under `internal/engine`.

View File

@@ -1,89 +1,89 @@
# Internal Component Overview
ConfigKit exposes the root `configkit` package and keeps its document and YAML
representations private. The implemented components and their owners are:
ConfigKit exposes one supported root `configkit` package. The root owns public
named types, declaration comments, constructors, and explicit conversion/wrapper
code. It delegates loading and frozen inspection to `internal/engine` through
private state; public signatures and fields do not expose engine types.
- immutable semantic `Path` construction and strict JSON Pointer parsing;
- detached effective presence/origin and chronological history queries in
`metadata.go`, with provenance and diagnostic record types;
- canonical, control-safe diagnostic rendering and structured `LoadError` access;
- opaque file, byte, reader, and no-input source construction in `source.go`,
with shared regular-file checks and bounded read mechanics in `source_file.go`;
- a parser-independent ordered tree in `internal/document`, with root-private
semantic lookup, indexing, and value traversal in `document_walk.go`;
- the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary;
`parse.go` owns shared translation into detached, ordered public diagnostics;
- a private strict composer in `compose.go` that joins only disjoint document-tree
definitions, retains ordered claims and composite mapping origins, reports every
conflicting location, and never aliases an input tree;
- fixed selected-profile loading in `profile.go` and recursive, non-aliasing overlay
with displacement records in `overlay.go`; overlay records retain origins,
while value explanation uses separate history applications;
- CLI override validation/preparation in `override.go`, parser-neutral values in
`override_value.go`, and synthetic-source application in `override_apply.go`,
sharing overlay, source, byte, node, and depth policies;
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
- descriptor-driven canonical projection in `projection.go`, detached typed
cloning with explicit secret-state modes in `clone.go`, shared reflection and
cycle/limit state in `value_operation.go`, and configuration-value comparison
in `document_compare.go`; validation separately audits sensitivity and secrets;
- strict schema-driven decoding over detached defaults in `decode.go`, including
mapping/sequence semantics, null and numeric policy, safe codec errors, presence,
baseline/final trees, and initial default/input origins;
- public lifecycle coordination in `load.go`, option selection in `options.go`,
input assembly in `load_input.go`, typed phases/publication in `load_result.go`,
bounded canonical import DFS in `imports.go`, filesystem context/containment in
`filesystem.go`, directive parsing/restrictions in `directives.go`,
pattern-directed glob expansion in `glob.go`, schema-version checking and report
contexts in `validation.go`, and layered metadata/history assembly in
`load_metadata.go`;
- recorded application defaulting and normalization in `resolution.go`, including
explicitness queries, helper verification, structural mutation audit, coalesced
intent, private before/after snapshots, and resolution metadata integration;
- post-resolution schema-aware report collection and validator mutation detection in
`validation.go`, completing the public callback lifecycle after composed input;
- opaque typed environment references and per-load name resolution in
`secret_reference.go`, including credential-free projection/cloning and private
validator-state auditing;
- explicit bounded startup environment bootstrap and safe partial reporting in
`secrets.go`;
- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`;
- canonical effective output in `render.go`, using frozen-snapshot redaction
shared with explanation in `snapshot.go`;
- detached public explanation records and coordination in `explain.go`, immutable
input applications/replay and retained-node budgeting in `history.go`, shared
safe snapshot projection in `snapshot.go`, and deterministic human output in
`explain_render.go`; the only upstream dumper integration remains isolated in
`internal/yaml/write.go`;
- a separate maintained `examples/consumers` package with four sanitized consumer
shapes, fixtures, domain validators, and runtime-boundary functions; and
- an everyday single-toolchain check in `scripts/check.sh` and a stricter
source-candidate checker in `scripts/release-check.sh`, with local
documentation-link and public import-boundary gates owned by root-package tests.
Implemented component owners:
Dependency direction remains inward: the public lifecycle coordinates private
source, document, schema, and inspection helpers. Semantic paths stay in the root
package; `internal/document` owns only representation and cloning and never imports
the root package. Only `internal/yaml` imports the upstream YAML package. Maintained
consumer examples use the public package and standard library.
- `internal/engine` owns host and native-filesystem source acquisition and
explicitly selected configuration discovery. A search resolves once during
shared acquisition; the selected concrete file or absent-input state then
follows the existing composition, metadata, and resource-context lifecycle.
It also owns grouped strict composition, recursive precedence assembly,
per-input mapping and sequence-item placement, YAML collection declarations
and operations for identified sequences, and application identity registrations for strict
composition, inheritance, and final validation. It applies the selected
collection operation at precedence boundaries and owns dependency-ready,
destination-local inheritance before typed decoding. CLI assignment follows
inheritance and decoding, before application resolution. The engine also owns
shared source-version checking with sanitized declaration evidence.
Explicit import discovery supports declared paths, directory searches, and
globs; load-wide defaults and source-local empty-selection evidence cross the
root facade without changing acquisition order. Import and inheritance
resolution share dependency-ready scheduling within their composition scopes.
It retains each source's effective placement alongside physical source
identity and owns schema
inspection, lifecycle coordination, callback auditing,
provenance/history, passive source-context retention, secret resolution and
bootstrap, safe inspection rendering, and effective-value comparison over
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/yaml` is the sole upstream YAML adapter and parser/dumper boundary.
- Engine mechanism tests own source, composition, inheritance, decoding,
callback, provenance,
source-context, secret, history, rendering, and bootstrap behavior. Root tests
own public API compatibility, facade integration, documentation links, and
dependency/public boundary checks. Consumer examples remain under
`examples/consumers`.
The [configuration contract](../config.md) owns implemented input, schema, decoding,
loading lifecycle, path imports, composition, profile/CLI overlay, version checking,
recorded resolution, typed secret references/bootstrap, semantic validation,
metadata, secret classification, redacted effective rendering, historical
explanation, and ownership behavior.
The [consumer adoption guide](../consumers/README.md) owns current application-fit
and migration guidance.
Dependency direction is root to engine to document/YAML support. The root may
import only `internal/engine` among project internal production packages; engine
and its supporting packages do not import root. The root boundary test enforces
these constraints and rejects exported aliases, fields, or signatures involving
internal types.
The source-extraction inventory records the application foundations used by the
implementation:
- [Extraction inventory](extraction.md)
The [architecture policy](../policy/architecture.md) owns binding boundaries and
invariants. The [deferred scope](../roadmap/deferred.md) records deliberate
exclusions. See the
[development guide](../development.md) for contributor orientation and validation
and the [release procedure](../release.md) for source-candidate gates.
The [configuration contract](../config.md) owns implemented YAML and lifecycle
semantics, including
[configuration discovery](../config.md#configuration-discovery),
[input destinations](../config.md#input-destinations),
[shared source-version checks](../config.md#shared-source-version-checks), and
[effective configuration diffs](../config.md#effective-configuration-diffs).
The [strict composition contract](../config.md#strict-document-composition)
owns sequence declarations, identity, operations, ownership, and final
validation; [CLI overrides](../config.md#explicit-cli-overrides) own override
ordering. The [consumer guide](../consumers/README.md#identified-prompt-sequences)
links maintained workflow examples.
The [consumer adoption guide](../consumers/README.md) owns application guidance.
The [architecture policy](../policy/architecture.md) owns contributor
boundaries; [ADR 0006](../adr/0006-public-facade.md) records the facade decision
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
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.

43
docs/oss/attribution.md Normal file
View File

@@ -0,0 +1,43 @@
# Open Source Attribution Notices
## Standard Repository Policies
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/architecture.md`, `docs/policy/testing.md`; root `AGENTS.md`; and the
Policy Reading Guide in `docs/development.md`, adapted from
`d84a38ebab26f048cafbe59a2246700c35d29843` for this repository.
Copyright (C) 2026 Eric Rakestraw
SPDX-License-Identifier: CC-BY-SA-4.0
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) |
[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/e833faddf120c789a9b45348edaf75720de16348).
## go-yaml v4
Selected module: `go.yaml.in/yaml/v4` at `v4.0.0-rc.6`.
Copyright 2011-2019 Canonical Ltd
Copyright 2025 The go-yaml Project Contributors
Copyright 2006-2010 Kirill Simonov
SPDX-License-Identifier: Apache-2.0 AND MIT
License and notices: [Apache 2.0](licenses/Apache-2.0.txt) |
[MIT](licenses/MIT.txt) |
[NOTICE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/NOTICE) |
[LICENSE](https://github.com/yaml/go-yaml/blob/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125/LICENSE).
[Source](https://github.com/yaml/go-yaml/tree/1c17b9cee72bf81ae73a8be50fbd7dd3a9985125).

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,170 @@
Creative Commons Attribution-ShareAlike 4.0 International
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.
Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.
Creative Commons Attribution-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
Section 1 – Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights under this Public License.
k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.
Section 2 – Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. Downstream recipients.
A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.
C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
Section 3 – License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
Section 4 – Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
Section 5 – Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
Section 6 – Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 – Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
Section 8 – Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

18
docs/oss/licenses/MIT.txt Normal file
View File

@@ -0,0 +1,18 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

91
docs/policy/agents.md Normal file
View File

@@ -0,0 +1,91 @@
# Agent Policy
**Base Policy Version:** 1.3
**Last Updated**: 2026-09-28
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/agents.md)
## Purpose
Agents must read and follow these rules.
## Implementation Discipline
**KEY PRINCIPLES:** Write the simplest idiomatic code that satisfies the user's
request. Do not implicitly strengthen requirements, restrictions, or guarantees
beyond what the user actually requested.
**Keep implementation machinery proportionate.** Use straightforward,
idiomatic code and introduce only the machinery needed to support the requested
behavior and flexibility. Optimize for conceptual simplicity and
maintainability, not for the fewest lines, fewest options, or narrowest product
surface.
**Do not invent restrictions.** Do not reject, prohibit, constrain, cap,
harden, or otherwise narrow behavior unless required by the user, an explicit
repository contract, or an unavoidable correctness constraint. Preserve
operator and caller choice where policy has not been established. If an
additional restriction appears advisable, present it as a decision rather than
silently implementing it.
A correctness constraint must follow from the requested behavior or an
established invariant, not from the convenience of the chosen implementation.
Explain the specific failure it prevents and why a less restrictive
implementation cannot satisfy the contract.
**Do not invent guarantees.** Do not strengthen public contracts, guarantees,
or invariants beyond those expressly requested by the user.
**Use the standard library.** For example: in almost every case, the best
approach to copying files is to use the standard library. Writing custom code to
implement power-loss protection at the application level, or to guard against
bad actors with privileged and concurrent access to the host's filesystem,
is wasteful, unnecessary, and not idiomatic. Stick to the simple, standard, and
idiomatic approach unless another path is expressly requested by the user.
**An implementation detail is not repository policy.** Tests and documentation
must not elevate incidental behavior into a durable guarantee or restriction
unless that behavior was expressly selected as part of the contract. Existing
explicit contracts remain authoritative; changing or weakening one requires a
deliberate decision rather than being inferred from this principle.
Agent-authored plans, tests, documentation, and prior implementations are not
evidence that an additional restriction was deliberately approved. Identify the
authorizing requirement or decision; when that authority is unclear, surface
the ambiguity rather than treating repetition as approval.
## Git Commit Policy
By default, commit each completed, nonempty unit of work using a
concise, plain-English message describing what changed. This policy is standing
authorization to create those local commits; a separate user request is not
required. An explicit user instruction to leave work uncommitted takes precedence.
A unit of work is a focused, reviewable outcome, including its relevant tests and
documentation.
Complete the appropriate validation before committing. If work is incomplete or
validation is blocked, report that state accurately.
Before committing:
1. Run `git status --short` and review the relevant diff, including any staged
changes. Identify changes that predate the current unit of work.
2. Exclude secrets, local environment files, generated junk, and unrelated changes.
3. Stage only the files or hunks belonging to the completed unit. Use `git add .`
only when every included change belongs to that unit. Preserve unrelated
staged and unstaged work.
4. Review `git diff --staged` and run appropriate checks for the change. Reuse
successful checks for unchanged content.
5. Review `git log --oneline -n 10` and use
`git commit -m "<concise plain-English commit message>"`.
6. Report the commit identifier and any remaining uncommitted work.
If the unit cannot be safely separated from pre-existing work, leave it uncommitted
and explain the overlap rather than silently including that work. If a commit
fails, preserve the changes and report the failure; do not bypass hooks.
Default commit authorization does not authorize pushing, publishing, amending,
resetting, reverting, or rewriting history. Those actions require a separate user
instruction. Do not use `git commit -a` or `git commit --no-verify` to bypass the
staging and validation discipline above.

View File

@@ -1,18 +1,61 @@
# Architecture Policy
**Base Policy Version:** 1.0
**Base Policy Version:** 1.3
**Last Updated:** 2026-09-06
**Last Updated**: 2026-10-22
## Purpose and applicability
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/architecture.md)
## Purpose And Ownership
The adopted policy defines the application's purpose and the architectural
constraints contributors must follow. It owns system boundaries, responsibility
and dependency rules, established invariants, and explicit non-goals.
Describe the concrete package inventory and implementation mechanics in internal
documentation. Link to canonical public contracts for their exact behavior and
to ADRs for decision rationale.
ConfigKit is an opinionated Go library for consistent YAML configuration loading
and resolution across consuming applications. This policy governs contributions.
The [component overview](../internal/overview.md) owns implementation inventory, the
[configuration contract](../config.md) owns implemented behavior, and the
[deferred scope](../roadmap/deferred.md) preserves deferred scope.
and resolution across consuming applications. The [component overview](../internal/overview.md)
owns implementation inventory, the [configuration contract](../config.md) owns
implemented behavior, and the [future feature list](../roadmap/future.md) preserves
candidates for future consideration.
## Success criteria
## Adopted Constraints And Implemented Structure
Architectural constraints govern current and future development. Record
implemented structure accurately, proposals and implementation status in the
roadmap, and significant decisions and rationale in ADRs. Describe a target
structure as implemented when its changes land.
Constraints must reflect deliberately selected requirements and decisions.
Do not promote incidental implementation choices into architectural invariants.
The implemented collection-policy and CLI-ordering constraints below reflect
accepted [ADR 0017](../adr/0017-collection-sequence-policy.md) and
[ADR 0018](../adr/0018-cli-after-structural-resolution.md). The configuration
contract owns their current behavior; these architectural constraints describe
the system boundaries and guarantees that guide changes to that behavior.
## Success Criteria
ConfigKit prioritizes a coherent configuration model: consistency and
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
configuration infrastructure while retaining clear ownership of their domain
@@ -21,8 +64,11 @@ The [component overview](../internal/overview.md) owns implementation inventory,
configuration model, with only the flexibility needed to express genuine
application requirements?
Both criteria must hold. ConfigKit should not preserve divergent consumer
conventions merely for compatibility, nor impose restrictions that prevent useful
Both criteria must hold. Minimize consumer loading and processing code subject
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
existing primitives before adding another mechanism. Extend the model when a
concrete requirement cannot be accommodated cleanly, not simply because a
@@ -51,56 +97,209 @@ boundaries, and keep simple consumers independent of optional composition
features. The [consumer adoption guide](../consumers/README.md) demonstrates the
implemented surface against representative simple and complex shapes.
## Responsibility boundaries
## Simplicity, Defaults, And Explicit Controls
These principles apply throughout ConfigKit: to configuration syntax, the public
Go API, defaults, and feature design. Evaluate their effects on both configuration
authors and consuming applications.
**Keep straightforward workflows straightforward.** Common configuration
workflows should require little configuration text and minimal API setup.
Support more demanding workflows through explicit, composable options, without
requiring simple consumers to understand or configure those options.
**Prefer simple, permissive defaults with explicit controls.** Choose predictable
defaults that minimize required setup and accommodate valid configurations
without unnecessary restrictions. Provide selectable options for workflows that
need additional capabilities, narrower behavior, or stronger requirements.
Measure simplicity by what consumers must understand and maintain, including
configuration text and application-side plumbing. Concise syntax is useful, but
hidden inference and interacting meanings can make a shorter declaration harder
to understand. Prefer explicitness where it makes behavior more predictable.
A terse configuration format that requires substantial application-side
infrastructure does not satisfy the simplicity principle.
Define what permissiveness means for each behavior. Allowing an optional
contribution to be absent or accepting valid inputs without an unnecessary
filename restriction can reduce required setup. Invalid declarations, ambiguous
composition, and unreadable selected sources must still produce meaningful
errors under the applicable contracts. Permissiveness does not authorize silent
recovery from invalid supplied configuration or weaken configuration integrity.
Options should express coherent choices with understandable purposes and fit
ConfigKit's shared configuration model. Support powerful controls when they
serve concrete needs; these principles do not require a switch for every
implementation behavior or conceivable consumer preference.
Defaults require judgment when simplicity and permissiveness pull in different
directions. Broader discovery can accommodate more layouts while increasing the
implicit search scope. Evaluate such tradeoffs explicitly; these principles do
not mechanically select the broadest behavior for every option. Changes to
existing defaults remain subject to compatibility review, and proposed behavior
remains in the roadmap until implemented.
## Responsibility Boundaries
ConfigKit owns configuration input processing and the coordination of decoding,
resolution, validation, and inspection. Applications own their configuration
types, field meanings, defaults, normalization, semantic validation, and schema
version policy. Applications also own main-file search conventions, CLI wiring,
remote acquisition, runtime initialization, and resource checks.
version policy. Applications choose and order their main-file search conventions;
ConfigKit provides explicitly selected search descriptions that resolve during
loading. Applications also own CLI wiring, remote acquisition, runtime
initialization, and resource checks. See the [configuration discovery
contract](../config.md#configuration-discovery) and [ADR
0019](../adr/0019-explicit-configuration-discovery.md).
The engine applies each input's mapping destination within the existing source
assembly, 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
runtime resources. It must not perform implicit network access, discover
application-specific paths, or change process-global state during ordinary `Load`.
runtime resources. It must not perform implicit network access, implicitly
discover application-specific paths, or change process-global state during
ordinary `Load`.
Import discovery is available only through declarations in selected sources:
explicit `path`, `dir`, and `glob` selectors may search their declared scope, but
ConfigKit does not search conventional application paths unless the application
explicitly supplies a discovery description, and it never discovers a main
configuration file implicitly. Imports and sibling inheritance share
dependency-ready scheduling within their existing scopes; acquisition order and
precedence-tier boundaries remain distinct. The [configuration contract](../config.md#explicit-imports)
owns observable behavior; [ADR 0015](../adr/0015-shared-dependency-ready-ordering.md)
and [ADR 0016](../adr/0016-independent-import-controls.md) record the decisions.
The explicit secrets-directory bootstrap is a narrow, opt-in exception: applications
invoke it during startup, and it reports writes, skips and partial failures. It
must never run implicitly as part of configuration loading.
Remote data can enter through application-supplied input without introducing a
network provider into the library.
## Public API and dependency direction
## 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
Keep one small, coherent public surface for the supported lifecycle. Simple
single-document consumers must not need import, profile, or provider machinery.
single-document consumers must not need import, overlay, or provider machinery.
Do not expose parser-specific nodes, errors, or extension hooks through the
public API. Parser integration belongs behind a private boundary, as recorded
in [ADR 0002](../adr/0002-own-yaml-semantics.md).
The root `configkit` package is the sole supported consumer import path. Public
named types stay root-owned and may hold private delegated implementation state;
their exported signatures, fields, and method sets must not expose internal
types or acquire internal methods through anonymous embedding. Root production
code may depend inward on a cohesive engine and its supporting internal packages,
but internal production packages must not import the root. Explicit forwarding
and local conversion are acceptable where they preserve type identity,
ownership, callback lifetime, and inspection behavior. The accepted root-wrapper
boundary and its migration rationale are recorded in
[ADR 0006](../adr/0006-public-facade.md); the implemented layout is described in
the [component overview](../internal/overview.md).
Separate input access, document processing, composition, decoding, metadata, and
rendering responsibilities without prescribing an interface or package for each.
Dependencies must preserve those boundaries; package splits and abstractions
must earn their complexity. Do not impose a full application or hexagonal
framework on this library.
## Configuration integrity
## Configuration Integrity
Composition and intentional overlay are distinct operations. Composition must
not silently become last-wins override through file ordering. The decision and
alternatives are recorded in [ADR 0001](../adr/0001-separate-composition-and-overlay.md).
Within a configuration tier, independent contributions use strict composition.
Shared mappings combine recursively. Sequences with an applicable identity
combine distinct records and reject duplicate identities, including equal
records. Other overlapping non-mapping values remain conflicts, including equal
values. Conflicting identity or operation declarations among peers also fail.
Traversal order establishes stable reporting and record order; it does not grant
precedence to peers. [ADR 0001](../adr/0001-separate-composition-and-overlay.md)
records the distinction between strict composition and intentional overrides.
The main document and its imports form a strictly composed tier. Explicit
underlay groups compose strictly within each group, then combine 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
parser defaults. Processing order and diagnostics must be deterministic for the
same inputs and options. Invalid input must not produce a successful usable
configuration. Do not run application hooks on invalid typed configuration.
User-controlled precedence is application defaults, composed base, selected
profile, then explicitly supplied CLI overrides. Applications own flag parsing;
ConfigKit owns overlay semantics and provenance. Domain resolution follows these
inputs and must remain recorded, not become a hidden override channel.
User-controlled precedence is application defaults, ordered underlays, composed
main input, selected overlay, then explicitly supplied CLI overrides.
This priority order is distinct from processing order: typed defaults are applied
at decoding, and application resolution follows CLI input under the lifecycle
rules above.
Preserve node-based composition until the effective input is ready for typed
decoding. Preserve presence and source information through processing. Ordinary Go zero
values must not be used to infer whether input was absent. Final configuration,
Preserve node-based composition through source assembly and inheritance until
typed decoding. Preserve presence and source information through later CLI and
application-resolution changes as well. Ordinary Go zero values must not be used
to infer whether input was absent. Final configuration,
metadata, and effective inspection must agree, including application resolution
changes; see [ADR 0003](../adr/0003-preserve-presence-and-resolution-history.md).
Safe historical values and an explanation renderer are part of the implemented contract,
@@ -110,7 +309,80 @@ See [ADR 0005](../adr/0005-explicit-overrides-and-safe-explanation.md).
Detailed syntax and operation tables belong to the
[current configuration contract](../config.md).
## Ownership and confidentiality
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
Returned configuration and metadata must not share mutable state across loads.
ConfigKit must not mutate a result after returning it. Ordinary exported Go
@@ -131,12 +403,12 @@ forwarded blindly when they can include raw input. Do not log configuration
implicitly. Applications remain responsible for classifying their secrets and
for safe messages supplied by their hooks.
## Scope boundaries
## Scope Boundaries
Do not introduce automatic directory discovery, network providers, arbitrary
merge strategies, additional configuration languages, or application runtime
management without concrete consumer evidence and an explicit design decision.
The [deferred scope](../roadmap/deferred.md#explicit-non-goals-and-deferred-scope)
owns the deferred-feature inventory. Significant boundary
changes require an ADR and corresponding policy/contract updates; implementation
convenience alone is not a reason to broaden the public API.
Record significant decisions to exclude capabilities, and their rationale, in
ADRs. Significant boundary changes require an ADR and corresponding
policy/contract updates; implementation convenience alone is not a reason to
broaden the public API.

View File

@@ -1,125 +1,179 @@
# Documentation Policy
**Base Policy Version:** 1.0
**Base Policy Version:** 1.11
**Last Updated:** 2026-09-06
**Last Updated**: 2026-10-16
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/documentation.md)
## Purpose
Assign each documentation topic one canonical owner so ConfigKit's documentation
remains accurate and useful to consumers, maintainers, and implementation agents.
This policy assigns each documentation topic to one canonical owner. Its goal is
to keep ConfigKit's documentation accurate, concise, discoverable, and resistant
to drift for consumers, maintainers, developers, and coding agents.
## Core rules
## Core Rules
### One canonical owner
### One Canonical Owner
Each authoritative fact belongs in one owning document or Go declaration's
reference documentation. Other documents may summarize and link to that owner;
they must not maintain competing definitions of commands, API contracts, YAML
semantics, defaults, paths, diagnostics, or compatibility guarantees.
Each authoritative fact belongs in one document. A non-owning document may give
a short, stable summary for orientation, but it must link to the canonical owner
for volatile details.
### Policies, current behavior, and future work
Volatile details include commands, flags, configuration fields and defaults,
module keys, schemas, file names, paths, status codes, retry behavior, and
runtime guarantees. If readers could reasonably treat a statement as a
contract, maintain it only in the owning document.
- Policies state currently binding contributor rules, including constraints on
future implementation. They do not imply that features already exist.
- Current API/configuration contracts, consumer guidance, and component
documentation describe implemented behavior only, including partial boundaries.
- ADRs record accepted decisions and rationale. Acceptance is not evidence of
implementation; execution status belongs in roadmaps.
- Roadmaps own unimplemented targets, deferred work, and execution status.
### Current And Future Behavior
Future implementation plans own their task order, acceptance gates, and progress;
`docs/roadmap/deferred.md` owns deliberate exclusions, not execution instructions.
As behavior lands, move its normative definition into the current contract and
replace the roadmap definition with a link. Retire completed roadmap material
as part of release preparation; ADRs preserve significant decision history.
Outside `docs/roadmap/`, documentation describes implemented behavior only.
Partial features may be described only to their implemented boundary.
### Audience and examples
Architectural constraints may govern future changes. Describe current structure
from implemented behavior.
Consumer documentation explains how to use the library and what consumers own.
Internal documents explain mechanisms and dependencies. Keep implementation
mechanics out of consumer guidance unless they affect a meaningful choice.
ADRs may record an accepted architectural decision before implementation. The
roadmap owns implementation status and sequencing until the decision is
implemented. Current architecture, user, operator, integration, and internal
documentation are updated when the behavior lands.
Complete copyable artifacts belong in `examples/`; Go API examples may live in
executable Go example tests beside the relevant package. Prose should use the
smallest useful excerpt and link to the maintained artifact. Examples must be
secret-free and checked against implemented behavior. Clearly label illustrative
future syntax in roadmaps; do not present it as a runnable current example.
`docs/roadmap/future.md` owns potential enhancements and their open questions;
task-specific plans own execution order and progress. Record significant
architectural exclusions and their rationale in ADRs. Do not maintain a roadmap
inventory of rejected proposals. Completed plans retire during release preparation.
### Security and privacy
### Audience And Detail
Do not include real credentials, private keys, environment dumps, sensitive input,
or private infrastructure details unless intentionally public. Document secret
handling, not secret values.
Write for the document's stated audience and include only the detail needed for
its owned topic. Keep user and operator docs focused on using and operating the
application. Developer docs should link to user-facing and external contracts
for their definitions.
## Canonical ownership
### Examples
| Topic | Owner | Boundary |
| --- | --- | --- |
| Product orientation | `README.md` | Current status, purpose, navigation, and a minimal working example |
| Contributor entry point | `docs/development.md` | Reading guide and actual baseline validation commands |
| Architecture rules | `docs/policy/architecture.md` | Responsibilities, dependency constraints, invariants, and scope boundaries |
| Documentation rules | `docs/policy/documentation.md` | Ownership and document lifecycle |
| Testing rules | `docs/policy/testing.md` | Risk-based sufficiency, test boundaries, and maintenance principles |
| Release rules | `docs/policy/release.md` | Compatibility, integrity, gates, immutability, and exceptions |
| Release procedure | `docs/release.md` | Exact maintainer commands, prerequisites, supported targets, and verification |
| Exported Go API | Go documentation beside declarations | Symbol contracts, errors, ownership, and lifecycle API behavior |
| YAML and resolution semantics | `docs/config.md` | Syntax, composition, precedence, conversion, secret references/bootstrap, and explanation semantics; Go reference docs link here for shared rules |
| Consumer walkthroughs | `docs/consumers/`, when useful | Task-oriented usage and consumer responsibilities, not duplicate API reference |
| Component inventory | `docs/internal/overview.md` | Implemented components and links to their owners |
| Component mechanics | Other `docs/internal/` files, when needed | Detailed implementation behavior and change recipes |
| Architectural decisions | `docs/adr/` | Context, decisions, alternatives, consequences, and supersession history |
| Deferred scope | `docs/roadmap/deferred.md` | Deliberate exclusions and the conditions for reconsidering them |
| Future implementation plans | Task-specific files under `docs/roadmap/` | Unfinished work, task order, acceptance gates, and progress; completed plans retire to Git history |
| Historical release notes | `docs/releases/` | Version-specific historical summaries required by release policy |
| Complete examples | `examples/` and executable Go examples | Maintained artifacts; prose links instead of duplicating full examples |
Complete copyable files belong in `examples/`. Documentation may use the
smallest illustrative snippet needed to explain its owned topic, but should link
to maintained examples for complete copyable files.
A listed owner is not a requirement to create an empty file before it has useful
content. Create API, consumer, example, and component documents when the corresponding behavior
exists and there is useful content to own. Link only to existing files; use plain
code paths for planned files. Unvalidated candidate and publication work has the
explicit temporary exception recorded in the release policy.
Executable Go API examples may live beside the relevant package.
ConfigKit has no owned HTTP service, application CLI, or runtime operations
interface. Do not create those reference documents for consuming applications.
Examples must be valid, secret-free, and tested where practical. Commands and
configuration used in documentation should match the application.
## Boundary rules
### Security And Privacy
Architecture owns governing constraints; ADRs explain why; contracts describe
implemented semantics; internal documentation explains how. A roadmap may link
to all of them but must not become a second current reference.
Documentation and examples must not contain real credentials, private keys,
private environment dumps, sensitive source material, or private infrastructure
details unless intentionally public. Document secret-handling mechanisms, not
secret values.
Release policy owns guarantees; the release procedure owns commands. A release
note cannot redefine a compatibility contract or replace a current-documentation
update. The note at an immutable tag is that release's historical record.
## Canonical Ownership
This policy owns what documentation may disclose. Architecture owns security
invariants. Current configuration and Go API contracts own secret-classification
and rendering behavior when implemented; internal docs own their mechanisms.
| Topic | Canonical owner | Owned content | Content owned elsewhere |
| --- | --- | --- | --- |
| Product orientation and minimal example | `README.md` | Purpose, current status, shortest useful Go example, and navigation, including licensing and attribution. | Full Go API and configuration contracts, release procedure, and implementation detail. |
| Contributor entry point | `docs/development.md` | Task-oriented reading guide, contributor orientation, baseline validation commands, and canonical links. | Package inventory, architecture rules, subsystem behavior, detailed change recipes. |
| Application architecture policy | `docs/policy/architecture.md` | System shape, normative ownership, dependency direction, boundaries, invariants, safety properties, and non-goals. | Concrete package inventory, implementation mechanics, contributor procedures, decision history, future work. |
| Policy-set provenance and upstream navigation | `docs/policy/meta.md` | Policy-set origin, local authority, adoption baselines, and upstream adoption and update links. | Task-specific reading triggers, detailed adoption procedures, and formal attribution notices. |
| Agent policy | `docs/policy/agents.md` | Proportionate, idiomatic implementation; rules against inventing requirements, restrictions, guarantees, or policy. | Application contracts, architecture, testing policy, and contributor procedures. |
| Documentation policy | `docs/policy/documentation.md` | Documentation ownership, audience boundaries, maintenance rules, and ADR/document lifecycle. | Application architecture or product behavior. |
| Testing policy | `docs/policy/testing.md` | Test philosophy, risk sufficiency, boundaries, doubles, coverage, regressions, and test maintenance. | Subsystem behavior, application contracts, subsystem-specific test inventories, and implementation plans. |
| Release policy | `docs/policy/release.md` | Versioning, compatibility, release integrity and immutability, release documentation, proportionate gates, publication controls, completion, and required versus optional checks. | Repository-specific commands, product installation guidance, CLI version semantics, historical release summaries, and CI implementation detail. |
| Open source licensing and attribution policy | `docs/policy/oss.md` | License defaults and author choices, COPYRIGHT and attribution formats, license/notice links, discoverability, maintenance, and concrete exceptions. | Component entries, license texts, release commands, and dependency security review. |
| Project copyright and license grant | `COPYRIGHT` | Project identity, copyright holders and years, SPDX expression, code grant, separately licensed document scope, warranty disclaimer, and third-party statement. | Complete license texts and upstream component attribution. |
| Project license texts | `LICENSE` | Complete project and incorporated companion license texts, with required project-specific fields. | General licensing policy and upstream component attribution. |
| Upstream component attribution | `docs/oss/attribution.md` | House Format entries for dependencies and adapted documents: identity, material scope, copyright, license, notices, and source links. | General policy, exception reasoning, and complete standard license texts. |
| Standard license texts | `docs/oss/licenses/` | Complete, verbatim standard license texts shared by applicable entries, including copied documentation. | Project-specific upstream notices, attribution prose, and policy interpretation. |
| Repository-specific release procedure | `docs/release.md` | Maintainer prerequisites, candidate preparation, validation and publication commands and automated guards, asynchronous deliverables, delivery status, optional consumer verification, and recovery. | General release requirements, product installation guidance, historical release summaries, and current compatibility contracts. |
| Exported Go API | Go documentation beside declarations | Symbol contracts, errors, ownership, and lifecycle behavior. | Shared YAML semantics and complete examples. |
| Configuration contract | `docs/config.md` | YAML syntax, composition, precedence, schema, defaults, resolution, validation, secrets, and explanation semantics. | Go symbol contracts, complete examples, and implementation detail. |
| Consumer guidance for the public Go API | `docs/consumers/` | Task-oriented public Go API usage, minimal examples, and consumer responsibilities. | Go symbol contracts, configuration semantics, and internal implementation detail. |
| Inventory of implemented internal packages | `docs/internal/overview.md` | Implemented packages and components, responsibilities, and links to focused internal docs. | Normative architecture, contributor reading policy, external contracts. |
| Behavior and architecture of internal packages | Other files under `docs/internal/` | Implementation flow, collaborators, state transitions, package-local guarantees and failures, and relevant tests. | Global architecture invariants, configuration definitions and defaults, external schemas, operator procedures. |
| Architectural decision records | `docs/adr/` | Significant decisions, context, alternatives, rationale, consequences, and supersession history. | Current behavior reference, implementation status, task sequencing. |
| Potential future features | `docs/roadmap/future.md` | Candidate enhancements and open questions for future consideration. | Rejected proposals, architectural decisions, task execution, and current behavior. |
| Future implementation plans | Task-specific files under `docs/roadmap/` | Unfinished work, sequencing, acceptance, and progress; completed plans retire during release preparation. | Implemented behavior and ADR rationale. |
| Release-note history | `docs/releases/` | One checked-in summary per source release under the procedure; the note at its immutable tag is the release record. | Current commands, behavior, contracts, and compatibility definitions. |
| Complete examples | `examples/` and executable Go examples | Maintained copyable artifacts and Go API examples. | Configuration reference, symbol contracts, and prose explanation. |
## Architectural decision records
Create supporting documents for existing interfaces and responsibilities when
substantive content is available.
Use sequential filenames, such as `0001-separate-composition-and-overlay.md`,
and the lightweight Nygard structure: title, status, date, context, decision,
alternatives considered, and consequences.
Do not create empty owners.
Accepted decision content is immutable. A changed decision requires a new ADR
and a superseded status/link in the old one. Navigation links may be repaired
when specification ownership moves without rewriting historical rationale.
Rejected architectural alternatives belong in ADRs; deferred or rejected product
features belong in roadmaps. Do not create an ADR for every routine choice.
## Boundary Rules
## Maintenance and verification
### Orientation
Update the owning current documentation in the same change that implements or
changes behavior. When ownership moves, remove the old definition and preserve
useful navigation links.
Link among orientation, architecture, and internal documents; maintain each
package or behavior description in its canonical owner.
### Go API And Configuration
Go declaration documentation owns exported symbol behavior. The configuration
contract owns shared YAML and resolution semantics. Consumer guidance explains
tasks and links to those contracts.
### Releases
The release policy defines requirements and invariants that every release must
satisfy. The repository-specific release procedure defines the commands and
workflow maintainers use to satisfy that policy. Release notes record history
at immutable tags. Maintain current behavior in its canonical documentation,
update it with the corresponding behavior changes, and keep release procedures
consistent with adopted policy.
### Project Licensing And Upstream Attribution
README and release documents link to the canonical licensing and attribution
materials under the [OSS policy](oss.md). The release procedure owns URL
construction and publication mechanics.
### Contracts And Implementation
Go API and configuration documents define externally observable behavior.
Internal documents explain implementation and link to the owning contract.
### Security Topics
This policy owns what documentation and examples may contain. Architecture owns
application security invariants. The configuration and public API contracts own
secret-classification, bootstrap, and rendering behavior. Internal docs own
implementation mechanisms only.
## Architecture Decision Records
Use sequentially numbered ADR filenames such as
`0001-record-architecture-decisions.md`. Follow the lightweight Nygard format:
1. title;
2. status;
3. date;
4. context;
5. decision;
6. alternatives considered;
7. consequences.
Treat the decision content of an accepted ADR as immutable. When a decision
changes, create a new ADR and update the earlier ADR's status to superseded.
Rejected architectural alternatives and significant architectural exclusions
belong in ADRs. Roadmaps preserve plausible future work, not rejected product
ideas. Routine choices and rejected suggestions need no ADR unless they
represent a significant architectural decision.
## Maintenance
When behavior changes, update its canonical owner in the same change. If
ownership moves, remove the old definition and replace it with a link where
navigation remains useful.
Before completing documentation work:
- verify claims against the implementation or clearly identify future design;
- check changed local links, paths, fragments, examples, and commands;
- preserve the distinction between policy, decisions, target behavior, and status;
- check canonical ownership and remove stale or duplicate definitions;
- inspect formatting and whitespace;
- ensure no sensitive data was introduced.
- verify affected behavior and examples;
- check commands, flags, fields, defaults, schemas, and paths against their
implementation;
- keep unimplemented behavior in the roadmap, subject to the ADR exception;
- remove stale references and validate links;
- confirm that non-owning documents summarize and link rather than redefine;
- confirm that no secrets or sensitive private data were added.

27
docs/policy/meta.md Normal file
View File

@@ -0,0 +1,27 @@
# Policy Set Provenance
**Base Policy Version:** 1.0
**Last Updated**: 2026-09-28
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/meta.md)
## Origin And Local Authority
This repository's policy set is adapted from
[Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy).
The adopted local policies govern work in this repository. Upstream revisions
become local policy through deliberate adoption, preserving local decisions.
Each policy's `Upstream Source` identifies its adopted baseline.
## Adoption And Updates
For policy adoption, upstream revision reviews, and updates, consult the
[upstream adoption and update guide](https://gitea.maximumdirect.net/eric/policy/src/branch/main/docs/adoption.md).
That guide and the reference example collection are maintained upstream and
accessed through links. Keep discovery links pointed at the current upstream
project and guide; keep adopted-source and example links pinned to commits.
The local [developer guide](../development.md) routes tasks to applicable
policies. Formal credit and licensing notices for the copied policy material
belong in the local [attribution document](../oss/attribution.md).

217
docs/policy/oss.md Normal file
View File

@@ -0,0 +1,217 @@
# Open Source Licensing And Attribution Policy
**Base Policy Version:** 1.8
**Last Updated**: 2026-09-28
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/oss.md)
## ConfigKit Applicability
ConfigKit's selected grant is LGPL-3.0-or-later, recorded in
[COPYRIGHT](../../COPYRIGHT), with the complete LGPLv3 and incorporated GPLv3
texts in [LICENSE](../../LICENSE). The [release procedure](../release.md)
owns source-only Go module publication through Gitea. No separate hosted release
object or license asset is required for this channel.
## Purpose And Scope
This policy defines project licensing conventions and a repeatable House Format
for upstream attribution and license notices. It covers the project's own code
and upstream components incorporated into or distributed with the application
or library, including relevant transitive dependencies and copied code,
documentation, or assets.
Determine treatment from the component's terms and actual use.
Consulting development guidance or using tooling alone does not require an
attribution entry. Copying or adapting documentation for inclusion in the
repository is different: preserve its applicable attribution and license terms.
A material's role as development guidance does not exempt copied documents.
Apply any express permissions for attribution-free reuse of example formatting.
The baseline is a publicly available source repository with releases published
alongside it. The format is independent of language, forge, and build system.
It does not require attribution for every development tool, establish license
compatibility, or resolve every obligation arising from use or distribution.
## Canonical Locations
The [documentation policy](documentation.md) assigns ownership:
- `COPYRIGHT`: project name, copyright statements, SPDX expression, grant,
warranty disclaimer, and third-party statement.
- `LICENSE` contains the complete, unmodified project license text, including
incorporated companion text. Dependency attribution does not relicense the project.
- `docs/oss/attribution.md`: canonical component entries.
- `docs/oss/licenses/`: complete, verbatim standard and applicable companion texts.
Use recognizable license filenames, such as `Apache-2.0.txt` or `LGPL-3.0.txt`.
A shared standard text may serve multiple entries. Keep explanations outside
license texts and preserve distinct versions and exceptions.
## Upstream Reference Examples
Use the commit-pinned upstream examples to create actual project notices.
Retain their links and keep the example collection upstream; keep applicable
license texts locally as specified above. The
[standard license-text collection](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/oss/licenses/README.md)
identifies the available reference texts.
## Project Licensing
Unless the author specifies otherwise, use **GPL version 3 or later**
(`GPL-3.0-or-later`) for applications and **LGPL version 3 or later**
(`LGPL-3.0-or-later`) for libraries. An author's explicit choice takes precedence:
for example, a one-off script may use MIT. Such a choice requires no exception
rationale, additional approval, or compensating controls under this policy.
These defaults guide new license selection. Adopting this policy does not
relicense existing code or override third-party terms. Preserve an existing
license unless the author deliberately changes it. For mixed-purpose projects,
follow the author's selected scope rather than inferring different licenses for
individual packages.
Use the upstream [application COPYRIGHT example](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/examples/licensing/application/COPYRIGHT)
or [library COPYRIGHT example](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/examples/licensing/library/COPYRIGHT)
to create the project’s root `COPYRIGHT` file.
Use verified project names, contribution years, and copyright holders.
For another selected license, adapt the grant, SPDX expression, disclaimer, and
text references together to reflect the author's selected grant.
For GPLv3 projects, copy the complete GPLv3 text into `LICENSE`. For LGPLv3
projects, place the complete LGPLv3 text followed by the complete GPLv3 text in
`LICENSE`; the LGPL incorporates the GPL terms. Keep the texts unmodified and
place project-specific statements in `COPYRIGHT`. For the default grants, the
grant and SPDX expression in `COPYRIGHT` must expressly identify `or-later`;
follow the author’s choice if a different grant is selected.
For other licenses, provide their complete text and any required project-specific
fields; the standard reference collection remains unchanged. Keep `COPYRIGHT`,
`LICENSE`, SPDX expressions, and README descriptions consistent with the actual
grant. This convention introduces no blanket requirement for per-file headers;
preserve notices or add them where the applicable terms require them.
Identify separately licensed documentation or assets in the project grant, with
links to their attribution and license information. An application's default
code license does not override the terms of incorporated documentation.
## Upstream Attribution House Format
Begin `docs/oss/attribution.md` with `# Open Source Attribution Notices`,
followed directly by component entries. These comprise the complete document.
Each entry presents these five fields in order, with blank lines between them:
1. **Project name** as a level-two heading. An optional identification paragraph
may follow, such as `Selected module: MODULE at VERSION.` with the module and
version in backticks. For copied documentation, a concise `Material:` sentence
may identify the affected files and whether they were adapted or copied
without modification. A factual qualifier such as `(transitive dependency)`
is permitted. Limit optional identification prose to factual component and
material scope.
2. **Upstream copyright statement or statements**, preserving their wording.
Put multiple statements in separate paragraphs. Include this field when the
selected upstream source supplies an applicable statement; otherwise, omit it.
3. **SPDX license identifier or expression**, when applicable, using
`SPDX-License-Identifier: EXPRESSION`.
4. **License and notice links**, beginning `License and notices:`, separated
by ` | `, and ending with a period. Label local standard texts by license
name, such as `MIT` or `Apache 2.0`. Label upstream files by their actual
filenames, such as `COPYRIGHT`, `LICENSE`, `NOTICE`, or `PATENTS`.
5. **Source repository link**, rendered exactly as `[Source](COMMIT_URL).`.
Use the upstream [attribution example](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/examples/licensing/attribution.md)
to create the project’s `docs/oss/attribution.md`.
The example illustrates omitted fields and multiple copyrights and licenses.
Replace the uppercase placeholders and illustrative URLs with actual component
information. Local license links are relative to the destination
`docs/oss/attribution.md`; copy the applicable texts into its `licenses/`
directory. Do not copy example entries for components the project does not use.
These are five fields, not a line limit. Omit the SPDX field when no applicable
identifier exists; identify nonstandard terms through the linked text. Preserve
licensing alternatives and exceptions, and record a selected option when
relevant through the factual license identification. Do not invent an identifier
or silently resolve an unsettled licensing choice.
Keep entries factual. Do not add agent-authored explanations of license
expressions, file-level license scope, research performed, retained source
notices, or compliance conclusions. Avoid filler such as "none is invented
here" and embellished link labels such as "upstream LICENSE", "additional
patent grant", or "at the imported commit". Preserve actual required upstream
content; this rule excludes editorial additions, not required notices. Policy
interpretation and exception reasoning belong in the adopted OSS policy or an
appropriate decision record, not the attribution document.
## License Texts And Upstream Links
Link standard license texts to their local copies. Link project-specific
notices to their upstream copies. Every upstream repository or notice link must
identify the exact commit corresponding to the
imported version, including the selected source revision for copied material.
Classify material according to its contents. An upstream `LICENSE`,
`COPYING`, or similar file may contain project-specific notices or exceptions
in addition to standard wording. Link both the local standard text and the
commit-specific upstream file when needed. Make companion license texts directly
reachable from the entry.
For project-specific license and attribution material, this policy accepts
direct links to the applicable upstream texts as satisfying reproduction,
inclusion, or copy requirements. This is the adopted house interpretation.
Agents must apply it without adding local-copy requirements or seeking repeated
approval solely because a license uses those terms.
This interpretation concerns notice reproduction. Concrete obligations such as
marking modified files or supplying corresponding source receive their own
applicable treatment. Preserve required notices already present in incorporated
source files; the central attribution entry does not authorize stripping them.
## Discoverability
End the project's README with two friendly statements linking to its actual
grant in `COPYRIGHT` and the attribution document. ConfigKit's
[README](../../README.md) contains the two required statements: one linking to
the project grant and one linking to attribution.
Each new checked-in release document and its hosted download page must end
with links whose visible text is `COPYRIGHT | LICENSE | ATTRIBUTION`. Use absolute
URLs to those files at the release's immutable tag or corresponding immutable source
revision so the links work in both locations. This release navigation is distinct
from upstream component links, which must use the imported commit.
Reuse the checked-in release document as the hosted release body where supported.
The release procedure owns concrete URLs and publication mechanics for delivering
the links through the selected channel. Do not maintain duplicate notice lists.
These conventions do not by themselves require separate downloadable license
assets, source bundles, or a different binary package format.
## Maintenance And Exceptions
Update project licensing statements together when the author changes the grant
or its scope. Update upstream notices in the change that adds, removes, upgrades,
copies, or changes the distribution of a relevant component. Check the selected source's license
and notices rather than assuming they are unchanged. Update affected entries,
commit links, and texts; retain historical versions through Git.
Check affected statements, faithful standard-text copies, and link destinations
as part of ordinary change review. For a future release tag, check intended file
paths against the candidate source; the tag need not resolve remotely before
publication. Do not add publication-time network checks, CI polling, recurring
legal sign-offs, mandatory scanners, or a full software bill of materials merely
to implement this policy.
Use the House Format routinely. Raise an exception only for a concrete obligation
or unresolved rights question it does not address, including any arising from a
particular distribution channel. Record the issue and selected treatment in the
adopted OSS policy or an appropriate decision record. Attribution entries
contain the resulting factual identification and required material, without
repeating that reasoning. Resolve the issue before the affected use or
distribution without reopening settled choices.
Common ownership of projects is neither required nor a blanket exception.
Apply the formatting convention prospectively. Do not rewrite published tags,
notes, binaries, or checksums to retrofit it. Address actual omissions deliberately
under the applicable terms and the [release correction policy](release.md#immutability-and-correction).
Ordinary attribution maintenance and release links are preparation work, not new
release approval gates.

View File

@@ -1,78 +1,108 @@
# Release Policy
**Base Policy Version:** 0.1.0
**Base Policy Version:** 1.6
**Last Updated:** 2026-09-06
**Last Updated**: 2026-10-09
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md)
## Purpose
This policy defines the minimum requirements for selecting, validating,
publishing, verifying, and correcting releases. Its goal is to make every
release intentional, reproducible from an exact source revision, documented
for consumers, and safe from retrospective replacement.
This policy defines a proportionate workflow for validating and publishing an
intended source revision under a fresh version. Releases should require little
routine intervention, avoid repeated work, and preserve published contents.
The repository-specific commands that implement these requirements belong in
`docs/release.md`. Historical release summaries belong in `docs/releases/`.
Neither document may weaken this policy.
Adopt stronger verification or reproducibility requirements through an explicit
project-specific decision justified by the intended audience and support needs.
## ConfigKit applicability
The repository-specific commands belong in `docs/release.md`. Historical release
summaries belong in `docs/releases/`. The procedure must agree with the adopted
policy and link to it rather than introduce additional release requirements.
ConfigKit adopts a source-only Go module release model. The source tag and
version-matched release note are the declared deliverables. Standalone binaries,
container images, and separate binary/package-registry publication are not
required. This does not waive source compilation, module hygiene, testing, or
consumer-facing module retrieval and build verification.
## ConfigKit Applicability
The release procedure must define the module path, authoritative remote,
supported Go versions and operating-system/architecture targets, prerequisite
dependency acquisition, and the exact verification commands once established.
The default tests themselves remain deterministic and offline. Additional
publication channels require documented deliverables and verification before use.
ConfigKit publishes a source-only Go module. Its declared deliverables are the
immutable annotated source tag and version-matched checked-in release note.
Standalone binaries, container images, hosted release objects, and separate
registry publication are not required. Additional channels require a deliberate
update to the policy and procedure.
The Go API, documented YAML/resolution semantics, and structured diagnostic
contracts all participate in compatibility review. A signature-preserving change
The Go API, documented YAML and resolution semantics, and structured diagnostic
contracts participate in compatibility review. A signature-preserving change
can still break consumers. Human diagnostic wording is stable only where a
contract expressly promises it. Minimum Go version and platform support changes
also require an explicit compatibility assessment.
contract expressly promises it. Minimum Go version and platform support
changes also require an explicit compatibility assessment.
## Release Model
### Platform Support
A release consists of:
Linux is the release-validated platform. macOS has best-effort support: keeping
it working remains desirable, but neither native macOS validation nor Darwin
cross-builds are release gates. Missing macOS evidence or a macOS-specific failure
does not by itself block a release. Voluntary checks and fixes are welcome; there
is no guarantee that each release has been validated on macOS.
- one exact source commit reachable from the repository's primary branch;
- one immutable version tag identifying that commit;
- one version-matched, checked-in release note; and
- every artifact, image, package, registry entry, or other deliverable the
repository declares for that release.
The [release procedure](../release.md#release-identity-and-support) owns target
architectures and required Linux checks. Historical release notes and their
validation records remain unchanged.
The repository's primary branch is `main` unless an adopted policy documents
an exception. A release candidate must be committed and pushed to that branch
before its release tag is published.
## Release Model And Completion
The source tag is the authoritative release identity. Published deliverables
must be traceable to the tagged source and must agree with its version. A
repository may publish source only. When it publishes additional deliverables,
its release procedure must enumerate them and define how each is built,
identified, published, and verified.
A source release identifies one exact committed source revision through an
immutable version tag and a version-matched, checked-in release note. The
primary release branch is `main` unless the adopted policy specifies otherwise.
The procedure must name the authoritative publication remote.
Release automation may validate or publish a release only when its exact
responsibility is documented. Automation must not silently change the source
candidate, move a published tag, replace a published deliverable, or repair a
release in place.
A successful push of the intended tag, with its source commit available on the
authoritative primary branch, completes the release. The source commit may be
pushed before the tag or published together with it through an atomic push.
The procedure must choose and document the supported approach.
ConfigKit uses sequential publication: first push the committed candidate to
authoritative `main`, then invoke ReleaseKit once to validate that candidate and
publish its annotated tag. The [release procedure](../release.md) names
`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
or poll CI, require successful CI status, inspect downloadable artifacts, or
check consumer installation or Go module availability as completion gates.
A failed or ambiguous push requires resolving the authoritative remote state
before declaring success or retrying; successful pushes do not require a
separate remote read-back gate.
Remote tag read-back and fresh consumer retrieval are optional and non-gating
after successful publication. A failed push receives one bounded check of the
authoritative ref; do not poll or blindly retry.
Repositories may publish source only. Additional binaries, images, packages,
checksums, or other declared deliverables may be published asynchronously from
the tagged source. The procedure must identify those deliverables and the
responsibility of their automation. Published deliverables must be traceable
to that source and identify the same version.
ConfigKit currently declares no additional deliverables. Confirmed publication
of the intended source tag is the completion point. CI, macOS validation,
hosted delivery, and consumer retrieval are outside the release gates.
## Versioning And Compatibility
Stable releases use Semantic Versioning in the form `vMAJOR.MINOR.PATCH`, with
no leading zeroes. Each version must be unused locally and in every configured
remote or publication channel before publication begins.
no leading zeroes. Prereleases require an explicit adopted policy defining
their format and use.
Unless an adopted policy explicitly supports and defines prereleases,
prerelease versions are not permitted.
ConfigKit does not support prereleases.
The selected version must be unused locally and on the authoritative remote.
Do not require queries to every configured remote or publication service.
A version known to have already been published must not be reused, even if the
normal guards do not inspect that channel. Adding a distribution channel
requires a deliberate update to the policy and procedure where needed.
For releases at or after `v1.0.0`:
- 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.
Before `v1.0.0`, a minor release may change a documented public, operational,
@@ -80,145 +110,220 @@ configuration, data, integration, or compatibility contract when the release
note explains the impact and required consumer action. A patch release must
not intentionally break those contracts within its minor line.
A repository with additional compatibility promises must define them in its
adopted policy or canonical contract documentation. A release note may explain
a compatibility change, but it must not redefine the current compatibility
contract.
Additional compatibility promises and deliberate exceptions belong in the
adopted policy or canonical contract documentation. Release notes explain
changes; they do not redefine current compatibility contracts.
## Release Documentation
## Consumer Minimum And Release Toolchain
Every release made under this policy must include
`docs/releases/<version>.md` in the tagged source tree. The filename must use
the complete version tag, and the document must begin with a title containing
the product name and that exact version.
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.
Every new release note must contain these sections:
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.
```markdown
# <Product> vMAJOR.MINOR.PATCH
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.
This release ...
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.
## Summary
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.
## Compatibility
### ConfigKit Toolchain Selection
## Upgrade
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.
## Changes
```
## Release Documentation And Preparation
Use an explicit statement such as "No action required" when a section has no
substantive content.
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
title containing the product name and that exact version.
Release notes are immutable historical summaries. They should link to current
canonical documentation for exact behavior. Every affected canonical document
must be updated in the same candidate; a release note must not substitute for
current documentation or defer required documentation changes.
ConfigKit requires the first line `# ConfigKit <version>`. ReleaseKit's
publication guard checks the release-note H1 for the product and version.
## Candidate Preparation And Validation
Include a short summary and any relevant compatibility impact or upgrade
instructions. Choose sections suited to the release and keep the note
proportionate to the change.
Each repository must provide one documented release-check command or script
that exercises the complete set of automated source-candidate gates. The
repository-specific procedure must state all additional manual gates.
Keep exact current contracts in their canonical documents and update affected
documentation with the behavior change. Link to those documents from the note
where useful. If annotated tags are used, derive their summary from the
checked-in note.
The required gates are:
ConfigKit uses the complete checked-in note as the annotated tag message.
- dependency or module hygiene appropriate to the language and ecosystem;
- the complete deterministic, offline default test suite;
- race, concurrency, or equivalent safety checks where the implementation can
be meaningfully checked for those risks;
- static analysis and linting used by the repository;
- production builds for every supported target that can be built in the
release environment;
- formatting and whitespace checks;
- validation of maintained examples, configuration, schemas, generated
sources, and documentation commands where applicable;
- review of changed local documentation links;
- removal of obsolete roadmap documents under `docs/roadmap/` that describe
completed work included in the release;
- review for unintended files, generated output, credentials, private data,
and unrelated changes; and
- any additional security, compatibility, migration, packaging, or platform
checks required by the product.
Include the release footer defined by the [OSS policy](oss.md#discoverability)
in each new release document. The release procedure defines the concrete URLs
and publication mechanics. Prepare these links with the release note; they do
not add a network check or approval gate. If a hosted download page is used in
a future channel, preserve the footer there.
Unavailable tooling or an inapplicable check must be addressed explicitly in
the adopted policy or release procedure; it must not disappear silently from
the candidate gates.
Handle version selection, release-note content, and retirement of completed
roadmaps during preparation. Preserve planned and deferred work and repair
affected links when retiring roadmaps. Reuse ordinary change review for
unintended files, credentials, and private data. Scope release-content review
to the tagged source.
Run the complete checker while preparing the candidate. Commit the release
note and all associated code, tests, documentation, and configuration, then
run the complete checker again against the exact commit intended for release.
Do not publish a release from uncommitted or unvalidated content.
## Candidate Validation
Provide one publication command that validates the final committed candidate,
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
before publication repeats the same checks.
ConfigKit uses the module-selected ReleaseKit CLI's `publish
vMAJOR.MINOR.PATCH` command. Its `check` command is optional for diagnosis; it
is not a required preliminary pass to publication.
Run the complete deterministic, offline default test suite once for the final
candidate, following the [testing policy](testing.md). For repositories that
publish builds, verify the required build targets. Select those targets for
the application's actual support commitments; native testing on each platform
and access to particular hardware are not default release prerequisites.
Prepare dependencies and build the tool before offline validation; the
publication command is the required validation run, with no separate successful
test or CI run required before it.
The selected profile runs the complete deterministic, offline default suite
once with the race detector and test-time vet disabled. The suite already
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
adequately cover. Do not automatically require linting, race testing, module
hygiene audits, documentation audits, credential scans, example validation, or
expanded platform matrices solely because they are possible. Retain such
checks when their distinct protection justifies their cost. A one-time migration
check must not silently become a permanent release requirement.
ReleaseKit runs its selected Go commands with readonly module mode and
`GOPROXY=off`; acquire dependencies and build the tool before candidate
validation. Formatting and whitespace remain ordinary development checks in
`scripts/check.sh`.
Review module tidiness when dependencies change, not as a release gate.
Release tests must not require real credentials, mutable application services,
paid APIs, provider probes, or inference calls. The procedure may describe dependency
preparation separately from offline validation.
Avoid duplicate validation across release phases. Successful checks for the
same final candidate need not be repeated solely to satisfy another phase;
any reuse must clearly apply to that exact candidate. Persistent validation
caches are not required. If the candidate changes, validate the new candidate.
Cheap publication-state checks may be repeated immediately before pushing
because local or remote state may have changed during validation.
Automate mechanical guards. Human review gates are strongly disfavored: add one
only through an explicit project-specific policy decision explaining the
concrete risk and why ordinary change review or automation is insufficient.
Do not add routine confirmations for checks the command can perform itself.
Compatibility review, release-note accuracy, completed-roadmap retirement, and
review for unintended files or credentials remain ordinary preparation. The
executing agent may perform them without routine human approval or separate
manual evidence records. Publication itself requires authorization.
## Publication Guards
Immediately before publication, the maintainer or release automation must
fetch the current primary branch and tags from the authoritative remote and
verify all of the following:
Automate the checks needed to publish the intended source under a fresh version:
- the version has the required format and is not already published;
- the current branch is the primary release branch;
- the worktree and index are clean;
- the current commit is the recorded, validated candidate;
- the authoritative remote branch points to that same commit;
- the matching release note exists in that commit and has the required title
and sections;
- all required approvals and automated gates succeeded for that commit; and
- no local or remote tag already uses the selected version.
- the version has the adopted format and is unused locally and on the
authoritative remote;
- the worktree and index are clean on the primary release branch;
- the source being published is the exact candidate that passed validation,
including its matching release note H1;
- the remote primary branch can be advanced to that candidate without
discarding remote history, and is published before the tag; and
- the tag identifies the validated candidate and only the intended refs are
published.
Create the version tag against the guarded commit and verify its target before
publishing it. Publish only the exact intended ref and deliverables. Broad
operations such as `git push --tags` are discouraged because they can publish
unreviewed refs unrelated to the release.
ReleaseKit checks that `origin`'s effective push URL exactly matches the
authoritative URL before candidate validation. This does not assert the
configured fetch URL or require the release-note H1 to be the first line; the
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.
The repository-specific procedure must make these guards executable or give
copyable commands for performing them.
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
refs rather than all local tags. Do not require CI success, publication-service
availability checks, or a separate manual confirmation for each guard.
## Immutability And Correction
Published version tags and published deliverables are immutable. Do not move,
overwrite, recreate, or delete a published tag. Do not replace an artifact,
image, package, checksum, signature, provenance statement, or release note
under an already published version.
Published version tags and published contents are immutable. Do not move,
overwrite, recreate, or delete a published tag, or replace a published artifact,
checksum, signature, provenance statement, or release note under that version.
The checked-in note at the immutable tag is that release's historical record.
If validation fails before publication, correct the candidate, rerun the
complete gates, and repeat the publication guards. An unpublished local tag
may be removed after confirming that it was never published or used by a
release channel.
If validation fails before publication, correct and commit the candidate and
rerun the publication command. If a push fails or its outcome is ambiguous,
inspect the authoritative remote before retrying. Remove an unpublished local
tag only after confirming it was never published or used by a release channel.
An unavailable remote leaves publication status unknown; do not overwrite a tag
to resolve uncertainty.
If publication or post-publication verification reveals a defect, leave the
published release intact. Correct the defect on the primary branch, choose a
new patch version or other semantically appropriate new version, write a new
matching release note, and perform the complete release procedure again.
Transient network, authentication, CI, or consumer-availability failures do not
by themselves require a new version.
A distribution channel's supported yank, deprecation, or advisory mechanism
may warn consumers away from a defective release, but it must not replace that
release's contents or reuse its version.
A defect requiring changes to published source or contents requires a corrected
commit, a new semantically appropriate version, a matching release note, and
the release procedure again. Leave the original release intact. A channel's
yank, deprecation, or advisory mechanism may warn consumers away from a defective
release without replacing its contents or reusing its version.
## Repository Release Procedure
Every adopting repository must maintain `docs/release.md` as the canonical
maintainer runbook for implementing this policy. It must define, in execution
order:
Maintain `docs/release.md` as a short, executable maintainer runbook. It must
identify prerequisites, the selected release toolchain, the primary branch and
authoritative remote, version and note preparation, one publication command with
local checks and guards, source-only deliverables, the successful-tag-push
completion point, and partial or unknown-state recovery.
1. maintainer prerequisites and required access;
2. version selection and compatibility review;
3. release-note creation;
4. candidate preparation and the complete validation command;
5. candidate commit, push, and exact commit recording;
6. publication guards;
7. exact tag and deliverable publication commands;
8. authoritative remote and consumer-facing verification; and
9. failure handling and correction.
The procedure should use copyable, fail-fast commands where practical. It must
name the primary branch, authoritative remote, tag form, published
deliverables, supported build targets, release automation responsibilities,
and consumer verification path. It must link to this policy instead of
restating or redefining its requirements.
Use copyable commands where practical. Keep optional diagnostics and consumer
verification clearly optional. The procedure must not turn preparation work,
ordinary change review, or post-publication investigation into additional
release gates.
## Policy Exceptions
None. Candidate selection, publication, and post-publication verification remain
mandatory steps in the [release procedure](../release.md), not policy exceptions.
The following historical exceptions do not change the current release workflow.
For v0.1.1 only, the maintainer approved renaming the reserved `_configkit` key to
`configkit` as an error-correcting point release because there are no current
consumers. This intentionally changes the v0.1.0 configuration contract without a
minor-version increment. The release note must explain the renamed directive,
the lack of a legacy alias, and the newly reserved application key. This exception
does not apply to subsequent releases or waive any validation or publication gate.
For v0.2.0 only, the maintainer accepted the successful native macOS check at
`244104f` for the final candidate, provided subsequent changes are documentation
only. The final candidate still requires the complete Linux release checker.
This exception waives only repeating the macOS check after those documentation
changes; it does not waive other publication or verification gates.

View File

@@ -1,14 +1,16 @@
# Testing Policy
**Base Policy Version:** 1.0
**Base Policy Version:** 1.3
**Last Updated:** 2026-09-06
**Last Updated**: 2026-09-28
**Upstream Source:** [Standard Repository Policies](https://gitea.maximumdirect.net/eric/policy/src/commit/d84a38ebab26f048cafbe59a2246700c35d29843/docs/policy/testing.md)
## Purpose
Our tests exist to make **incorrect changes expensive and correct changes cheap**.
We do not optimize for test count, line coverage, exhaustive isolation, or the fewest possible tests. We optimize for sufficient confidence in important behavior while imposing as little unnecessary friction as possible on future development.
We optimize for sufficient confidence in important behavior while minimizing friction on future development.
## ConfigKit-specific priorities
@@ -44,32 +46,19 @@ policy does not imply that those tools or tests already exist.
## Every test has a cost
Testing is not an unqualified good. Every test imposes both an immediate cost and a continuing lifetime cost.
A test must be:
- written and reviewed;
- understood by future maintainers and coding agents;
- executed in local and CI workflows;
- diagnosed when it fails;
- updated when legitimate behavior changes;
- maintained as fixtures, APIs, and dependencies evolve; and
- removed or rewritten when it becomes redundant, brittle, misleading, or obsolete.
Tests also create cognitive and architectural friction. They can constrain refactoring, duplicate policy, slow feedback loops, add noise to failures, and cause harmless implementation changes to require unrelated edits across the suite.
Every test has an immediate and continuing cost: writing, review, execution,
diagnosis, fixture maintenance, and updates when legitimate behavior changes.
Tests can also constrain refactoring, duplicate policy, and add failure noise.
A test is warranted only when the confidence it provides justifies these costs.
Apply this cost-benefit analysis at two levels:
Evaluate that tradeoff at two levels:
1. **Per test:** What realistic defect does this test detect, how consequential would that defect be, and is that protection worth the test's lifetime cost?
2. **Across the suite:** Does this collection provide materially more confidence than a smaller, simpler suite would?
The preferred test suite is a **lean suite that provides sufficient confidence in the risks that matter, without redundant or low-value tests**. We seek sufficient confidence with the least unnecessary testing friction, not the fewest possible tests.
Some friction is intentional. Tests should make dangerous changes—such as breaking compatibility, corrupting data, violating security boundaries, or reintroducing subtle bugs—require deliberate review. Keep ordinary internal changes inexpensive.
Some friction is intentional. Tests should make dangerous changes—such as breaking compatibility, corrupting data, violating security boundaries, or reintroducing subtle bugs—require deliberate review. They should not make ordinary internal changes needlessly expensive.
The cost of a test is not a reason to omit testing by default. Do not cite maintenance cost abstractly. When omitting a plausible test, be able to state why the protected failure is low-risk, already covered, obvious, reversible, or cheaper to detect elsewhere. For consequential, subtle, or difficult-to-observe behavior, the presumption should favor testing.
Evaluate maintenance cost against the specific failure being protected. When omitting a plausible test, be able to state why the protected failure is low-risk, already covered, obvious, reversible, or cheaper to detect elsewhere. For consequential, subtle, or difficult-to-observe behavior, the presumption should favor testing.
## Default testing style
@@ -81,6 +70,8 @@ Use a **classical/Detroit-style** approach:
- Prefer package-level behavioral tests over tests coupled to private helpers or internal call sequences.
- Treat exact collaborator interactions as testable behavior only when the interaction itself is a requirement.
Examples of appropriate seams include clocks, randomness, subprocesses, remote APIs, object storage, email, and paid LLM calls.
For ConfigKit, controlled input readers and filesystem failures are useful seams.
Prefer actual parsing, composition, and decoding over mocks of their call order.
@@ -92,7 +83,7 @@ Control clocks, randomness, environment variables, and other process-global or m
## What deserves tests
Prioritize tests for:
To the extent the following features are present in the application, prioritize tests for:
1. Public and package-level contracts.
2. Domain rules and important invariants.
@@ -102,15 +93,27 @@ Prioritize tests for:
6. Previously observed or plausible regressions.
7. Representative integration and end-to-end workflows.
A package-level contract is behavior relied upon by another package or major collaborator, not every observable detail of a package implementation.
A package-level contract comprises behavior relied upon by another package or major collaborator.
For behavior involving **data integrity, destructive operations, compatibility, security, concurrency, idempotency, or recovery**, presume that durable tests are required unless the behavior is already credibly protected at another layer.
Choose tests for requested behavior and established contracts. Tests must not
introduce new restrictions, guarantees, or threat scenarios merely because they
could be asserted. Follow the [agent policy](agents.md) when deciding whether a
behavior belongs to the contract; current implementation behavior alone is not
sufficient authority.
For established behavior involving **data integrity, destructive operations,
compatibility, security, concurrency, idempotency, or recovery**, favor durable
tests for realistic failures whose consequences warrant protection. Identify
the failure and the existing contract it would violate. These categories call
for attention, not an automatic test inventory or permission to strengthen the
application's guarantees. Account for protection already provided at other layers.
Do not add tests merely because a function, branch, or line exists. Do not add a test when the same meaningful risk is already adequately protected elsewhere.
## Choose the right test boundary
Test through the narrowest stable boundary that expresses the behavior clearly.
Choose the narrowest stable boundary that expresses the behavior clearly and
provides durable confidence with minimal incidental coupling.
This is often the package API, but it may instead be:
@@ -118,28 +121,15 @@ This is often the package API, but it may instead be:
- a package-level operation when several internal collaborators jointly produce the behavior; or
- a larger integration boundary when correctness emerges from interaction with a real dependency.
Do not force all behavior through oversized end-to-end tests. Do not test every private helper merely because it exists. Choose the boundary that gives durable confidence with the least incidental coupling.
## Test behavior, not implementation
A test should protect a decision, contract, or invariant—not memorialize the current implementation.
A test should protect a selected decision, contract, or invariant.
Before adding or retaining a test, ask:
A test is suspect when it primarily detects changes to private constants or
helpers, equivalent operation ordering, incidental formatting, equivalent
algorithms, or internal structure that preserves behavior.
> What realistic defect would this test catch?
A test is suspect when its main purpose is to detect that someone:
- changed an internal constant;
- renamed or split a private helper;
- reordered equivalent internal operations;
- changed incidental formatting;
- replaced one correct algorithm with another; or
- refactored internal object structure without changing behavior.
Refactoring should normally require no test edits unless the refactored structure is itself part of the contract.
A test can be factually correct and still have negative value. Accurately describing current behavior is not enough; the protected behavior must be important enough to justify the future friction.
Behavioral tests should normally remain stable through internal refactoring that preserves the contract.
## Expected effects of different changes
@@ -156,12 +146,13 @@ A test failing is not the same as a test needing to be edited. Many tests may co
## Separate mechanism from policy
Configurable thresholds and defaults must not be duplicated throughout the test suite.
Derive expectations from test-controlled configuration and relationships.
For example, do not encode an internal concurrency limit indirectly:
For this example, assume the selected contract rejects a request to start more
processes than a configured limit. Do not encode its production default indirectly:
```go
// Production policy:
// Production default:
const maxConcurrency = 4
// Brittle test:
@@ -179,25 +170,30 @@ require.NoError(t, runner.Start(limit))
require.ErrorIs(t, runner.Start(limit+1), ErrTooMuchConcurrency)
```
The test should prove:
For this selected contract, the test should prove:
- the configured limit is accepted; and
- one beyond the configured limit is rejected.
Rejection is not a general requirement for concurrency control. If the selected
contract queues excess work or limits active workers while accepting larger
batches, test that behavior instead. Do not add rejection or a configurable
limit solely to reproduce this example.
The production default should be tested exactly only when its literal value is itself a public, operational, safety, protocol, or compatibility requirement.
Apply the same rule to limits, timeouts, capacities, retry counts, and ranges: test relationships and behavior, not duplicated literals.
Apply the same rule to limits, timeouts, capacities, retry counts, and ranges: test relationships and behavior.
For concurrency limits, test both kinds of behavior when relevant:
For established concurrency limits, distinguish these behaviors where the contract requires them:
1. **Configuration enforcement:** invalid or excessive requested values are handled correctly.
1. **Configuration handling:** values are accepted or rejected according to the selected contract.
2. **Runtime enforcement:** observed peak concurrency never exceeds the configured limit.
Use a test-controlled limit and measure the behavior relative to that limit. Do not merely assert today's default value.
Use a test-controlled limit and measure the behavior relative to that limit.
## Avoid semantic duplication across layers
## Assign each behavior a test owner
Each behavior should have a clear test owner.
For ConfigKit, assign ownership as follows:
- YAML boundary tests own syntax and policy enforcement.
- Composition and overlay tests own their operation semantics.
@@ -209,9 +205,7 @@ Each behavior should have a clear test owner.
These are responsibility boundaries, not a required package layout.
Higher-level tests should not repeat every lower-level case. A single intentional policy change should not require unrelated edits across many test files.
Tests that are individually reasonable may still be collectively redundant. Evaluate the marginal value of each additional test in light of the protection already provided by the rest of the suite.
Give each layer its distinct responsibility and keep expectation updates localized to the contract being changed.
## Use test doubles deliberately
@@ -232,104 +226,77 @@ Mocks are appropriate when the contract includes facts such as:
- an expensive API is called no more than once; or
- a security audit event is emitted.
Do not use mocks merely to isolate every object or reproduce the implementation's call graph.
## Go-specific guidance
Use:
Select techniques by behavior and risk:
- table-driven tests for meaningful behavioral categories and boundaries;
- `t.TempDir()` for real filesystem behavior;
- fuzz tests for parsers, normalization, path handling, and broad input spaces;
- `httptest.Server` for realistic HTTP interactions;
- fuzz tests when exploring broad input spaces, such as parsers or path handling, adds useful protection beyond representative cases;
- golden files only when the complete output is intentionally stable;
- integration tests where correctness depends on component interaction; and
- a small number of representative end-to-end tests.
- a small number of representative end-to-end tests when assembled workflows expose meaningful risks not adequately covered at narrower boundaries.
Avoid exact error-string assertions unless the wording is itself contractual. Prefer `errors.Is`, `errors.As`, typed errors, or structured error fields.
Prefer `errors.Is`, `errors.As`, typed errors, or structured error fields. Use exact error-wording assertions only when the wording is contractual.
At the public library boundary, prefer diagnostic codes, paths, and source
locations over complete message snapshots. Assert wording only when contractual.
Golden-file updates must require an explicit local flag. CI must not update golden files automatically, and reviewers must inspect the semantic diff before accepting an update.
Keep tests readable and direct. Test helpers and fixture frameworks must earn their own maintenance cost; do not build elaborate test infrastructure for small or isolated needs.
Keep tests readable and direct. Size test helpers and fixture frameworks to the maintenance benefit they provide.
## Coverage
Coverage is a diagnostic, not a target.
Use coverage to locate untested critical branches and unexpectedly weak packages. Evaluate test quality through meaningful behavioral protection and choose coverage according to risk.
Use it to find untested critical branches and unexpectedly weak packages. Do not write low-value tests solely to increase a percentage, and do not infer test quality from coverage alone.
Dense configuration semantics will often warrant higher coverage than simple input wiring. Uneven coverage is acceptable when it reflects risk.
Increasing coverage is valuable only when the newly covered behavior protects a meaningful risk at an acceptable cost.
Pure domain logic will often warrant higher coverage than CLI wiring or external adapters. Uneven coverage is acceptable when it reflects risk.
## Regression tests
A bug fix should normally include a regression test that fails before the fix and passes afterward.
Retain the test when the defect could realistically recur and its consequences justify the ongoing cost. Prefer the narrowest durable test of the violated contract or invariant; do not preserve accidental implementation details from the original bug.
Retain the test when the defect could realistically recur and its consequences justify the ongoing cost. Prefer the narrowest durable test of the violated contract or invariant.
Not every historical bug requires a permanent test. If the underlying design has made recurrence impossible, the test has become redundant, or a stronger invariant test now subsumes it, remove or consolidate it.
Remove or consolidate a test when the underlying design has made recurrence impossible, its protection is redundant, or a stronger invariant test subsumes it.
## Deleting or rewriting tests
Tests are maintained code, not permanent historical artifacts.
Delete or rewrite a test when its maintenance cost exceeds the confidence it provides.
Delete or rewrite tests when maintenance cost exceeds their continuing
protective value.
Strong candidates include tests that:
- require updates after harmless internal changes;
- directly assert private constants without protecting a real contract;
- duplicate the same policy across several layers;
- assert incidental private constants or snapshot large amounts of incidental output;
- verify mock choreography rather than outcomes;
- snapshot large amounts of incidental output;
- test trivial private helpers already exercised through stable package behavior;
- protect risks already covered more effectively elsewhere;
- duplicate policy across layers, retest trivial helpers covered by package
behavior, or protect risks covered more effectively elsewhere;
- are flaky, misleading, obsolete, or disproportionately expensive to diagnose; or
- no longer correspond to a plausible failure mode.
Several brittle tests may encode one genuine requirement. Replace them with one durable behavior-level or invariant test rather than preserving all of them.
Deleting a low-value test can improve the quality of the suite by reducing noise, maintenance burden, and friction around legitimate change.
Several brittle tests may encode one genuine requirement. Consolidate them into one durable behavior-level or invariant test.
## Reviewing a proposed test
Use the following questions when the value, boundary, or durability of a proposed test is not self-evident. Significant test additions should be reviewable against them, but written answers are not required for every routine test.
Use the following questions when a proposed test needs closer evaluation. Significant test additions should be reviewable against them; written answers are optional for routine tests.
1. What realistic defect would it catch?
2. How likely is that defect?
3. How consequential would it be?
4. Is the behavior already protected elsewhere?
5. At which layer should this behavior be owned?
6. Does the test assert a durable contract or an incidental implementation detail?
7. Could the implementation be refactored without changing the behavior and without editing this test?
8. What should cause this test to fail?
9. What legitimate changes should not cause this test to fail?
10. What ongoing maintenance, execution, and diagnostic cost will the test impose?
11. Is there a smaller or more direct test that protects the same risk?
Do not add the test when its expected lifetime cost exceeds its expected protective value.
When deciding not to test plausible behavior, record or be able to explain why the risk is low, already protected, obvious, reversible, or cheaper to detect elsewhere.
1. Which requested behavior or established contract does it protect, and what realistic defect would violate it?
2. How likely and consequential is that defect, and is it already protected elsewhere?
3. What is the narrowest stable boundary that can test it clearly?
4. Would a correct internal refactor or a change to an incidental default require test edits?
5. Does its confidence justify its maintenance, execution, and diagnostic cost, or would a smaller test suffice?
## Definition of sufficient
A test suite is sufficient when:
- important contracts and invariants are protected;
- meaningful boundaries and failure modes are exercised;
- realistic and consequential regressions are credibly protected against silent recurrence;
- behavior involving data integrity, destructive operations, compatibility, security, concurrency, idempotency, and recovery is credibly protected;
- important external boundaries have realistic integration coverage;
- representative complete workflows are tested;
- failures provide useful signal rather than redundant noise;
- legitimate internal changes usually do not require test edits; and
- important requested behavior and established contracts are protected against realistic, consequential failures;
- boundaries and component interactions have coverage where they introduce meaningful risks not protected elsewhere;
- failures provide useful, distinct signals;
- tests usually remain stable through legitimate internal changes; and
- additional tests would mostly repeat existing protection or preserve inconsequential implementation details.
Sufficiency is a risk judgment, not a coverage percentage or test count. Reassess it as the library, its consumers, and the consequences of failure evolve.
The governing rule is:
> Test heavily where failure is consequential, subtle, or difficult to detect after the fact. Test lightly where failure is obvious, reversible, and inexpensive—and retain no test whose lifetime cost exceeds the confidence it provides.
Reassess sufficiency as the application, its users, and the consequences of
failure evolve.

View File

@@ -1,241 +1,237 @@
# ConfigKit Release Procedure
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
ConfigKit publishes one source-only Go module: the immutable source tag and the
matching checked-in release note are the deliverables. There is no application
binary, container image, generated package, or separate registry publication.
ConfigKit publishes a source-only Go module. The immutable annotated tag and
matching checked-in release note are the deliverables; there is no binary,
container image, generated package, or separate registry publication.
## Release identity and support
- Module: `gitea.maximumdirect.net/eric/ConfigKit`
- Primary branch: `main`
- Authoritative remote: `origin`, expected to fetch from
`https://gitea.maximumdirect.net/eric/ConfigKit.git`
- Tags: stable Semantic Versions such as `v0.1.0`; prerelease tags are not supported
- Minimum Go language/toolchain line: Go 1.25, tested with a supported patched
Go 1.25 toolchain and one newer supported toolchain
- Build targets: Linux and macOS on amd64 and arm64
- Authoritative remote: `origin`
- Authoritative push URL: `https://gitea.maximumdirect.net/eric/ConfigKit.git`
- Tags: stable Semantic Versions such as `v0.5.0`; no prereleases
- Minimum Go version: the first stable patch of the selected Go series, or a
higher patch when required by a dependency or specific correctness fix, as
recorded in `go.mod`; see the
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain)
- Release-gated targets: Linux amd64 and arm64
- Best-effort targets: macOS amd64 and arm64, under the
[platform support policy](policy/release.md#platform-support)
The automated checker cross-builds all four targets. Before release, the maintainer
must also obtain native test, race, symlink, and permission-fixture evidence on Linux
and macOS; cross-compilation is not native execution evidence.
Release validation runs one offline race-enabled Go suite, one full vet pass,
and CGO-disabled package compilation for both Linux targets. macOS and CI are
not release gates; do not wait for or poll CI.
## 1. Prepare access and tools
## 1. Select Go and prepare the tool
The release maintainer owns this procedure. Work in a dedicated clone with Git, Bash 3.2 or newer,
and two supported Go toolchains available. The checker supports the stock macOS
Bash; its filename enumeration is null-delimited, including names with whitespace. Configure private-host Git and Go module
access through the normal credential manager before starting. Keep tokens, passwords,
SSH material, and environment dumps outside the repository and command transcript.
Verify access without displaying credentials:
Use a clean checkout with Git, Bash 3.2 or newer, and a native compiler for race
tests. Choose the release type before selecting Go. For a minor or major release,
select the newest stable Go series available in the development environment.
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).
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
git remote get-url origin
git ls-remote origin HEAD
export GOTOOLCHAIN=local
go version
go env GOVERSION
command -v go
```
The remote URL must equal the authoritative URL above. Select the latest supported
patch in the Go 1.25 line and a newer supported toolchain. The values below are the
toolchains used while this runbook was introduced; update them to supported patch
releases during candidate preparation and record the actual versions in the release
note or validation record.
Only when a minor or major release advances the consumer minimum to a newer Go
series, derive that series' initial stable release from the selected executable:
```sh
export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5
export CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go version
GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download
GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go mod download
selected_go=$(GOTOOLCHAIN=local go env GOVERSION)
selected_series=${selected_go#go}
selected_series=${selected_series%.*}
release_minimum="$selected_series.0"
go mod edit -go="$release_minimum"
```
Those commands explicitly acquire toolchains and dependencies. The checker itself
sets `GOPROXY=off` and fails if its required cache is incomplete.
If a dependency or identified correctness fix requires a higher patch minimum,
use that required version instead and document the reason. When the series is
unchanged, leave the minimum unchanged unless a minor or major release
deliberately changes it under the
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
For example, validating with Go 1.26.8 leaves `go 1.26.0` unchanged. There is no
separate requirement to validate with Go 1.26.0.
## 2. Select the version and review compatibility
Choose an unused stable version and compare the candidate with the previous tag.
For the first release, review the whole public surface and current contracts.
Prepare dependencies whether or not the minimum changes:
```sh
release_version=v0.1.0
printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || :
if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then
printf 'remote tag already exists: %s\n' "$release_version" >&2
exit 1
else
test $? -eq 2
fi
git log --oneline --decorate --no-merges
go mod download
```
Review exported Go signatures and ownership, YAML and scalar behavior, precedence,
diagnostic codes, minimum Go version, and platform support. Before `v1.0.0`, any
intentional incompatible minor-release change must be explained with consumer action;
patch releases must remain compatible within their minor line. Review the maintained
consumer examples and the migration guide even when signatures did not change.
Reassess `go.yaml.in/yaml/v4` rather than using `@latest` implicitly:
Build the tool selected by `go.mod` into temporary storage outside the checkout.
Ensure `$TMPDIR` names a system temporary location, not the repository. These
commands never install another ReleaseKit version:
```sh
go list -m -versions go.yaml.in/yaml/v4
go list -m -json go.yaml.in/yaml/v4@latest
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
```
If changing the explicit pin, make that a reviewed dependency change and rerun the
complete checker, especially parser policy, limits, diagnostics, and rendering tests.
If retaining a prerelease pin, record that decision in Compatibility.
The module-selected executable is ReleaseKit v0.4.1. Use the built executable
for publication to preserve its exit status and publication-state diagnostics.
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
executable first on `PATH` through the publication command.
## 3. Write the release note
Create `docs/releases/$release_version.md` before selecting the candidate. It must
begin with `# ConfigKit $release_version` and contain `Summary`, `Compatibility`,
`Upgrade`, and `Changes` sections. Use “No action required” where appropriate. Link
to current canonical contracts instead of copying their definitions. Do not create a
release note until there is a real candidate and reviewed content.
## 4. Validate the source candidate
Run the complete checker from the repository root after dependencies have been
acquired. It validates module manifests in a disposable copy, both supported Go
toolchains, offline default tests, the native race detector, vet, native builds,
CGO-disabled cross-builds, formatting and whitespace (including untracked files),
examples, package isolation, and local documentation links.
Verify access to the authoritative remote without displaying credentials:
```sh
./scripts/release-check.sh
git ls-remote https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD refs/heads/main
```
`go vet` is the repository's static-analysis baseline. No additional linter is
claimed. Links to current ConfigKit files and headings are strict. Recorded links to
sibling consumer source are checked when those checkouts exist and are otherwise
inapplicable in an isolated clone; their immutable revisions remain recorded in the
extraction inventory.
Publication requires explicit authorization. Local implementation commits do
not authorize a push or release.
Complete these manual gates and record their evidence with the candidate:
## 2. Prepare and commit the candidate
- The maintainer reviews compatibility and downstream migration impact against
`docs/config.md`, Go API comments, and the consumer guide.
- The maintainer inspects `git status --short`, `git diff`, and the candidate file
list for credentials, private data, generated output, unintended files, and
unrelated changes.
- The maintainer records successful native checker runs on Linux and macOS, including
filesystem capability tests. If either host is unavailable, release remains
blocked unless platform support is formally narrowed in current documentation.
- The maintainer removes completed, obsolete documents under `docs/roadmap/`, repairs
inbound links, and reruns the checker. ADR rationale remains historical.
- The maintainer reviews the exact version-matched release note for accuracy and
confirms it describes only deliverables that will actually be published.
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).
## 5. Commit, push, and record the candidate
Create `docs/releases/<version>.md`. Its first line must be exactly
`# ConfigKit <version>`, and it must end with the license and attribution footer:
Commit all candidate source, contract, example, roadmap-retirement, and release-note
changes, rerun the checker against that clean commit, then push only `main` and record
the exact commit in Git metadata local to the clone:
```sh
git add .
git commit --message "Prepare ConfigKit $release_version"
./scripts/release-check.sh
test -z "$(git status --porcelain=v1 --untracked-files=all)"
git push origin HEAD:refs/heads/main
git rev-parse HEAD | tee .git/configkit-release-candidate
candidate_commit=$(cat .git/configkit-release-candidate)
```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)
```
If review changes the source after this point, create and validate a new candidate;
do not reuse the recorded commit.
Replace `VERSION` with the full tag and confirm the three target files exist in
the candidate. Retire completed roadmaps, preserve unfinished and deferred
work, and repair affected links. Review the complete candidate for unintended
files and credentials, then commit it on `main`. These are ordinary preparation
tasks, not separate approval gates. Any `go.mod` change belongs in this
candidate.
## 6. Run publication guards
## 3. Push the candidate to `main`
Immediately before tagging, fetch the authoritative branch and tags, restore the
recorded variables if this is a new shell, and execute every guard:
Push only that committed candidate to the literal authoritative URL. This does
not follow local tags:
```sh
release_version=v0.1.0
candidate_commit=$(cat .git/configkit-release-candidate)
git fetch --prune origin main
git fetch --tags origin
test "$(git branch --show-current)" = main
test -z "$(git status --porcelain=v1 --untracked-files=all)"
test "$(git rev-parse HEAD)" = "$candidate_commit"
test "$(git rev-parse refs/remotes/origin/main)" = "$candidate_commit"
test "$(git remote get-url origin)" = 'https://gitea.maximumdirect.net/eric/ConfigKit.git'
printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
test -f "docs/releases/$release_version.md"
test "$(sed -n '1p' "docs/releases/$release_version.md")" = "# ConfigKit $release_version"
for heading in Summary Compatibility Upgrade Changes; do
grep -Fxq "## $heading" "docs/releases/$release_version.md"
done
git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || :
if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then
printf 'remote tag already exists: %s\n' "$release_version" >&2
exit 1
else
test $? -eq 2
fi
git push --no-follow-tags https://gitea.maximumdirect.net/eric/ConfigKit.git HEAD:refs/heads/main
```
Confirm the recorded automated and manual approvals apply to exactly
`$candidate_commit`. A successful older run is not evidence for a changed commit.
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.
## 7. Tag and publish the exact ref
## 4. Publish the committed candidate
Create one annotated tag at the guarded commit, verify its peeled target, and push
only that ref. Do not use `git push --tags`.
With the authorized candidate already on authoritative `main`, invoke the
module-selected native executable exactly once:
```sh
git tag --annotate "$release_version" --message "ConfigKit $release_version" "$candidate_commit"
test "$(git rev-list -n 1 "$release_version")" = "$candidate_commit"
git push origin "refs/tags/$release_version:refs/tags/$release_version"
"$releasekit_bin" publish vMAJOR.MINOR.PATCH
```
The pushed tag completes the declared source-only publication. No Gitea release
object or binary upload is required.
## 8. Verify publication and clean consumer retrieval
Verify the authoritative tag and then retrieve that exact version into a new module
without a local `replace` directive. Private-host credentials must already be
configured. Use direct origin retrieval unless an explicitly approved internal Go
proxy is the publication channel.
Replace the placeholder with the selected version. ReleaseKit checks the
effective push URL against `expected_push_url`, verifies the candidate on
`main`, and checks the release-note H1 for the product and version. It then
runs the selected race-enabled standard suite once with test-time vet disabled,
one full vet pass, and CGO-disabled package compilation for Linux amd64 and
arm64. It publishes only the annotated tag for that candidate. Confirmed
publication of the intended tag completes the release. The tag points to the
captured commit and uses the complete checked-in note as its annotation message.
The optional `check`
command is available for diagnosis but is not a separate required pass:
```sh
remote_tag_commit=$(git ls-remote origin "refs/tags/$release_version^{}" | awk '{print $1}')
test "$remote_tag_commit" = "$candidate_commit"
"$releasekit_bin" check vMAJOR.MINOR.PATCH
```
Status 0 confirms publication, status 1 reports a known publication failure or
conflict, status 2 reports usage, profile, or version errors, and status 3 means
the push outcome remains unknown. Preserve both the native exit status and its
diagnostics.
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
git tag -d "$release_version"
"$releasekit_bin" publish "$release_version"
```
A validation failure after the candidate reached `main` leaves an untagged
candidate there. Correct it with a new commit rather than rewriting or rolling
back `main`. A conflicting published tag must never be moved or reused. A
defect in published source requires a corrected commit, a new semantically
appropriate version, and a matching release note.
## 6. Optional consumer verification
When useful or requested, read back the remote tag or retrieve the exact
version into a new module without a local `replace` directive. These checks are
optional and non-gating. Do not poll CI or wait for module propagation. For
direct-origin verification, use the exact published version in a temporary
consumer module:
```sh
release_version=vMAJOR.MINOR.PATCH
consumer_dir=$(mktemp -d)
(
cd "$consumer_dir"
go mod init example.invalid/configkit-release-check
GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version"
cat > main.go <<'EOF'
package main
import configkit "gitea.maximumdirect.net/eric/ConfigKit"
func main() {
_ = configkit.NoSource()
}
EOF
gofmt -w main.go
go test ./...
go build ./...
go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit
)
rm -rf -- "$consumer_dir"
```
The final `go list` output must show the exact module path and `$release_version`.
Record the result with the release evidence.
## 9. Handle failures without replacement
Before publication, fix the candidate, rerun every gate, commit, push, and record a
new exact candidate. An unpublished local tag may be deleted only after verifying it
was never pushed or used by another release channel.
After publication, never move, delete, or recreate the tag and never replace its
release note. Correct the defect on `main`, choose a new semantically appropriate
version, and repeat this procedure. A supported advisory or deprecation mechanism
may warn consumers about the old version but cannot change its contents.
The output should show the exact module path and selected version. Report this
optional check separately from publication success.

43
docs/releases/v0.1.1.md Normal file
View File

@@ -0,0 +1,43 @@
# ConfigKit v0.1.1
This release corrects the spelling of ConfigKit's reserved configuration key.
## Summary
The reserved directive key is now `configkit` instead of `_configkit`. Import
declarations continue to use the same `imports` sequence and path/glob syntax.
See the [configuration contract](../config.md#explicit-imports).
## Compatibility
This is an intentional configuration compatibility change, approved as a one-time
[patch-release exception](../policy/release.md#policy-exceptions) because there are
no current consumers. `_configkit` is no longer a directive or a reserved key; it
is ordinary application data and is subject to normal schema validation. There is
no legacy alias. `configkit` is now reserved, including in nested mappings, profiles,
and override paths and values.
The Go API, diagnostic codes, Go minimum version, and supported platforms are
unchanged. The private YAML dependency remains pinned to
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release
during candidate preparation.
## Upgrade
Rename `_configkit` directive keys to `configkit` in root configuration files and
imported fragments. Rename any application-owned `configkit` fields to avoid the
newly reserved key. Update diagnostic-path expectations to the new spelling.
Files retaining `_configkit` will not load their declared imports.
Consumers can select this release with:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.1
```
## Changes
- Renamed the directive key consistently across imports, profiles, and overrides.
- Updated current configuration documentation and the composed consumer fixture.
- Updated behavioral tests and added coverage proving the old spelling is not an
import alias.

50
docs/releases/v0.1.2.md Normal file
View File

@@ -0,0 +1,50 @@
# ConfigKit v0.1.2
This release adds safe enumeration of effective and removed metadata paths.
## Summary
`Metadata.HistoricalPaths` returns the semantic paths observed during a load,
including paths removed by profiles, CLI overrides, or recorded resolution. This
allows applications to discover hidden declarations and inspect their ownership
through the existing `Metadata.History` API without reloading configuration or
resolving unselected credentials.
See the [layered metadata contract](../config.md#layered-metadata) for current
behavior.
## Compatibility
This is a backward-compatible API addition. Existing APIs, configuration syntax,
diagnostic codes, minimum Go version, and supported platforms are unchanged.
Existing consumers do not need to change their code or configuration.
Returned paths are detached, unique by canonical JSON Pointer spelling, and sorted
by that spelling. The enumeration includes defaults and the root, exposes no
configuration values, and performs no I/O or secret resolution.
The private YAML dependency remains pinned to
`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release during
candidate preparation.
## Upgrade
No action required. Consumers that want historical path discovery can update with:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.2
```
Then iterate over `result.Metadata().HistoricalPaths()` and use `History` for
chronological definitions or `Lookup` to determine whether each path remains
effective.
## Changes
- Added `Metadata.HistoricalPaths` with detached, deterministic path enumeration.
- Included effective, default, and removed paths while excluding paths that never
existed.
- Added coverage for parent removal, mapping-to-sequence shape replacement, escaped
mapping keys, detached results, zero metadata, composite base ownership, and
avoidance of superseded secret lookup.
- Documented historical path enumeration in the current configuration contract.

77
docs/releases/v0.10.0.md Normal file
View 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)

58
docs/releases/v0.2.0.md Normal file
View File

@@ -0,0 +1,58 @@
# ConfigKit v0.2.0
This release adds integer schema-version input and consistent canonical sequence
path handling.
## Summary
Applications can retain YAML such as `version: 4` while storing and checking their
version as a Go string. The exception applies only to the path designated by
`VersionRule`; unrelated fields keep strict decoding. See the
[configuration contract](../config.md#application-schema-versions).
## Compatibility
This minor release intentionally changes integer schema-version input from a
`version.type` rejection to invoking the application checker with its decimal text.
Plain string destinations at that path now accept it. Public Go API shapes,
minimum Go version, and supported platforms are unchanged.
Parsed sequence indices now require canonical nonnegative decimal spelling.
Signed tokens such as `+0` and `-0`, previously accepted by some traversal paths,
no longer identify sequence elements. They remain valid literal mapping keys.
Version checking, decoding, and metadata matching now agree on this boundary.
The private YAML dependency remains pinned to `go.yaml.in/yaml/v4 v4.0.0-rc.6`,
confirmed as the newest available v4 release during candidate preparation. Linux
candidate checks use Go 1.25.5 and Go 1.26.7. The maintainer confirmed native macOS
checks at `244104f` and approved carrying that result across the documentation-only
candidate changes under the [recorded exception](../policy/release.md#policy-exceptions).
## Upgrade
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.2.0
```
Existing string schema versions need no configuration changes. Applications using
integer YAML versions can retain them and use a plain string field plus a
`VersionRule`. Review the application checker: it now receives integer identifiers
as text, and remains responsible for rejecting unsupported versions. Fractions,
exponents, booleans, and containers remain invalid version kinds.
Replace signed sequence-index tokens in configured paths with canonical indices
(for example, `/versions/+0` becomes `/versions/0`). Do not change signed mapping
keys. Literal integers elsewhere still do not decode into strings, and numeric
destinations retain their existing range checks.
## Changes
- Added load-local, path-scoped integer-to-string schema-version decoding without
mutating original input or introducing general scalar coercion.
- Preserved numeric historical input, string effective output, decoding
reconciliation, source attribution, and secret redaction.
- Unified sequence path interpretation and added regression coverage for signed
tokens, literal mapping keys, and explicit key/index distinctions.
- Updated maintained Notarius-shaped fixtures and migration guidance for quoted
and integer version forms.
- Retired completed implementation and remediation roadmaps to Git history.

70
docs/releases/v0.3.0.md Normal file
View File

@@ -0,0 +1,70 @@
# ConfigKit v0.3.0
This release adds source provenance during application resolution, separates the
public API from its internal engine, and moves macOS to best-effort support.
## Summary
Resolvers can query fixed entry-state metadata and already-acquired sources using
`Resolution.InputInfo` and `Resolution.Source`. This supports application-owned
decisions such as resolving relative paths against their declaring configuration
files. See the [resolution contract](../config.md#recorded-application-resolution)
and the maintained [source-aware path example](../consumers/README.md#source-aware-application-path-policy).
The root `configkit` package now owns public types and explicit wrappers around a
single private engine. Internal changes no longer automatically expand public
method sets through shared type aliases. See the [component overview](../internal/overview.md)
and [facade decision](../adr/0006-public-facade.md).
## Compatibility
Existing consumer imports, exported API signatures, configuration syntax,
precedence, secret handling, callback lifetime, and ownership behavior are
preserved. The two resolution query methods are additive and available only
during an active resolution callback; returned records remain detached. The
library supplies provenance, not a new filesystem normalization policy.
macOS on amd64 and arm64 is now best-effort rather than release-validated.
Neither native macOS evidence nor Darwin cross-builds are required for publication,
and macOS-specific failures do not by themselves block a release. Linux on amd64
and arm64 retains release-gated builds and native Linux validation. See the
[platform support policy](../policy/release.md#platform-support).
The module still requires Go 1.25 or newer. The private YAML dependency remains
pinned to `go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4
release during preparation. It is still a prerelease dependency, isolated behind
the existing private parser boundary; no dependency upgrade is included.
Linux candidate validation uses Go 1.25.14 and Go 1.26.8, including the complete
release checker, race tests, consumer examples, filesystem fixtures, and Linux
cross-builds. No native macOS validation is claimed for this candidate.
## Upgrade
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.3.0
```
No code or configuration migration is required for the facade refactor. Adopt the
new provenance queries only if your resolver needs them. Applications retain
ownership of relative-path bases and fallback/error behavior.
macOS consumers should account for the reduced validation commitment and perform
their own application checks when upgrading if macOS behavior is important to
their deployment.
## Changes
- Added callback-scoped, fixed entry metadata and source lookup without additional
filesystem access or credential resolution.
- Added a maintained Notarius-shaped path-normalization example with provenance,
frozen effective output, and explanation coverage.
- Moved loading, schema, resolution, provenance/history, secret, and rendering
machinery into one internal engine while keeping public named types root-owned.
- Added exact-type secret and callback adapters, with regression coverage for
missing version checkers and nil/zero closed-context failures.
- Added type-aware public-boundary checks covering aliases, collections, generic
constraints, and exported method sets; restored public reference documentation.
- Removed macOS release gates and retained required Linux validation.
- Retired completed feature, implementation, and remediation documents to Git
history and repaired navigation to current contract and component owners.

43
docs/releases/v0.3.1.md Normal file
View File

@@ -0,0 +1,43 @@
# ConfigKit v0.3.1
This release fixes secrets-directory bootstrap when the directory contains Git
metadata or other dot-prefixed entries.
## Summary
`LoadSecretsDir` now ignores dot-prefixed files, directories, and symlinks,
including `.git`, `.gitignore`, and `.DS_Store`. These entries no longer prevent
otherwise valid secrets from loading and are omitted from the bootstrap report.
See the [bootstrap contract](../config.md#explicit-secrets-directory-bootstrap).
## Compatibility
This is a backward-compatible bug fix for previously rejected directory contents.
There are no Go API signature changes or migrations for previously accepted
inputs. Validation of non-hidden entries, existing-environment precedence, and
the existing enumeration and content limits remain unchanged. Hidden entries
still count toward the directory enumeration limit.
The minimum Go version remains 1.25. Linux remains release-validated and macOS
remains best-effort. The explicitly pinned YAML dependency remains
`go.yaml.in/yaml/v4 v4.0.0-rc.6`; reassessment found no newer published version.
Retaining this prerelease pin avoids unrelated parser changes in this patch.
## Upgrade
Update the module dependency and rebuild the consuming application:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.3.1
```
No configuration migration is required. A secrets directory can retain its
dot-prefixed metadata without treating those entries as secret candidates.
## Changes
- Filter hidden entries before filename and metadata validation and reporting.
- Cover hidden-only and mixed directories, including Git metadata and a dangling
hidden symlink, with regression tests.
- Update public API comments and the canonical bootstrap contract.
- Stop tracking local codebase-memory tooling artifacts and ignore their directory.

56
docs/releases/v0.4.0.md Normal file
View File

@@ -0,0 +1,56 @@
# ConfigKit v0.4.0
This release renames selected configuration profiles to overlays, intentionally
breaking the previous naming contract without compatibility aliases or fallback.
## Summary
Selected configuration now uses `Options.Overlay` and `overlays/<name>.yml`.
Selection, merge rules, input precedence, and lifecycle behavior are unchanged.
See the [selected-overlay contract](../config.md#selected-overlays).
## Compatibility
This is a breaking minor release before v1.0.0. Four surfaces change:
- Go API: `Options.Profile` becomes `Options.Overlay`, `LayerProfile` becomes
`LayerOverlay`, and all seven `CodeProfile*` constants become `CodeOverlay*`.
- Selected files: `profiles/<name>.yml` becomes `overlays/<name>.yml`; there is
no legacy-directory fallback.
- Provenance: the layer string changes from `profile` to `overlay`.
- Structured diagnostics: the `profile.*` code prefix becomes `overlay.*`.
The old Go identifiers are removed. Application-owned concepts such as
`ModelProfile` and `model_profile` are unchanged. Ordinary explicit imports from
a directory named `profiles` remain valid base inputs.
The minimum Go version remains 1.25. Linux remains release-validated and macOS
remains best-effort. The YAML dependency remains pinned to
`go.yaml.in/yaml/v4 v4.0.0-rc.6`; this release does not change parser dependencies.
## Upgrade
Update the dependency:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.4.0
```
Rename the affected Go identifiers, move selected files into `overlays/`, update
any handling of provenance-layer and diagnostic-code strings, and rebuild.
The maintained example helper is now `LoadNarratioOverlay` rather than
`LoadNarratioProfile`. Application CLI flag names remain application-owned;
ConfigKit does not require renaming domain-specific profile concepts.
See the [migration guide](../consumers/README.md#selected-overlay-migration).
## Changes
- Rename the selected feature consistently across the public facade, internal
engine, diagnostics, source labels, examples, and current documentation.
- Add regression coverage for the new directory, absence of legacy fallback,
public values, explanations, and preservation of application profile fields.
- Align contributor policies with the current shared standard.
- Simplify release validation to one local minimum-toolchain check. CI is not a
release gate; successful publication of the intended tag completes the
source-only release. See the [release procedure](../release.md).
- Retire completed overlay and release-workflow planning documents to Git history.

42
docs/releases/v0.4.1.md Normal file
View File

@@ -0,0 +1,42 @@
# ConfigKit v0.4.1
This release formalizes ConfigKit's licensing and upstream attribution
documentation without changing library behavior.
## Summary
ConfigKit now includes an explicit LGPL-3.0-or-later grant in COPYRIGHT,
complete LGPLv3 and incorporated GPLv3 license texts, and centralized upstream
attribution. README and release guidance provide navigation to those materials.
## Compatibility
No Go code, public API, configuration semantics, diagnostic behavior, or
dependencies changed from v0.4.0. The minimum Go version remains 1.25, Linux
remains release-validated, and macOS remains best-effort. The YAML dependency
remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`.
The project grant is explicitly LGPL-3.0-or-later. Third-party material remains
subject to its respective terms, recorded in the attribution document below.
Published earlier releases are unchanged.
## Upgrade
Update the module dependency and rebuild to select this release:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.4.1
```
No application-code or configuration migration is required from v0.4.0.
## Changes
- Add the project COPYRIGHT statement and complete standard project license texts.
- Document go-yaml attribution with exact upstream revision links and local
Apache-2.0 and MIT license texts.
- Adopt the house OSS policy and connect licensing ownership and contributor guidance.
- Add license and attribution navigation to README and future release documents,
without adding release gates or changing publication mechanics.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.1/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.1/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.1/docs/oss/attribution.md)

45
docs/releases/v0.4.2.md Normal file
View File

@@ -0,0 +1,45 @@
# ConfigKit v0.4.2
This patch release simplifies and safeguards source publication and aligns
maintainer policy and licensing guidance. Library behavior is unchanged.
## Summary
Maintainers can now publish a prepared release with one guarded command. It
validates the exact committed candidate, checks the authoritative destination,
and pushes only the candidate on `main` and its annotated version tag. The
[release procedure](../release.md) explains preparation and partial-publication
recovery; successful tag publication completes the source-only release without
waiting for CI or consumer retrieval.
## Compatibility
No Go code, public API, configuration semantics, diagnostic behavior, or
dependencies changed from v0.4.1. The minimum Go version remains 1.25. Linux
amd64 and arm64 remain release-validated; macOS remains best-effort. The YAML
dependency remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`.
## Upgrade
Select this version and rebuild:
```sh
go get gitea.maximumdirect.net/eric/ConfigKit@v0.4.2
```
No application-code or configuration migration is required from v0.4.1.
## Changes
- Add guarded single-command publication with exact candidate, version, and
remote checks; note-derived annotated tags; and bounded failed-push recovery.
- Keep one offline race suite, one full vet pass, and Linux amd64/arm64 builds
as release gates. Formatting and whitespace remain development checks;
dependency tidiness is reviewed when dependencies change.
- Align adopted policies and contributor guidance with their pinned upstream
revision, and include the CC BY-SA 4.0 license text and attribution for
adapted policy material. The project grant remains LGPL-3.0-or-later.
- Retire completed release-workflow and policy-alignment planning records while
preserving their history in Git. Deferred scope remains documented.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.2/COPYRIGHT) | [LICENSE](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.2/LICENSE) | [ATTRIBUTION](https://gitea.maximumdirect.net/eric/ConfigKit/src/tag/v0.4.2/docs/oss/attribution.md)

53
docs/releases/v0.5.0.md Normal file
View 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
View 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#3-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
View 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
View 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
View 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
View 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)

222
docs/roadmap/complexity.md Normal file
View File

@@ -0,0 +1,222 @@
# Internal Complexity Refactoring Roadmap
Status: proposed target state. This roadmap defines a behavior-preserving
refactor; a later implementation plan will define stages and exact private
signatures and file names.
## Purpose
Make ConfigKit's import, inheritance, and projection implementation easier to
understand, review, and change while preserving its public APIs and configuration
behavior. Concentrate on functions that coordinate several responsibilities and
carry substantial shared state.
The target is clearer responsibility and ownership. Adding a few well-named
helpers, private records, or focused files can serve that target even when total
line count increases. Complexity measurements help locate candidates; they do
not establish function-size limits, score thresholds, or release gates.
The [architecture policy](../policy/architecture.md) owns package and ownership
constraints. The [configuration contract](../config.md) and exported Go comments
remain authoritative for consumer behavior.
## Current Baseline
Reviewed baseline: `7a83704f43ef9870db33661231cdff5d6ee502f7`.
The root facade delegates to a cohesive private engine, supported by the ordered
document representation and YAML adapter. Most functions are small. The
complexity audit identified these concentrated maintenance costs:
| Area | Current implementation | Required improvement |
| --- | --- | --- |
| Imports | `imports.go` spans acquisition, item-local composition, and evidence projection. Private loading variants pass long positional argument lists. Source admission and directory selection contain substantial mixed control flow. | Give import entry points explicit, cohesive inputs and separate the responsibilities of acquisition, scope assembly, and evidence projection. |
| Inheritance | `applyInheritanceScopeTemplates` coordinates payload preparation, parent combination, policy ownership, origins, history, and accounting in one executor. | Keep one readable executor with focused operations and explicit state handoffs. |
| Projection | `valueOperation.project` combines type dispatch with struct flattening, map and sequence traversal, scalar conversion, sensitivity, and accounting. | Retain clear dispatch while moving substantial value-kind handling into focused helpers. |
The implemented inventory belongs to the
[component overview](../internal/overview.md). Existing import flow and scheduling
are described in [import internals](../internal/imports.md).
## Target Architecture
The refactor stays within `internal/engine`. The root facade, `internal/document`,
and `internal/yaml` retain their existing responsibilities and dependency
direction. Focused files and ordinary private functions express the new internal
organization. Package extraction is outside this roadmap's selected design.
`executeLoadRequest` remains the visible coordinator of the shared load lifecycle.
Its phase order should remain easy to follow. Helpers expose meaningful work and
results rather than distributing that order across a generic execution framework.
Existing shared mechanisms remain shared: recursive mapping combination,
identified-sequence operations, dependency-ready scheduling, schema descriptors,
and projection/accounting state. New helper boundaries make their ownership and
use clearer without creating parallel implementations.
### Imports: Cohesive Inputs And Scope Ownership
Import loading has a clear private entry surface using named request/context
fields for related inputs. Reuse the existing load-request, acquisition-scope,
and traversal models where they fit. Consolidate forwarding variants whose only
purpose is passing another optional positional argument through the chain.
Retain convenience helpers when they express a useful, distinct operation.
Keep these state lifetimes identifiable:
- Load-wide state: accounting, shared inclusion/repetition information,
namespaces, and source-version processing.
- Acquisition requests: source and filesystem context, placement, declarations,
applicable policy, and the relevant assembly handoff.
- Composition scopes: peer contributions, item-local helpers, record ownership,
and pending declarations.
- Evidence: physical source records, effective destinations, origins, and retained
history projections.
Group acquisition, pending-item assembly, and evidence projection in focused
files. A caller should be able to identify which operation acquires resources,
which composes already acquired content, and which projects retained evidence.
Shared mutable state has a visible owner; a request record need not duplicate
state already held by that owner.
Within dense admission, directory-selection, and declaration-parsing routines,
extract coherent operations where that reduces nested, mixed responsibilities.
Preserve the existing selection algorithms and host/native filesystem behavior.
Resource ownership and closure remain explicit across success and failure paths.
The resulting flow preserves the distinction among acquisition order, strict
scope scheduling, and precedence. Completing a helper scope before placing its
item remains part of the same import flow. Ordinary and layered loading continue
to share the applicable mechanisms.
### Inheritance: One Executor With Explicit Phases
Keep the mapping and identified-sequence adapters and their shared template
executor. The executor coordinates focused operations for:
- Preparing child payload, directives, and destination-local state.
- Preparing the resolved parent payload and applicable inherited policy.
- Combining values through the existing mapping and sequence mechanisms.
- Producing policy, origin, replacement, and replay evidence from that combination.
- Accounting for expansion and retained history under the existing budgets.
These are responsibility boundaries, not a required helper count or a new
scheduler. Small private result records are appropriate when related outputs
otherwise travel through long argument lists or implicit mutations.
Authored templates, resolved values, scoped policy, and inspection evidence remain
distinguishable. Immutable borrowed inputs and owned mutable results have clear
lifetimes. Nested instructions remain available for destination-local evaluation.
For identified-record inheritance, the child's own identity fields and origins
remain intact; the parent's identity fields are excluded from inherited payload
and its history, as the current contract requires.
One sequence-operation result continues to drive value construction, record
ownership, origins, and historical position projection. Helpers consume that
evidence directly. Scheduling remains dependency-ready, enclosing relationships
resolve before nested ones, and assembled sequence order remains unchanged.
### Projection: Explicit Value-Kind Handlers
Keep `projectConfiguration` as the schema-checked entry and `valueOperation` as
the owner of per-operation traversal and accounting state. The central dispatch
clearly selects the appropriate handling for pointers, dynamic values, structs,
maps, sequences, and scalars.
Substantial struct, map, and sequence work lives in focused handlers. Struct
projection retains declaration order, inline flattening, collision diagnostics,
and sensitivity propagation. Map projection retains deterministic key ordering.
Scalar conversion retains the existing number, duration, text-codec, and secret
reference representations.
Depth and node accounting retain their existing meaning, including flattened
containers. Cycle tracking stays active through recursive child traversal and
is balanced on error paths as well as success. Passing state through a helper
does not introduce another full-tree projection or an unnecessary ownership copy.
Cloning and source-value construction remain distinct operations with their
existing semantics. Reuse small helpers when the semantics coincide; keep the
projection refactor centered on making the existing traversal readable.
## Behavior And Scope Preservation
The implementation preserves:
- Root-owned public types, signatures, supported import paths, and facade boundaries.
- Supported source forms, opt-ins, import controls, placement, acquisition and
composition ordering, and source/version evidence.
- Strict peer composition, precedence operations, identity agreement and lifetime,
record ownership, and destination-local inheritance.
- The load lifecycle, including typed defaults, post-decode CLI assignment,
recorded application resolution, and final validation.
- Explicit presence, provenance, history, sensitivity, resource contexts, and
detached publication/query results.
- Established diagnostic codes, semantic paths, source coordinates, ordering,
resource lifetime, and existing limit behavior.
The owning contracts define the extent of these obligations. Incidental private
call sequences, exact helper names, and allocation counts do not acquire new
guarantees through this roadmap. The work adds no consumer options, configuration
syntax, dependencies, or complexity-based gates.
## Relationship To Remediation And Other Work
The completed remediation record at `ea46001:docs/roadmap/remediation.md`
documents the identified correctness and algorithmic efficiency findings.
Several overlap these files, particularly item-scope application matching and
inheritance policy localization.
Use the documented contract and completed remediation as the behavioral baseline.
Keep corrective changes reviewable under their findings, and retain their
regressions through this refactor. The later implementation plan should coordinate
overlapping work and select its starting revision rather than duplicating the
remediation plan here. These structural changes do not close a finding merely
because its function moved or became smaller.
[Configuration discovery](../config.md#configuration-discovery) is implemented.
Import cleanup should preserve its source-acquisition boundary and behavior as
part of this refactor.
## Work Needed And Validation
Implementation needs to consolidate import inputs and adapters, separate import
responsibilities, extract inheritance operations with explicit outputs, and
decompose projection handling. Remove superseded private plumbing as its callers
migrate. Review the resulting call flow for repeated traversals, policy scans,
copies, or hidden state transitions introduced by extraction.
Reuse the existing behavioral suites under the
[development guide](../development.md) and [testing policy](../policy/testing.md):
- Imports: selection, scheduling, source/version evidence, placement, helper scopes,
ordinary/layered equivalence, filesystem failures, and resource lifetime.
- Inheritance: mapping and record relationships, nested destination resolution,
cancellation and replacement, policy ownership, history, sensitivity, and
independent loads.
- Projection: schema/decoding agreement, inline fields, text codecs, nil/empty
values, cycles and budgets, secrets, and detached results.
Add focused regressions only where a consequential handoff risk lacks coverage.
Tests should continue to describe behavior as private functions and files move.
Use the existing everyday check and applicable ownership race checks during
implementation. Complexity measurements can inform review; acceptance does not
depend on numerical scores or a new benchmark campaign.
Update the component overview and import internals to describe the implemented
ownership and file responsibilities when the refactor lands. Update contributor
navigation and test-owner references where moves affect them. Public behavior
continues to be defined by its current canonical owners.
## Completion Criteria
- Import callers pass cohesive named inputs, with clearly owned load-wide and
scope-local state and fewer redundant forwarding variants.
- Import acquisition, item assembly, and evidence projection are easy to locate
and follow across focused private files.
- Inheritance orchestration exposes value, policy, evidence, and accounting work
without requiring readers to reconstruct their interaction from one large body.
- Projection dispatch and substantial value-kind handlers have clear boundaries
and share the original operation state correctly.
- Existing APIs and contracted behavior, including completed remediation, remain
covered by the relevant behavioral and ownership checks.
- Internal documentation matches the resulting implementation.

View File

@@ -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 profile 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.

85
docs/roadmap/future.md Normal file
View File

@@ -0,0 +1,85 @@
# Potential Future Features
This document lists potential enhancements for consideration in future ConfigKit
releases. Entries are not accepted contracts, implementation plans, or release
commitments. Adopting a candidate requires an explicit design decision and a
separate plan. Significant architectural exclusions and their rationale belong
in ADRs; this document does not maintain a list of rejected proposals.
## 1. Recursive body merging for identified records
Status: deferred. The implemented
[collection policy](../config.md#strict-document-composition) uses `upsert` to
replace a complete matched record. Recursive merging of fields inside matching
records remains a separate candidate and would revisit the whole-record update
boundary selected by [ADR 0017](../adr/0017-collection-sequence-policy.md).
Consider this only in response to a concrete consumer need. A proposal would need
to define conflicts for overlapping body fields, removal of omitted fields,
presence and ordering, and how provenance, history, sensitivity, and resource
context follow partially retained records. Any change requires a new decision and
implementation plan; this entry does not alter current upsert behavior.
## 2. Required version declarations for selected sources
Status: deferred. PromptKit requested a way to require version declarations on
a selected subset of source files while permitting other fragments to omit them.
### Motivation and proposed behavior
PromptKit could require each collection's root file (for example,
`collection.yml` or `meta.yml`, with naming still undecided) to declare an
accepted specification version. Other files would use the existing
[check-when-present behavior](../config.md#shared-source-version-checks).
This would enforce a rule that otherwise remains a documentation convention.
All declarations would use the same application-owned `Definition.Version`
path and acceptance policy. The proposal concerns where declarations are
required; it does not introduce independently versioned collections or subtrees.
A root declaration would not prove that headerless descendants declare a
compatible version or replace final configuration validation.
### Questions to resolve before implementation
- **Selection API:** should callers mark explicit inputs, select sources by
path or predicate, or annotate imports? An input-level requirement is simpler,
but cannot by itself select a root reached only through an import.
- **Scope:** does a requirement apply only to the selected document or also to
its imports? Applying it only to that document best fits PromptKit's request;
imported fragments could still declare versions checked by the shared policy.
- **Mode interaction:** how should selection interact with disabled source
checks? Require an enabled mode, or activate checking for selected sources?
Selection would strengthen check-when-present behavior and add no requirement
beyond the load-wide required mode.
- **Source handling and evidence:** how should selection work across file,
reader, byte, and mapping inputs, glob matches, and repeated imports? Define
behavior for absent optional inputs and preserve source-local diagnostics,
inspection evidence, and existing sensitivity handling.
## 3. Performance experiments
Status: deferred pending evidence. The completed performance audit identified
four internal optimization candidates for separate investigation:
- Branch-local explanation replay instead of the current whole-tree replay.
- Redesigning the private metadata-copy handoff while preserving detached public
records.
- Caching schema descriptors across loads.
- Indexing complex contributor-trace membership while preserving its equality
rules and compact linked traces.
These are experiments, not committed implementation work or promised speedups.
Reconsider them with evidence that the benefit justifies the ownership,
retained-memory, or equivalence risks. The completed audit and implementation
evidence are preserved in Git history at `8e91a2b` under `docs/roadmap/`.
### Questions to resolve before implementation
- Which representative workloads show enough remaining cost to justify each
optimization, and what measurements would establish a useful improvement?
- Can narrower replay and membership indexes preserve explanation results,
ordering, provenance, and sensitivity behavior?
- Can metadata handoff changes preserve detached public records and ownership
across loads without introducing aliasing?
- What cache lifetime and concurrency design would make schema reuse worthwhile
without excessive retained memory?

View File

@@ -19,6 +19,28 @@ func ExampleLoad() {
// Output: {true 3} <nil>
}
func ExampleLoad_sequenceDeclaration() {
type Item struct {
ID string `yaml:"id"`
}
type Config struct {
Items []Item `yaml:"items"`
}
result, err := configkit.Load[Config](configkit.Bytes("catalog.yml", []byte(`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() {
type Config struct {
Workers int `yaml:"workers"`
@@ -30,6 +52,32 @@ func ExampleNoSource() {
// 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() {
type Config struct {
First string `yaml:"first"`
@@ -128,8 +176,15 @@ func ExampleOverride() {
})
}
result, err := configkit.Load(configkit.NoSource(), configkit.Options[Config]{
Definition: configkit.Definition[Config]{Defaults: func() Config { return Config{Workers: 4} }},
Overrides: overrides,
Definition: configkit.Definition[Config]{
Defaults: func() Config { return Config{Workers: 4} },
Resolve: func(configuration *Config, resolution *configkit.Resolution) error {
configkit.Default(resolution, configkit.Keys("workers"), &configuration.Workers, 100,
"preserve an explicitly supplied worker count")
return nil
},
},
Overrides: overrides,
})
fmt.Println(result.Config.Workers, result.Sources()[0].Label, err)
// Output: 0 --workers <nil>

View File

@@ -4,6 +4,7 @@ package consumers
import (
"math"
"path/filepath"
"strconv"
"strings"
"time"
@@ -28,14 +29,14 @@ type NarratioConfig struct {
// LoadNarratio loads the maintained import fixture with Narratio-shaped rules.
func LoadNarratio(path string) (*configkit.Result[NarratioConfig], error) {
return LoadNarratioProfile(path, "")
return LoadNarratioOverlay(path, "")
}
// LoadNarratioProfile selects a ConfigKit configuration overlay independently
// LoadNarratioOverlay selects a ConfigKit configuration overlay independently
// from the application's ModelProfile field.
func LoadNarratioProfile(path, profile string) (*configkit.Result[NarratioConfig], error) {
func LoadNarratioOverlay(path, overlay string) (*configkit.Result[NarratioConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[NarratioConfig]{
Imports: true, Profile: profile,
Imports: true, Overlay: overlay,
Definition: configkit.Definition[NarratioConfig]{
Defaults: func() NarratioConfig { return NarratioConfig{PollInterval: time.Minute} },
Resolve: resolveNarratio,
@@ -83,7 +84,7 @@ type ObjectBinding struct {
Options map[string]any `yaml:"options"`
}
// NotariusConfig demonstrates string versions and dynamic catalog options.
// NotariusConfig demonstrates string-backed versions and dynamic catalog options.
type NotariusConfig struct {
Version string `yaml:"version"`
Bindings []ObjectBinding `yaml:"bindings"`
@@ -93,7 +94,7 @@ type NotariusConfig struct {
func LoadNotarius(path string) (*configkit.Result[NotariusConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[NotariusConfig]{
Definition: configkit.Definition[NotariusConfig]{
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: requireVersion("v1")},
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: requireVersion("4")},
Validate: validateNotarius,
},
})
@@ -146,6 +147,69 @@ func NotariusWindow(configuration NotariusConfig, binding int) (int, bool) {
return int(value), true
}
// NotariusPathConfig demonstrates application-owned interpretation of a
// path-valued configuration field. ConfigKit supplies provenance; this example
// selects its own relative-path bases.
type NotariusPathConfig struct {
Reference string `yaml:"reference"`
Sensitive string `yaml:"sensitive"`
}
// LoadNotariusPaths loads the path-bearing Notarius-shaped example. InvocationDir
// is the application-supplied base for relative CLI overrides; it is never read
// from the process working directory.
func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Override) (*configkit.Result[NotariusPathConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[NotariusPathConfig]{
Imports: true, Overrides: overrides,
Definition: configkit.Definition[NotariusPathConfig]{
Defaults: func() NotariusPathConfig { return NotariusPathConfig{Reference: "defaults/reference.txt"} },
Resolve: func(configuration *NotariusPathConfig, resolution *configkit.Resolution) error {
resolveNotariusReference(configuration, resolution, invocationDir)
return nil
},
Secret: func(path configkit.Path) bool { return path.String() == "/sensitive" },
},
})
}
func resolveNotariusReference(configuration *NotariusPathConfig, resolution *configkit.Resolution, invocationDir string) {
path := configkit.Keys("reference")
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
return
}
info, found := resolution.InputInfo(path)
if !found {
resolution.Warning(path, "relative reference has no entry provenance")
return
}
base := ""
if info.Origin.Layer == configkit.LayerCLI {
base = invocationDir
if base == "" {
resolution.Warning(path, "relative CLI reference has no invocation directory")
return
}
} else {
sourceID := "source-1" // ConfigKit's primary-source identity convention.
if info.Origin.Layer != configkit.LayerDefault {
if len(info.Origin.Locations) != 1 {
resolution.Warning(path, "relative reference has no unambiguous declaring source")
return
}
sourceID = info.Origin.Locations[0].SourceID
}
source, exists := resolution.Source(sourceID)
if !exists || source.Path == "" {
resolution.Warning(path, "relative reference has no declaring file")
return
}
base = filepath.Dir(source.Path)
}
configkit.Normalize(resolution, path, &configuration.Reference,
filepath.Join(base, configuration.Reference), "resolved relative reference from entry source")
}
// ModuleOptions is a WeatherReporter-shaped union of known module option fields.
type ModuleOptions struct {
WindowUnits *int `yaml:"window_units"`

View File

@@ -26,7 +26,13 @@ func Example_notarius() {
result, err := consumers.LoadNotarius(fixture("notarius", "config.yml"))
window, ok := consumers.NotariusWindow(result.Config, 0)
fmt.Println(result.Config.Version, result.Config.Bindings[0].Object, window, ok, err)
// Output: v1 summary 12 true <nil>
// Output: 4 summary 12 true <nil>
}
func Example_notariusPaths() {
result, err := consumers.LoadNotariusPaths(fixture("notarius-paths", "main.yml"), "/invocation")
fmt.Println(filepath.Base(result.Config.Reference), err)
// Output: imported.txt <nil>
}
func Example_weatherReporter() {
@@ -46,6 +52,22 @@ func Example_seriatim() {
// 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() {
const blockedName = "CONFIGKIT_WEATHER_EXAMPLE_BLOCKED"
const tokenName = "CONFIGKIT_WEATHER_EXAMPLE_TOKEN"
@@ -86,10 +108,13 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) {
if origin, ok := narratio.Metadata().Lookup(configkit.Keys("artifacts", "transcript", "output")); !ok || origin.Origin.Layer != configkit.LayerResolution {
t.Fatalf("artifact origin = %#v, %v", origin, ok)
}
profiled, err := consumers.LoadNarratioProfile(fixture("narratio", "pipeline.yml"), "testing")
if err != nil || profiled.Config.PollInterval.String() != "2m0s" ||
profiled.Config.ModelProfile != "concise" || len(profiled.Sources()) != 3 {
t.Fatalf("configuration/application profiles = %#v, %v", profiled, err)
overlaid, err := consumers.LoadNarratioOverlay(fixture("narratio", "pipeline.yml"), "testing")
if err != nil || overlaid.Config.PollInterval.String() != "2m0s" ||
overlaid.Config.ModelProfile != "concise" || len(overlaid.Sources()) != 3 {
t.Fatalf("configuration overlay and application model profile = %#v, %v", overlaid, err)
}
if source := overlaid.Sources()[2]; source.Label != "overlays/testing.yml" || source.Layer != configkit.LayerOverlay {
t.Fatalf("Narratio overlay source = %#v", source)
}
notarius, err := consumers.LoadNotarius(fixture("notarius", "config.yml"))
@@ -100,6 +125,10 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) {
if _, ok := notarius.Config.Bindings[0].Options["window_units"].(int64); !ok {
t.Fatalf("dynamic integer type = %T", notarius.Config.Bindings[0].Options["window_units"])
}
integerNotarius, err := consumers.LoadNotarius(fixture("notarius", "integer-version.yml"))
if err != nil || integerNotarius.Config.Version != notarius.Config.Version || integerNotarius.Config.Version != "4" {
t.Fatalf("Notarius version forms = %#v / %#v, %v", notarius.Config, integerNotarius, err)
}
weather, err := consumers.LoadWeather(configkit.File(fixture("weather", "config.yml")))
if err != nil || weather.Config.Reports["daily"].Modules[1].Options.Sections[0] != "short_term" {
@@ -124,8 +153,19 @@ func TestMigrationFixturesFailWithStableCodesAndPaths(t *testing.T) {
path string
}{
{
name: "integer schema version",
load: func() error { _, err := consumers.LoadNotarius(fixture("notarius", "integer-version.yml")); return err },
name: "unsupported application version",
load: func() error {
_, err := consumers.LoadNotarius(fixture("notarius", "unsupported-version.yml"))
return err
},
code: configkit.CodeVersionError, path: "/version",
},
{
name: "non-integer numeric version",
load: func() error {
_, err := consumers.LoadNotarius(fixture("notarius", "noninteger-version.yml"))
return err
},
code: configkit.CodeVersionType, path: "/version",
},
{
@@ -179,7 +219,7 @@ func TestMigrationFixturesFailWithStableCodesAndPaths(t *testing.T) {
if len(diagnostics) == 0 || diagnostics[0].Code != test.code || diagnostics[0].Path.String() != test.path {
t.Fatalf("diagnostics = %#v", diagnostics)
}
if strings.Contains(diagnostics[0].Message, "wrong-before-profile") {
if strings.Contains(diagnostics[0].Message, "wrong-before-overlay") {
t.Fatalf("diagnostic exposed raw parser input: %#v", diagnostics[0])
}
})
@@ -225,12 +265,89 @@ func TestWeatherLoadedAndConstructedRulesAgreeWithoutMutation(t *testing.T) {
}
}
func TestNotariusPathExampleUsesEntryProvenance(t *testing.T) {
main := fixture("notarius-paths", "main.yml")
declared := fixture("notarius-paths", "declarations", "path.yml")
declaredCanonical := canonicalFixture(t, declared)
wantImported := filepath.Join(filepath.Dir(declaredCanonical), "..", "assets", "imported.txt")
result, err := consumers.LoadNotariusPaths(main, filepath.Join(t.TempDir(), "invocation"))
if err != nil || result.Config.Reference != wantImported {
t.Fatalf("imported reference = %#v, %v", result, err)
}
var effective bytes.Buffer
if err := result.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), wantImported) ||
strings.Contains(effective.String(), "PATH_REFERENCE_SECRET_CANARY") {
t.Fatalf("effective output = %q, %v", effective.String(), err)
}
explanation, found := result.Explain(configkit.Keys("reference"))
if !found || len(explanation.History) == 0 || explanation.History[len(explanation.History)-1].Before.YAML != "../assets/imported.txt\n" ||
len(explanation.History[len(explanation.History)-1].Reasons) != 1 || explanation.History[len(explanation.History)-1].Reasons[0] != "resolved relative reference from entry source" {
t.Fatalf("reference explanation = %#v, %v", explanation, found)
}
sensitive, found := result.Explain(configkit.Keys("sensitive"))
var rendered bytes.Buffer
writeErr := result.WriteExplanation(&rendered, configkit.Keys("sensitive"))
if !found || !sensitive.Effective.Redacted || writeErr != nil ||
strings.Contains(fmt.Sprintf("%#v %s", sensitive, rendered.String()), "PATH_REFERENCE_SECRET_CANARY") {
t.Fatalf("sensitive inspection = %#v, %q, %v", sensitive, rendered.String(), writeErr)
}
defaults := fixture("notarius-paths", "defaults.yml")
defaultCanonical := canonicalFixture(t, defaults)
defaultResult, err := consumers.LoadNotariusPaths(defaults, filepath.Join(t.TempDir(), "invocation"))
wantDefault := filepath.Join(filepath.Dir(defaultCanonical), "defaults", "reference.txt")
if err != nil || defaultResult.Config.Reference != wantDefault {
t.Fatalf("default reference = %#v, %v", defaultResult, err)
}
invocation := filepath.Join(t.TempDir(), "caller")
cliResult, err := consumers.LoadNotariusPaths(main, invocation,
configkit.Override{Path: configkit.Keys("reference"), Value: "cli/reference.txt", Flag: "--reference"})
wantCLI := filepath.Join(invocation, "cli", "reference.txt")
if err != nil || cliResult.Config.Reference != wantCLI {
t.Fatalf("CLI reference = %#v, %v", cliResult, err)
}
missingBase, err := consumers.LoadNotariusPaths(main, "",
configkit.Override{Path: configkit.Keys("reference"), Value: "cli/reference.txt", Flag: "--reference"})
if err != nil || missingBase.Config.Reference != "cli/reference.txt" || len(missingBase.Diagnostics()) != 1 ||
missingBase.Diagnostics()[0].Code != configkit.CodeResolveWarning {
t.Fatalf("missing CLI base = %#v, %v", missingBase, err)
}
absolute := filepath.Join(t.TempDir(), "absolute.txt")
absoluteResult, err := consumers.LoadNotariusPaths(main, invocation,
configkit.Override{Path: configkit.Keys("reference"), Value: absolute, Flag: "--reference"})
if err != nil || absoluteResult.Config.Reference != absolute {
t.Fatalf("absolute reference = %#v, %v", absoluteResult, err)
}
target := fixture("notarius-paths", "symlink-target.yml")
link := filepath.Join(t.TempDir(), "linked-config.yml")
if err := os.Symlink(target, link); err != nil {
t.Skipf("symlink fixture unavailable: %v", err)
}
symlinkResult, err := consumers.LoadNotariusPaths(link, invocation)
targetCanonical := canonicalFixture(t, target)
wantSymlink := filepath.Join(filepath.Dir(targetCanonical), "..", "assets", "target.txt")
if err != nil || symlinkResult.Config.Reference != wantSymlink {
t.Fatalf("symlink reference = %#v, %v", symlinkResult, err)
}
}
func fixture(parts ...string) string {
_, file, _, _ := runtime.Caller(0)
values := append([]string{filepath.Dir(file), "testdata"}, parts...)
return filepath.Join(values...)
}
func canonicalFixture(t *testing.T, path string) string {
t.Helper()
canonical, err := filepath.EvalSymlinks(path)
if err != nil {
t.Fatal(err)
}
return canonical
}
func preserveEnvironment(name string) func() {
value, present := os.LookupEnv(name)
return func() {

View 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
}

View 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
View 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)
}

View 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)
}
}

View 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
}

View 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)
}

View 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)
}
}

View 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
}

View 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)
}
}

View 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")
}
}

View 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)
}
}

View 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"),
},
},
})
}

View 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)
}
}

View 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"),
}}}},
})
}

View 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

View File

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

View File

@@ -0,0 +1,3 @@
compact:
format: text
temperature: 0.2

View File

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

View File

@@ -0,0 +1,3 @@
weather:
extends: compact
temperature: 0.4

View File

@@ -0,0 +1,7 @@
brief:
version: "1"
default_profile: compact
content_file: welcome.txt
messages:
- role: user
content: Write a concise weather summary.

View File

@@ -0,0 +1,3 @@
custom:
extends: brief
default_profile: weather

View File

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

View File

@@ -0,0 +1 @@
Welcome to the weather collection.

View File

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

View File

@@ -0,0 +1,2 @@
brief:
version: "2"

View File

@@ -0,0 +1,10 @@
presets:
base:
enabled: false
settings:
primary: {width: 10}
secondary: {extends: primary}
custom:
extends: base
settings:
primary: {width: 20}

View File

@@ -0,0 +1,7 @@
configkit:
imports:
- path: ../shared.yml
enabled: true
workers: 4
templates:
welcome: messages/welcome.txt

View File

@@ -0,0 +1 @@
Welcome from the bundle.

View File

@@ -0,0 +1,2 @@
labels:
origin: bundle

View File

@@ -0,0 +1,3 @@
mode: production
enabled: false
workers: 0

View File

@@ -0,0 +1 @@
mode: unsupported

Some files were not shown because too many files have changed in this diff Show More