Finalize the effective configuration diff roadmap

This commit is contained in:
2026-10-09 05:02:49 +00:00
parent 6f59cdd237
commit e82b1c9054

View File

@@ -1,39 +1,28 @@
# Effective Configuration Diff Roadmap # Effective Configuration Diff Roadmap
Status: proposed; implementation has not started. Execute the stages below in Status: design agreed; implementation has not started. This document owns the
order. This document owns the targeted end state, implementation sequence, and targeted end state, selected contract, and work needed from the current baseline.
remaining decisions; it does not describe an available API. A future staged implementation plan will own execution order. No diff API is
available yet.
## Targeted end state ## Targeted end state
ConfigKit compares a left, fully resolved effective configuration with a right, ConfigKit compares the canonical effective inspection values retained by two
fully resolved effective configuration and returns structured differences. successful loads and returns structured differences. Consumers decide how to
Consumers decide how to render those differences: a changes table, a tree, or render those differences: a changes table, a tree, or a changed-region diff are
a conventional diff are possible presentations. ConfigKit owns comparison and possible presentations. ConfigKit owns comparison and safe value disclosure,
safe value disclosure, while applications own presentation and labels. while applications own presentation and labels.
Both configurations must have completed the normal loading lifecycle, including Both configurations have completed the normal loading lifecycle, including
typed decoding, resolution, secret lookup, and validation. Invalid input on typed decoding, resolution, secret lookup, and validation. Invalid input on
either side produces the existing load failure and diagnostics, with no usable either side produces the existing load failure and diagnostics, with no usable
diff. Comparing successful results must not rerun callbacks, reopen sources, or diff. Comparing successful results does not rerun callbacks, reopen sources, or
resolve credentials again. Later mutation of caller-owned `Result.Config` must resolve credentials again. Later mutation of caller-owned `Result.Config` does
not change the frozen values being compared. not change the frozen values being compared.
Each difference identifies a semantic path and the left and right values at that The comparison is over retained inspection values, not every value accessible
path. Absence must be distinguishable from explicit null. Additions, removals, through the returned Go configuration. In particular, typed secret references
and replacements must be identifiable without parsing presentation markers. contribute their configured environment names, not resolved credential bytes.
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: The motivating workflows are:
@@ -42,22 +31,138 @@ The motivating workflows are:
then inspect the overlay's effective impact, including subsequent resolution. then inspect the overlay's effective impact, including subsequent resolution.
An overlay alone is not the right side of the second workflow: omitted overlay An overlay alone is not the right side of the second workflow: omitted overlay
keys retain base values. Both workflows require successful, independent loads; keys retain base values. Both workflows use successful, independent loads;
applications control their definitions, options, and external inputs. Using the applications control their definitions, options, and external inputs. Using the
same definition and controlled inputs makes comparisons easier to interpret, same definition and controlled inputs makes comparisons easier to interpret.
but ConfigKit must not invent an identity check for callback functions. Sharing a Go type does not prove equal defaults or callbacks, and comparison
does not require callback-function identity checks.
This feature does not include input-text comparison, invalid-document inspection, ## Selected public contract
### Entry point and records
The target entry point is:
```go
Diff[T any](left, right *Result[T]) ([]Difference, error)
```
It compares results of the same configuration type. Nil or uninitialized results
produce an error rather than an empty successful diff. Valid results with equal
effective values produce no difference records. Loading remains a separate
operation; this API does not aggregate load failures or orchestrate callbacks.
The root-owned `Difference` record contains:
- `Path Path`: the semantic path of the changed region;
- `Left ValueSnapshot`: the safe value before the change;
- `Right ValueSnapshot`: the safe value after the change; and
- `HiddenChanged bool`: whether at least one actual change in the region is
withheld by diff redaction, as defined below.
Both sides reuse the existing `ValueSnapshot` representation: `Present`,
`Redacted`, and deterministic `YAML`. Absence is `Present=false` with empty
YAML; explicit null is present and renders as `null`. Presence identifies
additions and removals. A record with both sides present describes a replacement
at its reported path. Record membership establishes a change even when both
safe snapshots render identically.
Paths and records are detached. Consumer mutation does not affect subsequent
comparisons or either result's existing inspection state. The public facade
exposes no internal document or parser types.
### Equality, granularity, and ordering
Compare the underlying canonical effective inspection values before redaction.
Equal values produce no differences even if source locations, explicit-input
status, or loading histories differ. An explicit zero and an omitted field
defaulting to zero therefore compare equally. Presence and provenance remain
separately inspectable through each result's existing APIs. Original comments,
formatting, and mapping declaration order do not participate in equality.
Retain the existing comparator's granularity:
- Mappings compare recursively. Additions and removals report the affected key;
changes beneath mappings report the changed descendant region.
- Sequences compare atomically: any change reports the sequence path.
- Scalar, cross-kind, and null/container replacements report the affected path.
Sequence comparison introduces no element alignment or public correspondence
between old and new indices. Records are ordered by canonical pointer spelling,
matching the existing comparator. Paths retain semantic key/index intent and
proper escaping, including numeric mapping keys.
### Cross-configuration redaction
Apply the applicable sensitivity from both loads before returning either side's
value. A classification on either side protects both snapshots. Include
historical and dependency-derived classification, classified ancestors and
descendants, and paths absent from one effective tree.
Where the two shapes permit corresponding sensitive regions to be identified,
mask those regions while retaining public sibling values. Do not treat matching
pointer text alone as proof of matching mapping-key versus sequence-index shape.
When a structural replacement prevents safe correspondence for a classified
region, mask the complete reported replacement region on both present sides.
For example, replacing a mapping containing a classified descendant with a
scalar masks the whole mapping/scalar replacement, rather than trying to apply
the old descendant path to the scalar. This fallback also covers incompatible
mapping/sequence shapes within an atomic replacement. An absent side remains
absent; redaction does not fabricate a value for it.
`ValueSnapshot.Redacted` describes masking, not change. `HiddenChanged` is true
when underlying value or presence differences occur in a region withheld by the
combined redaction policy. It is false when only public content changes and all
masked regions retain equal values and presence. This includes atomic sequence
records: an unchanged masked member does not alone set the flag. The complete
replacement fallback sets the flag because the changed region itself is
withheld. The flag does not identify individual changed children, counts, or
sequence correspondence, and does not assert that every redacted child changed.
For a wholly masked changed value, record membership and `HiddenChanged` let a
consumer show `[REDACTED]` on the left and `[CHANGED]` on the right. These
strings are presentation choices, not configuration values or the encoding of
change state. Literal marker-looking input remains ordinary input; applications
use record metadata rather than interpreting YAML marker text.
Errors and returned records disclose neither classified values nor credential
bytes.
### Credential boundary and renderer context
Compare classified values and configured secret reference names retained in the
effective inspection snapshots. Rotation of a resolved credential behind an
unchanged reference is outside this comparison. Diff does not read mutable
`Result.Config`, reread the environment, or introduce credential retention or
credential-derived comparison tokens.
Return changed regions only. The records do not include paired full-document
context or a structured dynamic value tree, and do not alone supply unchanged
context for unified hunks. Calling `WriteEffective` independently on each side
is not a substitute for cross-protected context because each call applies only
that result's sensitivity. Consumers can render the returned safe regions
without reconstructing unredacted context.
## Scope and architectural fit
This extends ConfigKit's existing snapshot-based inspection responsibilities.
Applications can compare completed configurations without duplicating structural
comparison, presence handling, or safe value disclosure. Loading and composition
rules remain unchanged.
The feature does not include input-text comparison, invalid-document inspection,
patch application, generating a reconstructing overlay, fingerprints, automatic patch application, generating a reconstructing overlay, fingerprints, automatic
reload, or a built-in renderer. A diff may describe removals that existing overlay reload, resolved-credential comparison, cross-type comparison, or a built-in
semantics cannot express. Unchanged context for a unified renderer is a separate renderer. A diff may describe removals that existing overlay semantics cannot
data-shape decision below. express. These boundaries define this feature's selected scope, not intrinsic
correctness restrictions on every possible future extension.
## Current baseline ## Current baseline
- [Deferred scope](deferred.md) currently excludes configuration diffs and - [Deferred scope](deferred.md) currently excludes configuration diffs and
fingerprints. This roadmap proposes reconsidering only the diff boundary; fingerprints. This design reconsiders only the diff boundary; fingerprints
it does not silently change that document or approve fingerprints. and unrelated exclusions remain deferred.
- The [configuration contract](../config.md#typed-projection-and-ownership) - The [configuration contract](../config.md#typed-projection-and-ownership)
describes canonical typed projection and private structural comparison. describes canonical typed projection and private structural comparison.
[The existing comparator](../../internal/engine/document_compare.go) reports [The existing comparator](../../internal/engine/document_compare.go) reports
@@ -66,194 +171,79 @@ data-shape decision below.
provenance and sensitivity metadata. provenance and sensitivity metadata.
- Results retain private frozen inspection state. Existing - Results retain private frozen inspection state. Existing
[inspection types](../../inspection.go) include `ValueSnapshot`, with [inspection types](../../inspection.go) include `ValueSnapshot`, with
`Present`, `Redacted`, and deterministic `YAML`. Absence is distinct from null. presence, redaction, and deterministic YAML. Explanation records describe
Existing explanation records describe one load's history, not a comparison one load's history rather than a comparison between loads.
between two loads.
- [Secret classification](../config.md#secret-classification-and-effective-rendering) - [Secret classification](../config.md#secret-classification-and-effective-rendering)
includes historical classification and dependency propagation. Existing includes historical classification and dependency propagation. Existing
snapshots apply one load's sensitivity; a diff needs protection from both snapshots apply one load's sensitivity; diff requires protection from both.
loads, including removed paths and structural replacements.
- [Typed secret references](../config.md#typed-secret-references) retain reference - [Typed secret references](../config.md#typed-secret-references) retain reference
names in inspection, not resolved credential bytes. The existing snapshot names in inspection, not resolved credential bytes.
comparator cannot detect rotation behind an unchanged reference.
- The [public facade](../internal/overview.md) is root-owned and delegates to - The [public facade](../internal/overview.md) is root-owned and delegates to
the engine. Public diff types must follow that boundary and expose no parser the engine. The new operation and records follow that same boundary.
or internal document types.
## Ordered implementation plan ## Work needed from the baseline
### 1. Settle the public contract and scope decision ### Architecture and scope documentation
Resolve the Open Decisions below before implementing dependent behavior. Record Record the inspection boundary extension in an ADR and reconcile the diff
the agreed API shape, equality and granularity rules, sensitivity behavior, exclusion in `deferred.md`, retaining fingerprints and unrelated exclusions.
credential boundary, and error behavior. Distinguish unavailable/nil results Keep planned behavior here until implementation lands. The staged implementation
from valid configurations with no differences; recommend an error for the former. plan should reference the selected contract above rather than reopen it.
Review the extension against the architecture and release policies. Record the ### Private comparison and disclosure
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 Add an engine operation over two successful frozen inspections, reusing the
sensitive nested values to check that the proposed data is sufficient. Agree existing comparator where its semantics match this contract. Sharing helpers
which rendering needs those examples demonstrate; do not add a renderer merely must preserve resolution and validation auditing behavior.
to exercise the API.
Completion: a reviewable contract with no unresolved decisions that block the Implement cross-configuration masking, structural-replacement fallback, and
comparison or disclosure model. `HiddenChanged`. Compare canonical values rather than serialized redacted
output, caller-mutated configuration values, or historical replay. Construct
the needed safe snapshots without replaying every path's full explanation
history. Keep comparison and disclosure distinct so masking does not erase
change detection.
### 2. Implement comparison of frozen inspection state ### Root API and consumer guidance
Add an engine operation over two successful frozen inspections. Reuse the Add root-owned records, the comparison entry point, and explicit conversion
existing comparator where its semantics match the selected contract. Do not across the engine boundary. Document direction, equality, failures, ownership,
change resolution or validation auditing semantics incidentally when sharing secret behavior, and the credential limitation beside declarations.
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, 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 and for comparing a base result with a base-plus-overlay result. Include
consumer-owned formatting and changed-secret indicators using disposable data. resolution and sensitive nested values, demonstrating consumer-owned formatting
No usable diff should be presented when either load fails. and changed-secret indicators with disposable data.
Completion: consumers can implement the motivating workflows through the root ### Behavioral validation and completion
package without internal imports or duplicating structural comparison.
### 5. Validate behavior and update owning documentation Use engine tests for comparison and combined sensitivity, root tests for the
facade and ownership, and consumer examples for the complete workflows. Cover
meaningful risks without duplicating all existing loading tests:
Use engine tests for structural comparison and combined sensitivity, root tests - equality despite mapping order, provenance, or explicit-input differences;
for the public facade and ownership, and consumer examples for complete workflows. canonical equality after defaults and resolution;
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 - addition/removal versus explicit null, empty containers, scalar and kind
replacements, sequence changes, and escaped or numeric mapping keys; replacements, atomic sequence changes, and escaped or numeric mapping keys;
- deterministic ordering and the agreed subtree granularity; - deterministic ordering and reported subtree granularity;
- changed and unchanged sensitive values, classification on only one side, - changed and unchanged sensitive values, classification on only one side,
removed secrets, classified containers, and mixed public/sensitive changes; removed secrets, classified containers, and mixed public/sensitive changes;
- a changed hidden descendant whose rendered left/right values are identical, - cross-kind replacement of a classified descendant, incompatible key/index
and literal strings resembling redaction markers; shapes, and complete-region masking without changing absence semantics;
- the selected reference/credential behavior with controlled environment state; - hidden changes with identical safe YAML, public-only changes alongside
- invalid left/right loads, unavailable results, frozen snapshot behavior, and unchanged masked values with `HiddenChanged=false`, and literal marker-like
returned-record mutation isolation. input;
- changed secret reference names and unchanged names with credential rotation,
using controlled environment state;
- invalid left/right loads, unavailable results, caller mutation after loading,
and returned-record mutation isolation.
Use disposable canaries to check non-disclosure, including failure paths. Use disposable canaries to check non-disclosure, including failure paths. Follow
Run focused tests while implementing, then the everyday check and ownership the focused, everyday, ownership-race, and documentation checks in the
race validation from the [development guide](../development.md). Validate local [development guide](../development.md). New performance benchmarks are not
documentation links. No independent performance benchmarks are part of this required for this feature.
roadmap; the existing request to avoid them remains in force.
Update the configuration contract, consumer guidance, and component overview Update the configuration contract, consumer guidance, and component overview
when their corresponding behavior lands. Keep symbol details in Go declaration when their behavior lands. Keep symbol details in Go declaration documentation.
documentation. Review compatibility and release notes under the release policy; Review compatibility and release notes under the release policy; publication is
publication is separate from feature completion. separate from feature completion. Completion means the selected contract is
implemented, checks pass, and maintained examples and documentation agree.
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.