Add safe historical configuration explanations

This commit is contained in:
2026-09-06 22:14:54 +00:00
parent d4ec8607d1
commit 6ef240b1a0
17 changed files with 1032 additions and 141 deletions

View File

@@ -141,10 +141,60 @@ translated to a fixed safe error. There is no unredacted rendering option and
ConfigKit performs no implicit logging.
`Result.Config` is the explicit trusted boundary and contains ordinary typed values,
including secrets. Metadata, histories, diagnostics, and load errors contain no raw
configuration values. Applications must still keep source labels, resolution
reasons, and diagnostic messages value-free because ConfigKit cannot infer and
sanitize secrets embedded in arbitrary application prose.
including secrets. Metadata records, diagnostics, and load errors contain no
configuration values. Explanation snapshots deliberately expose non-secret
configuration history after applying the redaction contract below. Applications
must still keep source labels, resolution reasons, and diagnostic messages value-free
because ConfigKit cannot infer and sanitize secrets embedded in arbitrary application
prose.
### Historical-value explanation
`Result.Explain(path)` returns a detached `Explanation` for any known effective or
historical path. Its effective value and each chronological before/after value are
`ValueSnapshot` records containing deterministic YAML text rather than mutable Go or
parser values. `Present=false` with empty YAML means absence or deletion; explicit
null is present and renders as `null`. The history starts with the canonical typed
application-default or Go-zero baseline, then includes only relevant base sources in
traversal order, the selected profile, supplied CLI records, and audited net
resolution operations. Explicit equal writes remain visible.
Base, profile, and CLI history preserves the policy-validated input representation,
including a superseded value that could not have decoded into the final Go field.
Baseline, effective, and resolution values use canonical typed projection. Mapping
queries reconstruct each source or override contribution, while sequences remain
atomic: a descendant history identifies its presence before and after the containing
sequence replacement without implying element-wise merging. Removed paths remain
explainable; unknown and malformed paths return false.
Every snapshot applies the same historical tag, predicate, and recorded-dependency
sensitivity closure as effective rendering before YAML is exposed. A redacted scalar
is the quoted marker `'[REDACTED]'`, and a container snapshot has `Redacted=true` if
it contains a masked descendant. Typed `Secret` snapshots contain only the configured
environment name. `SecretReference` reports that name only for a successfully
resolved effective reference, and is omitted when its path is classified. Credential
values are never retained in explanation history.
`Result.WriteExplanation(output, path)` renders the safe structured result with the
canonical JSON-pointer path, canonical-versus-input representation labels, effective
source, composite locations, chronological values, resolution reasons and
dependencies, and successful secret-reference lookup attribution. Control characters
in paths, source identifiers and labels, reasons, dependencies, and reference names
are escaped. An unknown path returns a fixed safe error before writing anything;
writer errors and short writes are returned and may leave partial output. There is no
unredacted mode.
Retained history consists of immutable source applications and audited subtree
deltas, rather than a full tree copy per field. Shared nodes count once toward a
4,000,000-node retained-history limit; exceeding it fails loading with
`explain.limit_exceeded`. Replaying one requested path is performed on demand, and
the returned YAML strings and nested records cannot mutate later explanations.
Application commands should render the separate `SecretsReport` immediately after
an explicit `LoadSecretsDir` bootstrap, then use `WriteDiagnostics`,
`WriteEffective`, or `WriteExplanation` on the later load result as appropriate.
ConfigKit does not infer bootstrap outcomes from a secret reference and does not
bundle these independent reports into a command framework.
## Typed secret references

View File

@@ -7,7 +7,7 @@ composition, selected profile overlays, typed defaults, schema-version checks,
explicit CLI overrides, recorded application resolution, semantic validation,
layered metadata, secret
classification, typed environment references, explicit secrets bootstrap, and
redacted effective rendering. The
redacted effective rendering and historical value explanation. The
[component overview](internal/overview.md) owns the implementation inventory.
## Initial orientation

View File

@@ -39,14 +39,16 @@ module and package documentation boundary. Its implemented public primitives are
- explicit bounded startup environment bootstrap and safe partial reporting in
`secrets.go`;
- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`; and
- frozen-snapshot redaction and canonical effective output in `render.go`, with the
- frozen-snapshot redaction and canonical effective output in `render.go`;
- bounded immutable input applications, safe historical replay, detached explanation
records, and deterministic human rendering in `explain.go`, with the
only upstream dumper integration isolated in `internal/yaml/write.go`.
The [configuration contract](../config.md) owns implemented input, schema, decoding,
loading lifecycle, path imports, composition, profile/CLI overlay, version checking,
recorded resolution, typed secret references/bootstrap, semantic validation,
metadata, secret classification, redacted effective rendering, and ownership
behavior.
metadata, secret classification, redacted effective rendering, historical
explanation, and ownership behavior.
The source-extraction inventory records the application foundations assigned to
future implementation work:

View File

@@ -1365,7 +1365,43 @@ Acceptance:
## Stage 19 — Expose safe historical-value explanation
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Added detached `ValueSnapshot`, `ValueChange`, `SecretReferenceInfo`, and
`Explanation` records plus `Result.Explain` and `Result.WriteExplanation` in
[`explain.go`](../../explain.go). Effective/default/domain values use canonical
typed projection; base/profile/CLI events preserve policy-validated input, with
explicit absent, null, equal-write, replacement, and deletion states.
- Extended composition, profile, and CLI handling to retain immutable applications
in traversal/precedence order. Explanation replays those applications on demand
with mapping recursion and atomic sequence replacement, then applies the existing
audited resolution subtree deltas. This reconstructs container and descendant
histories without a full retained tree per field, including superseded input that
could not decode into the final destination type.
- Applied tag, predicate, and dependency-cycle sensitivity across final and
historical application paths before snapshot projection. Snapshots recursively
redact classified descendants, resolved `Secret` metadata retains only successful
effective environment names, and classified references omit that annotation.
Shared immutable nodes count once toward a fixed 4,000,000-node retained-history
budget; excess fails safely with `explain.limit_exceeded`.
- Added deterministic human rendering with canonical quoted pointer paths, effective
and composite source locations/labels, representation labels, chronological
before/after YAML, reasons, dependencies, and secret lookup attribution. Unknown
paths write nothing; writer errors and short writes propagate. Added executable
`ExampleResult_Explain` and moved normative behavior to the
[current explanation contract](../config.md#historical-value-explanation), keeping
bootstrap reporting explicitly separate.
- Added focused coverage in [`explain_test.go`](../../explain_test.go) for the
imported-default/profile sequence 4 → 8 → 1, CLI and normalization, equal writes,
removed paths, parent replacement, composite mappings, atomic sequences,
superseded invalid input, null/empty values, successful and classified references,
historical/dependency-cycle canaries, returned-record mutation, deterministic and
failing writers, structural-sharing budgets, and an end-to-end secrets-bootstrap/
load/explain flow. Verified `go test ./...`, `go test -race ./...`, `go vet ./...`,
`go build ./...`, `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on
linux/amd64. No deviations were required; consumer ergonomics remain later work.
Read: [explanation contract](initial.md#historical-value-explanation), redaction
rules, and history bounds above.

View File

@@ -916,7 +916,9 @@ the [current redaction contract](../config.md#secret-classification-and-effectiv
`Result.Config`, `Metadata`, `Sources`, `Diagnostics`, and `WriteEffective`, together
with `LoadError` and `WriteDiagnostics`, are implemented at the current loading boundary.
The [current loading contract](../config.md#single-source-loading) and Go reference
comments own their behavior. Historical-value explanation below remains target work.
comments own their behavior. Historical-value explanation is also implemented and
owned by the
[current explanation contract](../config.md#historical-value-explanation).
```go
type Result[T any] struct {
@@ -976,74 +978,14 @@ func InspectPipeline(path, profile string, output io.Writer) error {
Applications can use `Metadata().Lookup`, `Metadata().History`, and `Sources()`
for structured inspection without implementing value rendering or importing a
parser. The explanation API below supplies safe values and formatting for application-owned
`config explain` commands.
parser. The implemented explanation API supplies safe values and formatting for
application-owned `config explain` commands.
### Historical-value explanation
```go
func (r *Result[T]) Explain(path Path) (Explanation, bool)
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error
type Explanation struct {
Path Path
Effective ValueSnapshot
Origin Origin
History []ValueChange
SecretReference *SecretReferenceInfo
}
type ValueSnapshot struct {
Present bool
Redacted bool
YAML string
}
type ValueChange struct {
Before, After ValueSnapshot
Origin Origin
Reasons []string
Dependencies []Path
}
type SecretReferenceInfo struct { Name string } // successful resolved lookup
```
Snapshots contain detached deterministic YAML text, never `any`, parser objects,
or mutable internal trees. `Present=false` is absence/deletion and has empty YAML;
null is present with `null` YAML. Redacted snapshots use the quoted marker
`[REDACTED]`; `Redacted` also marks containers containing redacted descendants.
Apply the same historical sensitivity/dependency closure as effective rendering
before exposing any snapshots. A `Secret` projects only its environment name;
its resolved value is never captured, even privately as history. For classified
reference paths omit `SecretReference` as well as masking the projected name.
Retain private persistent node snapshots or operation deltas with structural
sharing; do not copy the entire tree for every changed leaf. Explain a known final
or historical path, including removed paths; unknown paths return false, and the
writer returns a safe unknown-path error without output. Malformed query paths
also fail safely. Render the canonical JSON-pointer path, effective value/source,
and chronological history: initial application-default (or Go-zero baseline),
base contributions in traversal order, profile, CLI, and recorded domain changes.
Include before/after values for changed or explicitly redefined paths, equal-value
writes, removals, reasons/dependencies, and composite origins. Inputs overridden
before decoding retain policy-validated node values even when they would not fit
the final Go type. Defaults/final/domain snapshots use canonical typed projection;
label those representations rather than claiming every historical input was typed.
Container queries reconstruct the value at each relevant operation; descendant
queries include ancestor replacement/removal and whole-sequence replacements,
without implying element-wise sequence merging. Omitted fields/default retention
must agree with the presence/default contract; do not fabricate a write for every
import that leaves a path alone. Resolution history records the audited net change
per path, not every intermediate Go assignment. Lookup of a secret reference
annotates its effective name as resolved from the process environment; bootstrap
file outcomes come from the separate report and are never guessed.
`WriteExplanation` is a deterministic human renderer over this safe structured
result. Escape control characters in paths, labels, names, and reasons, propagate
writer errors/short writes, and permit partial output only on writer failure.
No unredacted switch or bundled command is provided. For example an application
can call it for `/transcription/concurrency` to show default `4`, an explicitly
imported file's `8`, then a testing profile's `1`, with file line numbers and the
profile as effective source. This does not require automatic `conf.d` discovery.
Historical explanation is implemented. The
[current explanation contract](../config.md#historical-value-explanation) owns its
public records, replay ordering, redaction, rendering, and allocation bounds.
### Module-specific options