Files
ConfigKit/docs/roadmap/diff.md

13 KiB

Effective Configuration Diff Roadmap

Status: design agreed; implementation has not started. This document owns the targeted end state, selected contract, and work needed from the current baseline. The implementation plan owns execution order. No diff API is available yet.

Targeted end state

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

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:

  • 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 use successful, independent loads; applications control their definitions, options, and external inputs. Using the 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.

Selected public contract

Entry point and records

The target entry point is:

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, 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 currently excludes configuration diffs and fingerprints. This design reconsiders only the diff boundary; fingerprints and unrelated exclusions remain deferred.
  • 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, with presence, redaction, and deterministic YAML. Explanation records describe one load's history rather than a comparison between loads.
  • Secret classification includes historical classification and dependency propagation. Existing snapshots apply one load's sensitivity; diff requires protection from both.
  • Typed secret references retain reference names in inspection, not resolved credential bytes.
  • The public facade is root-owned and delegates to the engine. The new operation and records follow that same boundary.

Work needed from the baseline

Architecture and scope documentation

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.

Private comparison and disclosure

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.

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.

Root API and consumer guidance

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. Include resolution and sensitive nested values, demonstrating consumer-owned formatting and changed-secret indicators with disposable data.

Behavioral validation and completion

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:

  • 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, 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;
  • 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. Follow the focused, everyday, ownership-race, and documentation checks in the development guide. New performance benchmarks are not required for this feature.

Update the configuration contract, consumer guidance, and component overview 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.