Complete facade inspection and secrets bootstrap

This commit is contained in:
2026-09-12 16:19:22 +00:00
parent da307c9f49
commit 550f92d7c6
7 changed files with 358 additions and 6 deletions

View File

@@ -1,11 +1,11 @@
{ {
"schema_version": 2, "schema_version": 2,
"commit": "b8935e77bd5ac1dc347339966fbaba5de9af5348", "commit": "da307c9f49550d789df4efe7310bb3d837e97df4",
"indexed_at": "2026-09-12T16:16:09Z", "indexed_at": "2026-09-12T16:19:20Z",
"project": "home-eric-Workspace-ConfigKit", "project": "home-eric-Workspace-ConfigKit",
"nodes": 2447, "nodes": 2469,
"edges": 13587, "edges": 13742,
"original_size": 11206656, "original_size": 11206656,
"compressed_size": 1701304, "compressed_size": 1715591,
"compression_level": 3 "compression_level": 3
} }

Binary file not shown.

View File

@@ -423,7 +423,7 @@ candidate inspection and secrets-bootstrap surface.
## Stage 7 — Complete candidate inspection and bootstrap surface ## Stage 7 — Complete candidate inspection and bootstrap surface
Status: not started. Status: complete (2026-09-12).
1. Add the public explanation/value records and Result Explain, WriteEffective, 1. Add the public explanation/value records and Result Explain, WriteEffective,
and WriteExplanation delegation. Inspection uses the frozen engine state, and WriteExplanation delegation. Inspection uses the frozen engine state,
@@ -444,6 +444,24 @@ integration evidence for inspection, credentials, bootstrap, and writer failures
Run `go test ./internal/facade ./internal/engine`, race tests for both packages, Run `go test ./internal/facade ./internal/engine`, race tests for both packages,
and the everyday check. Root remains unchanged until Stage 8. and the everyday check. Root remains unchanged until Stage 8.
### Completion
Completed the candidate's inspection and bootstrap API with root-owned
explanation, snapshot, secret-reference, and bootstrap records. Result delegates
to the private frozen engine inspection for explanation and rendering, so public
configuration mutation cannot affect historical or effective output. Bootstrap
delegates to the engine while returning facade-owned report and error types with
detached converted entries and diagnostics.
The candidate's exported declarations and methods now match the root surface
after normalizing its temporary import path. External integration tests cover
frozen inspection, secret-safe explanation/rendering, malformed and nil result
handling, bootstrap application/skips, detached report entries, and typed
bootstrap failures. Linux validation with Go 1.26.7 passed
`go test ./internal/facade ./internal/engine`, `go test -race ./internal/facade ./internal/engine`,
and `./scripts/check.sh`. There were no deviations or remaining risks. The next
handoff moves the verified candidate to the root and removes migration copies.
## Stage 8 — Cut over the root and remove migration duplication ## Stage 8 — Cut over the root and remove migration duplication
Status: not started. Status: not started.

View File

@@ -89,3 +89,45 @@ func fromEngineChange(value engine.Change) Change {
} }
return result return result
} }
func fromEngineValueSnapshot(value engine.ValueSnapshot) ValueSnapshot {
return ValueSnapshot{Present: value.Present, Redacted: value.Redacted, YAML: value.YAML}
}
func fromEngineValueChange(value engine.ValueChange) ValueChange {
result := ValueChange{
Before: fromEngineValueSnapshot(value.Before),
After: fromEngineValueSnapshot(value.After),
Origin: fromEngineOrigin(value.Origin),
Reasons: append([]string(nil), value.Reasons...),
}
if value.Dependencies != nil {
result.Dependencies = make([]Path, len(value.Dependencies))
for index := range value.Dependencies {
result.Dependencies[index] = fromEnginePath(value.Dependencies[index])
}
}
return result
}
func fromEngineExplanation(value engine.Explanation) Explanation {
result := Explanation{
Path: fromEnginePath(value.Path),
Effective: fromEngineValueSnapshot(value.Effective),
Origin: fromEngineOrigin(value.Origin),
}
if value.History != nil {
result.History = make([]ValueChange, len(value.History))
for index := range value.History {
result.History[index] = fromEngineValueChange(value.History[index])
}
}
if value.SecretReference != nil {
result.SecretReference = &SecretReferenceInfo{Name: value.SecretReference.Name}
}
return result
}
func fromEngineSecretFileResult(value engine.SecretFileResult) SecretFileResult {
return SecretFileResult{Name: value.Name, Path: value.Path, Status: SecretFileStatus(value.Status), ExistingEmpty: value.ExistingEmpty}
}

View File

@@ -1,10 +1,14 @@
package configkit_test package configkit_test
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"os"
"reflect" "reflect"
"strings"
"testing" "testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit/internal/facade" configkit "gitea.maximumdirect.net/eric/ConfigKit/internal/facade"
@@ -29,6 +33,10 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
reflect.TypeFor[configkit.FilesystemContext](), reflect.TypeFor[configkit.Override](), reflect.TypeFor[configkit.FilesystemContext](), reflect.TypeFor[configkit.Override](),
reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](), reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](),
reflect.TypeFor[configkit.Result[struct{}]](), reflect.TypeFor[configkit.Result[struct{}]](),
reflect.TypeFor[configkit.ValueSnapshot](), reflect.TypeFor[configkit.ValueChange](),
reflect.TypeFor[configkit.SecretReferenceInfo](), reflect.TypeFor[configkit.Explanation](),
reflect.TypeFor[configkit.SecretFileResult](), reflect.TypeFor[configkit.SecretsReport](),
reflect.TypeFor[configkit.SecretsError](),
} { } {
if candidate.PkgPath() != packagePath { if candidate.PkgPath() != packagePath {
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath()) t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
@@ -48,6 +56,118 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
} }
} }
func TestFacadeInspectionUsesFrozenState(t *testing.T) {
t.Setenv("FACADE_INSPECTION_SECRET", "credential")
type configuration struct {
Value string `yaml:"value"`
Secret configkit.Secret `yaml:"secret"`
}
result, err := configkit.Load[configuration](configkit.Bytes("inspection.yml", []byte("value: original\nsecret: FACADE_INSPECTION_SECRET\n")))
if err != nil {
t.Fatal(err)
}
if !result.Config.Secret.IsResolved() || result.Config.Secret.Value() != "credential" {
t.Fatalf("secret = %#v", result.Config.Secret)
}
explanation, ok := result.Explain(configkit.Keys("value"))
if !ok || explanation.Effective.YAML != "original\n" {
t.Fatalf("explanation = %#v, %v", explanation, ok)
}
explanation.Effective.YAML = "mutated\n"
result.Config.Value = "mutated"
var effective bytes.Buffer
if err := result.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "value: original\n") || strings.Contains(effective.String(), "credential") {
t.Fatalf("effective = %q, %v", effective.String(), err)
}
again, ok := result.Explain(configkit.Keys("value"))
if !ok || again.Effective.YAML != "original\n" {
t.Fatalf("inspection changed = %#v, %v", again, ok)
}
secret, ok := result.Explain(configkit.Keys("secret"))
if !ok || secret.SecretReference == nil || secret.SecretReference.Name != "FACADE_INSPECTION_SECRET" {
t.Fatalf("secret explanation = %#v, %v", secret, ok)
}
var explanationOutput bytes.Buffer
if err := result.WriteExplanation(&explanationOutput, configkit.Keys("value")); err != nil || !strings.Contains(explanationOutput.String(), `path: "/value"`) {
t.Fatalf("explanation output = %q, %v", explanationOutput.String(), err)
}
unknown := bytes.NewBufferString("unchanged")
if err := result.WriteExplanation(unknown, configkit.Keys("unknown")); err == nil || unknown.String() != "unchanged" {
t.Fatalf("unknown explanation = %q, %v", unknown.String(), err)
}
var nilResult *configkit.Result[configuration]
if _, ok := nilResult.Explain(configkit.Keys("value")); ok {
t.Fatal("nil result explained a path")
}
if err := nilResult.WriteEffective(io.Discard); err == nil {
t.Fatal("nil result rendered effective configuration")
}
}
func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
const (
loadedName = "FACADE_BOOTSTRAP_LOADED"
existingName = "FACADE_BOOTSTRAP_EXISTING"
)
restore := func(name string) {
value, present := os.LookupEnv(name)
t.Cleanup(func() {
if present {
_ = os.Setenv(name, value)
} else {
_ = os.Unsetenv(name)
}
})
}
restore(loadedName)
restore(existingName)
if err := os.Unsetenv(loadedName); err != nil {
t.Fatal(err)
}
if err := os.Setenv(existingName, ""); err != nil {
t.Fatal(err)
}
directory := t.TempDir()
if err := os.WriteFile(directory+"/"+loadedName, []byte("bootstrap\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(directory+"/"+existingName, []byte("ignored\n"), 0o600); err != nil {
t.Fatal(err)
}
report, err := configkit.LoadSecretsDir(directory)
if err != nil {
t.Fatal(err)
}
entries := report.Entries()
if len(entries) != 2 || os.Getenv(loadedName) != "bootstrap" {
t.Fatalf("entries/environment = %#v / %q", entries, os.Getenv(loadedName))
}
status := map[string]configkit.SecretFileResult{}
for _, entry := range entries {
status[entry.Name] = entry
}
if status[loadedName].Status != configkit.SecretFileLoaded || status[existingName].Status != configkit.SecretFileSkippedExisting || !status[existingName].ExistingEmpty {
t.Fatalf("statuses = %#v", status)
}
entries[0].Name = "mutated"
if report.Entries()[0].Name == "mutated" {
t.Fatal("report entries share mutable state")
}
var rendered bytes.Buffer
if _, err := report.WriteTo(&rendered); err != nil || strings.Contains(rendered.String(), "bootstrap") || strings.Contains(rendered.String(), "ignored") {
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
}
failedReport, err := configkit.LoadSecretsDir("")
var secretsError *configkit.SecretsError
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
}
}
func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) { func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
t.Setenv("FACADE_SECRET_NAME", "credential") t.Setenv("FACADE_SECRET_NAME", "credential")

View File

@@ -0,0 +1,70 @@
package configkit
import (
"io"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
)
// ValueSnapshot is a detached, deterministic YAML representation of one value.
// Present distinguishes absence or deletion from an explicit null value.
type ValueSnapshot struct {
Present bool
Redacted bool
YAML string
}
// ValueChange describes one explicit definition or audited domain operation.
type ValueChange struct {
Before ValueSnapshot
After ValueSnapshot
Origin Origin
Reasons []string
Dependencies []Path
}
// SecretReferenceInfo describes a successfully resolved effective reference.
// Name is the environment-variable name, never its value.
type SecretReferenceInfo struct {
Name string
}
// Explanation is a detached safe account of one effective or historical path.
type Explanation struct {
Path Path
Effective ValueSnapshot
Origin Origin
History []ValueChange
SecretReference *SecretReferenceInfo
}
// Explain returns safe effective and chronological values for a known final or
// historical path. Unknown and malformed paths return false.
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
if r == nil {
return Explanation{}, false
}
value, ok := r.inspection.Explain(toEnginePath(path))
return fromEngineExplanation(value), ok
}
// WriteEffective writes deterministic redacted YAML from the frozen resolved
// snapshot. The output is intended for inspection and is not promised reloadable.
// Writer errors, including short writes, are returned and may leave partial output.
func (r *Result[T]) WriteEffective(output io.Writer) error {
if r == nil {
var inspection *engine.Inspection
return inspection.WriteEffective(output)
}
return r.inspection.WriteEffective(output)
}
// WriteExplanation writes a deterministic human-readable rendering of Explain.
// Unknown paths produce no output. Writer failures may leave partial output.
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
if r == nil {
var inspection *engine.Inspection
return inspection.WriteExplanation(output, toEnginePath(path))
}
return r.inspection.WriteExplanation(output, toEnginePath(path))
}

102
internal/facade/secrets.go Normal file
View File

@@ -0,0 +1,102 @@
package configkit
import (
"errors"
"io"
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
)
// SecretFileStatus describes the outcome for one file considered by
// LoadSecretsDir.
type SecretFileStatus string
const (
SecretFileLoaded SecretFileStatus = "loaded"
SecretFileSkippedExisting SecretFileStatus = "skipped_existing"
SecretFileNotApplied SecretFileStatus = "not_applied"
SecretFileFailed SecretFileStatus = "failed"
)
// SecretFileResult is the safe, value-free bootstrap result for one directory
// entry. ExistingEmpty is meaningful when Status is SecretFileSkippedExisting.
type SecretFileResult struct {
Name string
Path string
Status SecretFileStatus
ExistingEmpty bool
}
// SecretsReport is a detached snapshot of an explicit secrets-directory
// bootstrap. It does not retain file contents or environment values.
type SecretsReport struct{ value *engine.SecretsReport }
// Entries returns a copy of the report's filename-ordered entries.
func (r *SecretsReport) Entries() []SecretFileResult {
if r == nil || r.value == nil {
return nil
}
values := r.value.Entries()
if values == nil {
return nil
}
result := make([]SecretFileResult, len(values))
for index := range values {
result[index] = fromEngineSecretFileResult(values[index])
}
return result
}
// WriteTo renders one safe, deterministic line per entry. Names and paths are
// quoted so control characters cannot affect terminal output.
func (r *SecretsReport) WriteTo(output io.Writer) (int64, error) {
if r == nil || r.value == nil {
return (*engine.SecretsReport)(nil).WriteTo(output)
}
return r.value.WriteTo(output)
}
// SecretsError reports safe structured diagnostics for a failed explicit
// secrets-directory bootstrap. It intentionally does not unwrap filesystem or
// environment errors.
type SecretsError struct{ value *engine.SecretsError }
// Error implements error without rendering filenames, paths, or file contents.
func (e *SecretsError) Error() string {
if e == nil || e.value == nil {
return "configkit: secrets bootstrap failed"
}
return e.value.Error()
}
// Diagnostics returns a deep copy of the bootstrap diagnostics.
func (e *SecretsError) Diagnostics() []Diagnostic {
if e == nil || e.value == nil {
return nil
}
values := e.value.Diagnostics()
if values == nil {
return nil
}
result := make([]Diagnostic, len(values))
for index := range values {
result[index] = fromEngineDiagnostic(values[index])
}
return result
}
// LoadSecretsDir explicitly loads immediate regular files from directory into
// previously unset process environment variables. Applications should call it
// once during startup, before concurrent work; Load never calls it.
func LoadSecretsDir(directory string) (*SecretsReport, error) {
report, err := engine.LoadSecretsDir(directory)
result := &SecretsReport{value: report}
if err == nil {
return result, nil
}
var secretsError *engine.SecretsError
if errors.As(err, &secretsError) {
return result, &SecretsError{value: secretsError}
}
return result, &SecretsError{}
}