14 KiB
Effective Configuration Diff Roadmap
Status: proposed; implementation has not started. Execute the stages below in order. This document owns the targeted end state, implementation sequence, and remaining decisions; it does not describe an available API.
Targeted end state
ConfigKit compares a left, fully resolved effective configuration with a right, fully resolved effective configuration and returns structured differences. Consumers decide how to render those differences: a changes table, a tree, or a conventional diff are possible presentations. ConfigKit owns comparison and safe value disclosure, while applications own presentation and labels.
Both configurations must have completed the normal loading lifecycle, including
typed decoding, resolution, secret lookup, and validation. Invalid input on
either side produces the existing load failure and diagnostics, with no usable
diff. Comparing successful results must not rerun callbacks, reopen sources, or
resolve credentials again. Later mutation of caller-owned Result.Config must
not change the frozen values being compared.
Each difference identifies a semantic path and the left and right values at that path. Absence must be distinguishable from explicit null. Additions, removals, and replacements must be identifiable without parsing presentation markers. Equal effective values produce no differences, even if their source locations, explicit-input status, or loading histories differ. Original comments, formatting, and mapping declaration order do not participate in equality.
Secret handling must preserve detectable changes without exposing classified
values. Compare the underlying retained configuration values before redaction;
apply sensitivity from both sides before returning either side's value.
A consumer must be able to indicate a changed sensitive value, for example as
[REDACTED] on the left and [CHANGED] on the right. These strings are consumer
presentation choices, not configuration values or the encoding of change state.
The decisions below address nested redaction and the boundary around resolved
credential bytes.
The motivating workflows are:
- Load
app.conf.bakandapp.conf, then inspect effective differences. - Load a base configuration and separately load it with a selected overlay, then inspect the overlay's effective impact, including subsequent resolution.
An overlay alone is not the right side of the second workflow: omitted overlay keys retain base values. Both workflows require successful, independent loads; applications control their definitions, options, and external inputs. Using the same definition and controlled inputs makes comparisons easier to interpret, but ConfigKit must not invent an identity check for callback functions.
This feature does not include input-text comparison, invalid-document inspection, patch application, generating a reconstructing overlay, fingerprints, automatic reload, or a built-in renderer. A diff may describe removals that existing overlay semantics cannot express. Unchanged context for a unified renderer is a separate data-shape decision below.
Current baseline
- Deferred scope currently excludes configuration diffs and fingerprints. This roadmap proposes reconsidering only the diff boundary; it does not silently change that document or approve fingerprints.
- The configuration contract describes canonical typed projection and private structural comparison. The existing comparator reports mapping additions, removals, and replacements, ignores mapping order, treats sequences atomically, and sorts changes by pointer spelling. It ignores provenance and sensitivity metadata.
- Results retain private frozen inspection state. Existing
inspection types include
ValueSnapshot, withPresent,Redacted, and deterministicYAML. Absence is distinct from null. Existing explanation records describe one load's history, not a comparison between two loads. - Secret classification includes historical classification and dependency propagation. Existing snapshots apply one load's sensitivity; a diff needs protection from both loads, including removed paths and structural replacements.
- Typed secret references retain reference names in inspection, not resolved credential bytes. The existing snapshot comparator cannot detect rotation behind an unchanged reference.
- The public facade is root-owned and delegates to the engine. Public diff types must follow that boundary and expose no parser or internal document types.
Ordered implementation plan
1. Settle the public contract and scope decision
Resolve the Open Decisions below before implementing dependent behavior. Record the agreed API shape, equality and granularity rules, sensitivity behavior, credential boundary, and error behavior. Distinguish unavailable/nil results from valid configurations with no differences; recommend an error for the former.
Review the extension against the architecture and release policies. Record the
significant boundary change in an ADR and reconcile the diff exclusion in
deferred.md, retaining unrelated exclusions. Keep planned behavior in the
roadmap until it lands.
Use a simple two-file consumer and a layered consumer with resolution and sensitive nested values to check that the proposed data is sufficient. Agree which rendering needs those examples demonstrate; do not add a renderer merely to exercise the API.
Completion: a reviewable contract with no unresolved decisions that block the comparison or disclosure model.
2. Implement comparison of frozen inspection state
Add an engine operation over two successful frozen inspections. Reuse the existing comparator where its semantics match the selected contract. Do not change resolution or validation auditing semantics incidentally when sharing helpers. Compare canonical effective values, not serialized redacted output, caller-mutated configuration values, or historical replay.
Define deterministic record ordering and parent/subtree reporting. Retain semantic key/index intent and proper path escaping. Construct only the value snapshots and additional metadata needed by the chosen result shape; avoid replaying every path's full explanation history.
Completion: ordinary additions, removals, replacements, and equality work with the selected granularity and unavailable-result behavior.
3. Apply cross-configuration sensitivity
Combine the applicable sensitivity of the two inspections for disclosure on both sides. Account for classified ancestors and descendants, historical classification, dependency-derived sensitivity, missing paths, and container kind changes. Do not assume equal pointer text implies equal key/index shape.
Detect changes before masking and preserve their existence even when safe YAML is identical. Keep redaction state separate from literal marker-looking input. For containers with masked descendants, implement the selected hidden-change representation without falsely claiming that every redacted child changed. Errors and returned records must not disclose classified values or credentials.
Completion: changes remain identifiable under redaction, either side's classification protects both values, and returned data meets the agreed secret boundary.
4. Expose the root API and demonstrate consumer use
Add root-owned records and the selected comparison entry point, with explicit conversion across the engine boundary. Return detached paths and records; consumer mutation must not affect later comparisons or existing inspection. Document symbols beside declarations, including direction, equality, failures, snapshot lifetime, secret behavior, and the credential limitation if selected.
Add executable examples for loading both files and handling each load failure, and for comparing a base result with a base-plus-overlay result. Demonstrate consumer-owned formatting and changed-secret indicators using disposable data. No usable diff should be presented when either load fails.
Completion: consumers can implement the motivating workflows through the root package without internal imports or duplicating structural comparison.
5. Validate behavior and update owning documentation
Use engine tests for structural comparison and combined sensitivity, root tests for the public facade and ownership, and consumer examples for complete workflows. Cover meaningful risks without duplicating all existing loading tests:
- equality despite mapping order or provenance changes; canonical value equality after defaults and resolution;
- addition/removal versus explicit null, empty containers, scalar and kind replacements, sequence changes, and escaped or numeric mapping keys;
- deterministic ordering and the agreed subtree granularity;
- changed and unchanged sensitive values, classification on only one side, removed secrets, classified containers, and mixed public/sensitive changes;
- a changed hidden descendant whose rendered left/right values are identical, and literal strings resembling redaction markers;
- the selected reference/credential behavior with controlled environment state;
- invalid left/right loads, unavailable results, frozen snapshot behavior, and returned-record mutation isolation.
Use disposable canaries to check non-disclosure, including failure paths. Run focused tests while implementing, then the everyday check and ownership race validation from the development guide. Validate local documentation links. No independent performance benchmarks are part of this roadmap; the existing request to avoid them remains in force.
Update the configuration contract, consumer guidance, and component overview when their corresponding behavior lands. Keep symbol details in Go declaration documentation. Review compatibility and release notes under the release policy; publication is separate from feature completion.
Completion: checks pass, consumer examples demonstrate the agreed data contract, and implemented documentation agrees with the public API. Mark the roadmap complete only after these outcomes are achieved.
Open Decisions
Comparison entry point and type compatibility
Recommendation: start with a function over two successful results of the same
configuration type, illustratively
Diff[T any](left, right *Result[T]) ([]Difference, error). This fits both
motivating workflows, reuses normal load failures, and avoids a second loading
lifecycle. Return an error for nil or uninitialized results. The signature and
names remain proposals; sharing T does not prove equal defaults or callbacks.
Alternative: compare results with different configuration types through a root-owned inspection handle or another deliberately designed API. This supports schema migrations and comparisons across applications, but introduces another public abstraction or more type parameters and requires explaining how differing schemas affect equality. Choose it if a concrete consumer needs that flexibility; same-type comparison is a recommended initial surface, not an intrinsic correctness restriction. A combined load-and-diff convenience operation is also viable, but adds failure aggregation and callback orchestration without being necessary for the requested workflows.
Value representation and renderer context
Recommendation: use detached Difference records with Path, Left, and
Right, reusing ValueSnapshot for each side. Presence identifies additions and
removals, while record membership establishes a change. This reuses existing safe
inspection semantics without exporting a second dynamic value model. Initially
return changed regions only; do not promise that these records alone support
unified hunks with unchanged context.
Alternative: return a richer diff result with safe paired full-document context
or a structured safe value tree. That better supports context-aware and non-YAML
renderers, but increases the public representation and disclosure surface.
Independently calling WriteEffective on each side is not automatically a safe
substitute for paired context: it does not apply the other side's sensitivity.
If conventional context-rich rendering is required immediately, include paired
context in the contract rather than leaving consumers to reconstruct it unsafely.
Granularity and changes inside redacted containers
Recommendation: retain recursive mapping comparison and atomic sequence/subtree
replacement as the initial reporting model. It matches existing comparison and
does not invent correspondence between sequence elements. For a wholly masked
changed value, the difference record is sufficient for a consumer's [CHANGED]
indicator. For mixed containers, add explicit metadata identifying whether the
replacement includes hidden value changes; ValueSnapshot.Redacted alone only
means that some content is masked. Select the exact metadata shape in stage 1.
Alternative: expose finer-grained nested changes, including a defined sequence alignment, so renderers can identify individual changed hidden descendants. This gives more precise displays but needs rules for insertions, reordering, duplicates, and cross-kind replacements. Index-by-index comparison is simpler but can misleadingly present shifted elements as replacements. A conservative whole-container display is viable with atomic records, provided it labels the container as changed rather than asserting which masked children changed.
Resolved credentials versus retained configuration values
Recommendation: compare classified values and secret reference names retained in the effective snapshots, and explicitly exclude detection of resolved credential rotation behind an unchanged reference. This preserves the existing inspection boundary and avoids retaining credential-derived comparison material. Confirm that this limitation satisfies the intended meaning of changed secrets before implementation.
Alternative: support comparison of resolved credentials as well. It would meet
the broader expectation that any effective credential change is detectable, but
requires a deliberate trusted comparison mechanism beyond existing inspection.
Reading mutable Result.Config would violate frozen-snapshot semantics; retaining
credentials or comparison tokens introduces confidentiality and lifetime choices.
Do not add raw hashes or credential retention as an incidental implementation
detail. Resolve that design and its security consequences first if required.