diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 5365714..eaa0b06 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -468,6 +468,12 @@ separate design/evidence and do not block this sequence. ### Final evidence and outcomes +- Post-review Stage 9 completion: sensitivity collection now reuses existing + valid parsed paths without reparsing; later projections still replace typed + history paths. Repeated parsed contributions and both numeric mapping/sequence + transitions are covered. Focused sensitivity/inheritance/rendering tests, + `./scripts/check.sh`, and `go test -race ./internal/engine` passed. + - Reviewed planning baseline `0c77deed05417edb32f42c7bf24df2fdc2581a25`, production baseline `48619de024d794cf41e90b171b7fcbe609dcd437`, and the complete implementation diff through `799ebab5a6f15e8252474b8cb7ea164ef503f390`. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 3d8aa21..744e30c 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -17,7 +17,11 @@ The older, completed ReleaseKit review record remains in Git history at ## 1. Complete parsed-path reuse in sensitivity collection (P2) -Status: Open. +Status: Complete. Parsed accumulator paths are reused, while typed historical +paths still receive the later parsed replacement. Repeated root/escaped/numeric +contributions and both numeric mapping/sequence transitions are covered. +Focused sensitivity/inheritance/rendering tests, `./scripts/check.sh`, and +`go test -race ./internal/engine` passed. Affected: [secret.go](../../internal/engine/secret.go), `addSecretPath` (lines 85–89); implementation Stage 9, item 3. diff --git a/internal/engine/secret.go b/internal/engine/secret.go index 483c0ea..ef45e74 100644 --- a/internal/engine/secret.go +++ b/internal/engine/secret.go @@ -83,6 +83,20 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me } func addSecretPath(paths map[string]Path, pointer string) { + if existing, found := paths[pointer]; found && existing.valid() { + // Accumulator keys already match their paths' canonical spelling. History + // can install typed segments, which still need the parsed replacement. + parsed := true + for _, segment := range existing.segments { + if segment.kind != pathSegmentParsed { + parsed = false + break + } + } + if parsed { + return + } + } if path, err := ParsePath(pointer); err == nil { paths[pointer] = path } diff --git a/internal/engine/secret_classification_test.go b/internal/engine/secret_classification_test.go index 6b2502f..54de316 100644 --- a/internal/engine/secret_classification_test.go +++ b/internal/engine/secret_classification_test.go @@ -2,6 +2,62 @@ package engine import "testing" +func TestSecretPathCollectionKeepsParsedContributions(t *testing.T) { + for _, pointer := range []string{"", "/", "/records/0", "/records/a~1b~0/01"} { + t.Run(pointer, func(t *testing.T) { + paths := make(map[string]Path) + for repeat := 0; repeat < 3; repeat++ { + addSecretPath(paths, pointer) + path, found := paths[pointer] + if !found || !path.valid() || path.String() != pointer { + t.Fatalf("collected path = %#v, found %v; want %q", path, found, pointer) + } + for _, segment := range path.segments { + if segment.kind != pathSegmentParsed { + t.Fatalf("collected path %q retained a typed segment: %#v", pointer, segment) + } + } + } + }) + } +} + +func TestSecretClassificationParsesTypedHistoryAfterNumericShapeChanges(t *testing.T) { + tests := []struct { + name string + earlier string + final string + historical Path + }{ + {"mapping to sequence", "items: {'0': old}", "items: [new]", Keys("items", "0")}, + {"sequence to mapping", "items: [old]", "items: {'0': new}", Keys("items").Index(0)}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + earlier := parseCompositionDocument(t, "earlier", test.earlier) + final := parseCompositionDocument(t, "final", test.final) + pointer := test.historical.String() + metadata := Metadata{history: map[string][]Change{pointer: {{Path: test.historical}}}} + seen := false + classified := classifySecretPaths(nil, func(path Path) bool { + if path.String() != pointer { + return false + } + seen = true + for _, segment := range path.segments { + if segment.kind != pathSegmentParsed { + t.Fatalf("predicate received typed history after final projection: %#v", path) + } + } + return true + }, metadata, final, []historyApplication{{value: earlier}}, earlier) + if !seen || !classified[pointer] { + t.Fatalf("historical/final numeric path was not classified: %#v", classified) + } + }) + } +} + func TestSecretPredicateReceivesDetachedHistoricalPath(t *testing.T) { path := Keys("records").Key("1") metadata := Metadata{history: map[string][]Change{