Plan structured effective configuration diffs

This commit is contained in:
2026-10-09 04:55:29 +00:00
parent 1a885934c2
commit 6f59cdd237

259
docs/roadmap/diff.md Normal file
View File

@@ -0,0 +1,259 @@
# 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.bak` and `app.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](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.
- 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
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](../../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.
- [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.
- [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.
- 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.
## 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](../development.md). 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.