Files
ConfigKit/docs/roadmap/implementation.md

387 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implement effective configuration diffs
Status: planned. Audience: a Luna coding agent implementing one stage per prompt,
in numerical order. The [diff roadmap](diff.md) owns the agreed target contract
and scope. This document owns implementation steps and completion evidence.
Planning baseline: `e82b1c90543a6ab92138ee8c0c2a4cd4daea8af0`.
No diff API exists at this baseline. This plan adds comparison of successful
loads' frozen canonical inspection values; it does not change loading rules.
## Working instructions
- Begin each stage with [development.md](../development.md), applicable policies,
working-tree inspection, and the owning implementation and tests. Follow
`AGENTS.md`'s graph-first code discovery guidance. Preserve unrelated work.
- Implement only the current stage and its relevant tests/documentation. Update
its completion status and evidence here, then commit the validated nonempty
unit with a short plain-English message. Keep subsequent stages planned.
- Use the roadmap's same-type `Diff`, root-owned `Difference`, existing
`ValueSnapshot`, atomic sequences, cross-protected redaction, and reference-only
credential boundary. Do not replace these with an easier partial feature.
- Keep loading, callback auditing, secret classification, existing rendering,
and historical explanation behavior unchanged. No dependencies, toolchain
changes, new resource caps, callback identity checks, release gates, renderer,
sequence alignment, credential comparison tokens, or background work are
needed. These are this implementation's scope boundaries, not new repository
prohibitions on unrelated future designs.
- New diff state is operation-local. Read retained trees/indexes without mutation;
do not add load-time caches, retain paired documents after comparison, or share
mutable state between calls. Build safe output before exposing records.
- Run each stage's focused checks and the everyday `./scripts/check.sh` before
committing production changes. Documentation-only stages need local-link and
whitespace checks. Run engine race tests after the integrated engine stage
and the full race suite during final verification. Reuse successful checks for
unchanged content. These are development checks, not new release gates.
- Tests remain offline and use disposable canaries, temporary files, and
controlled environment variables. Environment-mutating tests are not parallel.
Reuse existing loading tests rather than reproduce the loader. Benchmarks are
optional; no machine-specific thresholds or public complexity promises.
- Do not push, tag, publish, upgrade consumer dependencies, or retire unfinished
roadmaps. Release preparation and inclusion in a particular version are
separate decisions; do not assume this feature belongs in the prepared v0.6.0
note.
## Implementation decisions
These decisions complete the roadmap's implementation details without extending
its public contract.
1. Root `diff.go` defines `Difference` and
`Diff[T any](left, right *Result[T]) ([]Difference, error)`. Engine `diff.go`
owns `Diff(left, right *Inspection) ([]Difference, error)` and an internal-
facing `Difference` record. Root conversion remains explicit, never an alias.
2. A usable result has a non-nil retained inspection with a non-nil final tree.
A valid defaults-only load is usable. Nil/zero results fail with a value-free
error and no records. Do not validate `Result.Config`, options identity,
source identity, or matching defaults. Do not add a public error hierarchy or
diagnostic codes just for this feature; exact error wording is not a promise.
3. Obtain regions with `compareConfigurationValues(left.final, right.final)`.
Resolve their nodes with the existing shape-aware document lookup and each
inspection's immutable lookup. Preserve the comparator's pointer ordering,
canonical scalar equality, atomic sequences, and owned paths. Do not alter
comparison used by resolution or validation.
4. Combine true entries from both `Inspection.secrets` maps once per operation.
Those maps already include historical and dependency-derived classification;
do not rerun predicates or propagation. Canonical pointer classification
retains its existing meaning. Actual document traversal determines whether
a token is a mapping key or sequence index; matching text alone does not prove
structural correspondence.
5. Use a small private diff-local sensitivity lookup for ancestor/equal masking
and strict-descendant classification. Reuse `pathPrefixIndex` with owner-held
IDs where useful. Do not scan the entire classification map for each visited
node or each difference, or build a second general path framework. Retain
historical-only selectors even when absent from both current trees.
6. Prepare safe paired subtrees by traversing the union of mapping keys and
corresponding sequence positions. Position pairing is solely for disclosure
and hidden-change detection; the public delta remains the whole sequence,
with no element alignment. Absence is a nil node; explicit null is a present
`document.Null` node.
7. If a selector masks the current path by equality or ancestry, replace each
present node there with the ordinary redaction marker. Otherwise recurse
through compatible mappings/sequences. A missing key/index or entirely absent
side is absence, not a kind mismatch: mask present sensitive descendants and
preserve public siblings.
8. A kind mismatch between two present nodes requires complete reported-region
masking if there is a classified strict descendant of that mismatch path.
A sensitive ancestor/equal selector already masks it under item 7. Check
nested mismatches within atomic sequence/addition/replacement regions, not
only the reported roots. On such a mismatch, mask both present roots of the
original reported region; leave an absent side absent. An unrelated sensitive
sibling does not trigger fallback for a public-only mismatch. Missing or
incompatible historical descendant selectors still require this fallback.
9. Compute `HiddenChanged` from raw value/presence differences at masking
frontiers. At a masked frontier use `configurationValuesEqual` on the two
raw nodes; otherwise OR the children's flags. A classified but unchanged
subtree yields false even alongside a public change. Complete-region fallback
yields true for an emitted changed region. Do not compare redacted YAML, use
`Redacted` as a change flag, or expose child identities/counts through this flag.
10. Render only already-safe documents through the private YAML adapter, using
existing deterministic snapshot formatting. An absent side is the zero
`ValueSnapshot`. Unexpected serialization failure returns a fixed value-free
error with no usable partial diff, rather than silently substituting a marker
and misreporting `HiddenChanged`. Existing explanation snapshot fallback
behavior remains unchanged. No raw dumper/codec error text is exposed.
11. No-difference results contain zero records; do not promise a distinction
between nil and allocated empty slices. Return detached paths and immutable
YAML strings. Sensitivity differences alone do not emit records for equal
effective values. Frozen snapshots include secret reference names, never
credentials, even when the caller changes the environment after loading.
## Stage 1 — Establish evidence and record the architectural decision
Status: complete. Scope: orientation, baseline evidence, and one ADR; no feature
code or current-behavior documentation changes.
1. Confirm the baseline and inspect the roadmap and relevant code/test owners:
`document_compare.go`, `snapshot.go`, `render.go`, `load.go`, `load_result.go`,
`document_walk.go`, `document_lookup.go`, root `lifecycle.go`, `inspection.go`,
`conversion.go`, and `path.go`.
2. Run `./scripts/check.sh`; record revision, local Go version, results, and any
environmental limitation here. Identify existing coverage for canonical
comparison, snapshot presence/redaction, immutable inspection, and load failure.
3. Write `docs/adr/0010-effective-configuration-diff.md` using the repository's
ADR format and the agreed roadmap. Check the next unused ADR number before
creating it. Record the accepted architectural choice and supporting reasons:
completed-load inspection, canonical equality, shared composition model,
cross-protected snapshots, application-owned presentation, and the credential
boundary. Put rejected choices only in Alternatives Considered. Do not discuss
downstream-specific conventions or use negative parallelism outside that
section. The roadmap continues to own implementation status.
Done: baseline evidence and an accepted decision are recorded. Existing public
behavior remains unchanged; the deferred inventory is reconciled when the API
lands in Stage 10.
### Baseline evidence
- Implementation baseline: `3697a511242d60c860549f09f02b75d1eb2fe660`;
planning baseline: `e82b1c90543a6ab92138ee8c0c2a4cd4daea8af0`. The worktree was
clean before validation. The accepted architectural decision is recorded in
[ADR 0010](../adr/0010-effective-configuration-diff.md).
- `GOTOOLCHAIN=local go version`: `go1.26.8 linux/amd64`.
`GOTOOLCHAIN=local ./scripts/check.sh`: passed formatting, whitespace,
`go test ./...`, `go vet ./...`, and `go build ./...`. No environmental
limitations were encountered.
- Existing owners identified: canonical changed-region comparison and escaped
ordering in `internal/engine/document_compare.go` and
`TestCompareConfigurationValuesSortEscapedPointers`; snapshot presence and
redaction in `internal/engine/snapshot.go`, `internal/engine/render.go`, and
`TestWriteEffectiveRedactsTagsDynamicHistoryAndDependencyCycles`; frozen
inspection state and read-only lookups in `internal/engine/load_result.go`,
`internal/engine/document_lookup.go`, `TestFacadeInspectionUsesFrozenState`,
and `TestConcurrentReadOnlyInspectionQueries`; load-failure behavior in
`TestLoadReturnsNilForSourceSchemaAndDecodeFailures` and
`TestFacadeLoadReturnsFacadeLoadError`.
## Stage 2 — Prepare canonical changed regions
Status: complete. Scope: a private helper and structural tests only.
1. Add `internal/engine/diff.go` with private preparation over two inspections.
Reject unavailable inspection/final state with safe errors and no regions.
2. Reuse `compareConfigurationValues`; package each delta's owned path with
borrowed immutable left/right nodes found through existing lookup helpers.
Do not clone metadata, inspect sources, replay history, project mutable Go
values, or write to retained lookup maps.
3. Add `internal/engine/diff_test.go` coverage for equal roots/mapping order,
additions/removals, null versus absence, empty containers, scalar kinds,
atomic sequences, root changes, lexical pointer order, escaped/empty/numeric
keys, and unavailable inspections. Reuse comparator fixtures where suitable.
Private node fixtures may exercise structural cases not reachable from one
typed schema; full-load tests belong in later stages.
Validate: `go test ./internal/engine -run 'Diff|CompareConfiguration|Path'` and
the everyday check. Done: raw regions match existing structural semantics, and
no consumer-facing operation exposes them yet.
## Stage 3 — Combine sensitivity into a local lookup
Status: complete. Scope: classification lookup, independently of safe rendering.
1. Add `internal/engine/diff_sensitivity.go` and focused tests. Union true
classifications from both inspections without mutating either map.
2. Implement ancestor/equal masking and strict-descendant lookup with separator-
aware canonical tokens. Preserve root versus the empty-key pointer, escaping,
exact terminal boundaries, and `/a` versus `/ab`.
3. Exercise either-side classification, false map entries, duplicates, root,
ancestors/descendants, and historical-only paths. Verify both input maps are
unchanged. Test queries by their results, not the chosen trie layout.
Validate: `go test ./internal/engine -run 'Diff|PathPrefix'` and the everyday
check. Done: later disclosure can query combined sensitivity without repeated
whole-map scans or new persistent inspection state.
## Stage 4 — Build paired safe snapshots for compatible shapes
Status: complete. Scope: compatible mapping/sequence/scalar traversal and absence.
1. Add private paired traversal in `internal/engine/diff_snapshot.go`, passing
the reported region path and Stage 3's lookup. Build safe owned containers
for each present side. Reuse the existing marker and safe snapshot conventions.
2. Mask ancestor/equal-selected nodes before exposing bytes. Visit the union of
mapping keys and sequence positions, preserving each side's own content and
length. Do not fill missing keys or indices with explicit null.
3. Preserve public siblings and deterministic YAML ordering. Borrow immutable
scalar nodes only where safe; never modify retained node structs or slices.
4. Test aligned sensitive/public mappings, sequences with unchanged masked
members, absent subtrees, added/removed sensitive descendants, classified root
and containers, one-sided classification, and literal marker-looking strings.
Keep this helper private and unexposed; Stage 5 completes incompatible shapes
before Stage 7 exposes the engine operation. Unsupported intermediate branches
must not return unprotected values.
Validate: `go test ./internal/engine -run 'Diff|Effective|Snapshot'` and the
everyday check. Done: compatible paired masking preserves presence and public
content, while existing standalone rendering remains unchanged.
Evidence: the focused engine tests and `./scripts/check.sh` passed. The check
completed formatting, whitespace, `go test ./...`, `go vet ./...`, and
`go build ./...` on `go1.26.8 linux/amd64`. Tests confirm one-sided
classification protects both sides, absent subtrees retain absence and public
siblings, compatible sequences preserve each side's length, retained trees are
unchanged, and unsupported intermediate shapes return no nodes.
## Stage 5 — Complete structural-replacement masking
Status: planned. Scope: the fallback rule and its shape-sensitive regressions.
1. Extend the paired traversal to detect incompatible present kinds at the root
or nested positions. Use strict-descendant classification under the mismatch
path to request complete masking of the original reported region.
2. On fallback discard partial safe trees for that region, mask each present
reported root, and preserve any absent side. Without applicable sensitivity,
ordinary public kind replacements retain their values. Do not infer shape
from pointer spelling or ignore a historical selector that is now absent.
3. Test both directions of mapping/scalar, mapping/sequence with numeric keys,
and null/container replacements; nested mismatches inside atomic sequences;
and historical-only descendant classification. Contrast public mismatches
with unrelated unchanged sensitive siblings, and ordinary missing keys/indices
that permit targeted redaction rather than complete masking.
Validate: focused `Diff` tests, existing rendering tests, and the everyday
check. Done: every shape branch has defined disclosure behavior without blanket
masking of otherwise compatible public regions.
## Stage 6 — Distinguish hidden changes from unchanged masked content
Status: planned. Scope: one flag threaded through the existing paired traversal.
1. Have paired preparation return `HiddenChanged` alongside the safe nodes and
each side's redaction state. At directly masked frontiers compare raw nodes
with `configurationValuesEqual`; include presence. OR flags through unmasked
containers. Full-region fallback on a delta sets true.
2. Do not use safe YAML equality to infer raw equality. Do not set the flag simply
because either snapshot is redacted. Do not align sequences or emit new
descendant delta records; hidden detection uses the same positional disclosure
traversal as Stage 4.
3. Test wholly masked changes, identical rendered snapshots with differing raw
values, mixed public/hidden changes, public-only changes with unchanged masked
content (false), masked additions/removals, changed sequence lengths, and
fallback (true). Include raw equality under different classifications to show
that classification alone does not create a delta.
Validate: `go test ./internal/engine -run 'Diff|Secret|Sensitivity'` and the
everyday check. Done: the flag communicates withheld change without claiming
that every masked child changed or revealing additional change details.
## Stage 7 — Assemble the complete engine diff operation
Status: planned. Scope: internal-facing records, serialization, and real loads.
1. Define engine `Difference` with `Path`, `Left`, `Right`, and `HiddenChanged`,
and the operation over two `*Inspection` values. Wire Stages 2–6 together:
prepare raw regions; build the local classification lookup once; make safe
pairs; serialize safe nodes; append records in comparator order.
2. Match existing `ValueSnapshot` YAML/presence/redaction behavior on ordinary
successful serialization. Return safe errors with no records for unavailable
inputs or unexpected serialization failure. Do not reuse explanation's
silent rendering-failure fallback for the diff flag. Do not expose internal
node pointers in the result.
3. Add full-load tests using typed/dynamic fields, defaults, recorded resolution,
dependency-derived sensitivity, and historical removal/replacement. Confirm
actual `Inspection.final` values and `Inspection.secrets` drive comparison.
4. Load typed secrets with disposable environment values: rotation behind the
same reference produces no diff; changing reference names does. Exercise
optional secret absence and classified reference names. Change the environment
after loading to prove diff does not reread it.
5. Exercise unchanged standalone effective rendering, explanations, metadata,
and independent loads after comparison. Repeated diff calls remain detached.
Validate: `go test ./internal/engine -run 'Diff|Explain|History|Secret|Independent'`,
`go test -race ./internal/engine`, and the everyday check. Done: complete safe
comparison works through the engine with no loading or existing inspection
regression. Do not expose a partial public API earlier.
## Stage 8 — Add the root-owned public facade
Status: planned. Scope: public declarations, conversion, and external tests.
1. Add root `diff.go` with the exact roadmap record and generic entry point.
Validate nil/zero `Result` state without inspecting caller-owned `Config`;
delegate through the private inspection pointer. Convert engine records with
existing path and `fromEngineValueSnapshot` conventions. Ensure engine paths
are owned before wrapping them; root signatures expose only root types.
2. Add Go documentation for direction, presence, granularity, ordering, frozen
state, combined sensitivity, `HiddenChanged`, equal-result behavior, errors,
and credential limitation. No extra options, method-only alternative,
exported bridge, generic value tree, or second supported import package.
3. Add `facade_diff_test.go` in the external test package for representative
results, nil/zero inputs, defaults-only loads, equal values from different
sources/defaults/resolvers, explicit zero versus omitted/defaulted zero,
and null/absence. Mutate `Result.Config` and returned records, then confirm
repeated diffs and existing inspection still use frozen values.
4. Confirm callbacks run during loads only. A temporary source file may be
removed after both loads and comparison still works. Test invalid left and
right loads retain the normal nil-result/LoadError behavior; callers must
handle those errors before diffing. Do not add a new load-failure aggregator.
Validate: `go test . -run 'Diff|FacadeInspection|PublicFacadeBoundary|DependencyImportBoundary'`
and the everyday check. Done: the selected API is available through the stable
root facade without weakened ownership or changed load contracts.
## Stage 9 — Demonstrate both consumer workflows
Status: planned. Scope: maintained examples and their behavior tests.
1. Add a small complete example under `examples/consumers` that loads two caller-
selected files with one definition, handles each load error, calls `Diff`, and
presents safe changes using consumer-owned formatting. Tests use temporary
files; documentation contains no real application configuration or credentials.
2. Add a base-versus-base-plus-overlay example using normal loading options and
the selected overlay. Include recorded resolution and a classified nested
value, showing the difference after resolution rather than diffing overlay
source text. Recreate one-shot sources where necessary under existing reuse
rules. Demonstrate retained lower-priority siblings.
3. Use `Present` for absence, record membership for change, and `HiddenChanged`
for withheld change indicators. Do not parse marker text. Do not supplement
records with separately rendered full contexts that lack combined sensitivity.
4. Add an executable root `ExampleDiff` using minimal mapping inputs if this
provides useful API discovery without duplicating the complete workflows.
Verify example output is deterministic and secret-free.
Validate: `go test ./examples/consumers`, `go test . -run '^ExampleDiff$'` when
added, and the everyday check. Done: applications can implement the motivating
workflows without internal imports or their own structural comparison.
## Stage 10 — Reconcile documentation and audit the complete feature
Status: planned. Scope: current-behavior documentation, integration audit, and
completion evidence; no release publication.
1. Add an effective-diff section to `docs/config.md` for shared equality,
granularity, cross-redaction, and credential semantics. Symbol details remain
beside declarations. Add task guidance/example links in
`docs/consumers/README.md` and concise navigation in the root README.
2. Update `docs/internal/overview.md` for implemented ownership; add the new ADR
and focused diff test commands to `docs/development.md`. Update architecture
policy only to recognize the approved inspection boundary and link to owners;
preserve existing loading, composition, confidentiality, and ownership rules.
3. In `docs/roadmap/deferred.md`, remove configuration diffs from the excluded
list, keep fingerprints and unrelated exclusions, and link implemented diff
behavior to its owner. No unrelated policy provenance, licensing, dependencies,
prepared release notes, or release machinery changes are required.
4. Review the complete diff against the planning baseline and every roadmap
contract. Check all structural/redaction branches, error disclosure, the
root/private boundary, detached records, and unaffected standalone inspection.
Remove abandoned temporary branches/helpers from earlier stages; do not keep
alternate production comparators or unsafe public entry points.
5. Run `./scripts/check.sh` and `GOTOOLCHAIN=local GOPROXY=off go test -race ./...`.
Confirm tests cover both workflows, aligned and incompatible secret shapes,
hidden-change false/true cases, reference rotation, invalid results, ownership,
and the existing public/import boundaries. Document actual results, revisions,
and limitations here without claiming unmeasured speedups.
6. Mark stages and roadmap complete only once the selected outcomes and owning
documentation agree. Keep planned/deferred work accurate. Retire completed
planning records later during separately authorized release preparation.
Done: the agreed feature is implemented, validated, and documented without
stronger unrelated guarantees or changes to the publication contract.
## Open Questions
None required for this scope. The roadmap's four choices are approved; the
shape fallback, `HiddenChanged` calculation, error handling, and sequencing are
resolved above. Any new requirement found during implementation should be
reported with its impact rather than inferred as an additional contract.