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
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.
Status: design agreed; implementation has not started. This document owns the
targeted end state, selected contract, and work needed from the current baseline.
A future staged implementation plan will own execution order. No diff API is
available yet.
## 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.
ConfigKit compares the canonical effective inspection values retained by two
successful loads and returns structured differences. Consumers decide how to
render those differences: a changes table, a tree, or a changed-region 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
Both configurations 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
diff. Comparing successful results does not rerun callbacks, reopen sources, or
resolve credentials again. Later mutation of caller-owned `Result.Config` does
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 comparison is over retained inspection values, not every value accessible
through the returned Go configuration. In particular, typed secret references
contribute their configured environment names, not resolved credential bytes.
The motivating workflows are:
@@ -42,22 +31,138 @@ The motivating workflows are:
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;
keys retain base values. Both workflows use 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.
same definition and controlled inputs makes comparisons easier to interpret.
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
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.
reload, resolved-credential comparison, cross-type comparison, or a built-in
renderer. A diff may describe removals that existing overlay semantics cannot
express. These boundaries define this feature's selected scope, not intrinsic
correctness restrictions on every possible future extension.
## Current baseline
- [Deferred scope](deferred.md) currently excludes configuration diffs and
fingerprints. This roadmap proposes reconsidering only the diff boundary;
it does not silently change that document or approve fingerprints.
fingerprints. This design reconsiders only the diff boundary; fingerprints
and unrelated exclusions remain deferred.
- The [configuration contract](../config.md#typed-projection-and-ownership)
describes canonical typed projection and private structural comparison.
[The existing comparator](../../internal/engine/document_compare.go) reports
@@ -66,194 +171,79 @@ data-shape decision below.
provenance and sensitivity metadata.
- Results retain private frozen inspection state. Existing
[inspection types](../../inspection.go) include `ValueSnapshot`, with
`Present`, `Redacted`, and deterministic `YAML`. Absence is distinct from null.
Existing explanation records describe one load's history, not a comparison
between two loads.
presence, redaction, and deterministic YAML. Explanation records describe
one load's history rather than a comparison between loads.
- [Secret classification](../config.md#secret-classification-and-effective-rendering)
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.
snapshots apply one load's sensitivity; diff requires protection from both.
- [Typed secret references](../config.md#typed-secret-references) retain reference
names in inspection, not resolved credential bytes. The existing snapshot
comparator cannot detect rotation behind an unchanged reference.
names in inspection, not resolved credential bytes.
- 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
or internal document types.
the engine. The new operation and records follow that same boundary.
## 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
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.
Record the inspection boundary extension in an ADR and reconcile the diff
exclusion in `deferred.md`, retaining fingerprints and unrelated exclusions.
Keep planned behavior here until implementation lands. The staged implementation
plan should reference the selected contract above rather than reopen it.
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.
### Private comparison and disclosure
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.
Add an engine operation over two successful frozen inspections, reusing the
existing comparator where its semantics match this contract. Sharing helpers
must preserve resolution and validation auditing behavior.
Completion: a reviewable contract with no unresolved decisions that block the
comparison or disclosure model.
Implement cross-configuration masking, structural-replacement fallback, and
`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
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 root-owned records, the comparison entry point, and explicit conversion
across the engine boundary. Document direction, equality, failures, ownership,
secret behavior, and the credential limitation beside declarations.
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.
and for comparing a base result with a base-plus-overlay result. Include
resolution and sensitive nested values, demonstrating consumer-owned formatting
and changed-secret indicators with disposable data.
Completion: consumers can implement the motivating workflows through the root
package without internal imports or duplicating structural comparison.
### Behavioral validation and completion
### 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
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;
- equality despite mapping order, provenance, or explicit-input differences;
canonical 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;
replacements, atomic sequence changes, and escaped or numeric mapping keys;
- deterministic ordering and reported 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.
- cross-kind replacement of a classified descendant, incompatible key/index
shapes, and complete-region masking without changing absence semantics;
- hidden changes with identical safe YAML, public-only changes alongside
unchanged masked values with `HiddenChanged=false`, and literal marker-like
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.
Run focused tests while implementing, then the everyday check and ownership
race validation from the [development guide](../development.md). Validate local
documentation links. No independent performance benchmarks are part of this
roadmap; the existing request to avoid them remains in force.
Use disposable canaries to check non-disclosure, including failure paths. Follow
the focused, everyday, ownership-race, and documentation checks in the
[development guide](../development.md). New performance benchmarks are not
required for this feature.
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.
when their 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 means the selected contract is
implemented, checks pass, and maintained examples and documentation agree.