diff --git a/docs/roadmap/diff.md b/docs/roadmap/diff.md index d80b94d..f0ffe91 100644 --- a/docs/roadmap/diff.md +++ b/docs/roadmap/diff.md @@ -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.