diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index df3c687..d9b3963 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -8,7 +8,7 @@ Comparison baseline: `0dd16aa8a935645ae2847d32edd86461dc4618f0` Scope: the completed [implementation plan](implementation.md) and [interface remediation roadmap](interface-remediation.md). This report records remaining defects and newly introduced scalability problems; it does not change -the selected architecture or public contracts. All eight findings below are open. +the selected architecture or public contracts. Completion is recorded per finding. ## Architecture And Review Evidence @@ -59,7 +59,12 @@ deferred contributor-trace and branch-local replay experiments remain separate. ## 1. Final Dynamic Field Tags Can Be Lost From Redaction -Priority: P1. Status: open. +Priority: P1. Status: complete. + +Completion: final inventory/sensitivity is registered unconditionally before +freezing. `TestFinalDynamicTagRedactsEqualValuedResolution` failed before the fix +and passes afterward, protecting effective and historical public rendering. +Validation: the full ordinary suite and `git diff --check` passed. Relevant code: `resolveLoadInput` in [load_result.go](../../internal/engine/load_result.go), lines 56–73; diff --git a/facade_dynamic_redaction_test.go b/facade_dynamic_redaction_test.go new file mode 100644 index 0000000..52f0923 --- /dev/null +++ b/facade_dynamic_redaction_test.go @@ -0,0 +1,53 @@ +package configkit_test + +import ( + "bytes" + "strings" + "testing" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +func TestFinalDynamicTagRedactsEqualValuedResolution(t *testing.T) { + t.Parallel() + type classified struct { + Token string `yaml:"token" configkit:"secret"` + } + type configuration struct { + Value any `yaml:"value"` + } + const canary = "DYNAMIC-TAG-CANARY" + result, err := configkit.Load(configkit.Bytes("input", []byte("value: {token: "+canary+"}\n")), configkit.Options[configuration]{ + Definition: configkit.Definition[configuration]{ + Resolve: func(c *configuration, _ *configkit.Resolution) error { + // No canonical value changes, so no normalization record is needed. + c.Value = classified{Token: canary} + return nil + }, + }, + }) + if err != nil { + t.Fatal(err) + } + var effective bytes.Buffer + if err := result.WriteEffective(&effective); err != nil { + t.Fatal(err) + } + if strings.Contains(effective.String(), canary) || !strings.Contains(effective.String(), "[REDACTED]") { + t.Fatal("effective rendering did not preserve the final dynamic tag") + } + explanation, found := result.Explain(configkit.Keys("value", "token")) + if !found || !explanation.Effective.Redacted || strings.Contains(explanation.Effective.YAML, canary) { + t.Fatal("effective explanation did not preserve the final dynamic tag") + } + if len(explanation.History) == 0 { + t.Fatal("source history is absent") + } + for _, change := range explanation.History { + for _, value := range []configkit.ValueSnapshot{change.Before, change.After} { + if value.Present && (!value.Redacted || strings.Contains(value.YAML, canary)) { + t.Fatal("historical explanation did not preserve the final dynamic tag") + } + } + } +} diff --git a/internal/engine/metadata.go b/internal/engine/metadata.go index c675c22..e58846c 100644 --- a/internal/engine/metadata.go +++ b/internal/engine/metadata.go @@ -150,6 +150,9 @@ func freezeMetadataHistory(m *Metadata, final *document.Node) { panic("configkit: metadata history prefix is not ready") } builder.model.final = final + // A resolver may change dynamic field tags without changing canonical values + // or producing a resolution snapshot. Always observe the final classification. + builder.observeDocument(final, historyPhaseResolution) m.historyModel = builder.freeze() m.historyBuilder = nil }