From fed591183bd1d4a6eefa7319f2dcb68351c49f9c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 15 Oct 2026 15:55:16 +0000 Subject: [PATCH] Classify source version evidence at original paths --- docs/roadmap/implementation.md | 18 +++- internal/engine/load_result.go | 25 +++-- internal/engine/secret.go | 61 +++++++++++- internal/engine/secret_classification_test.go | 94 ++++++++++++++++++- 4 files changed, 185 insertions(+), 13 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4dc0e20..392735d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement shared source version checks -Status: Stages 1–8 complete; evidence sensitivity classification has not started. +Status: Stages 1–9 complete; historical and dependency sensitivity remains. Audience: a Luna coding agent implementing one numbered stage per prompt, in order. The [feature roadmap](source-version-checks.md) owns target behavior and @@ -485,6 +485,22 @@ No environmental limitation affected validation; checks ran on Linux/amd64. Validate: source-version/sensitivity/rendering tests and everyday check. Done: direct/ancestor classification is correct without making headers payload history. +Completion evidence: carried private source-version evidence from per-load state +into the resolved publication input, without adding consumed declarations to +applications or metadata. Final classification now evaluates each declaration's +original mounted semantic path and captured ancestors with the existing schema +tag and predicate rules, then uses the existing sensitivity result for rendering. +This covers dynamic map/slice paths, typed original sequence indices, and +null/absent evidence while leaving unclassified evidence and shifted neighboring +payload visible. Evidence remains private. These checks passed with Go +`go1.26.8 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Secret|Sensitivity|History|Inheritance|Resolution' -count=1` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 10 — Preserve historical and dependency sensitivity 1. Connect evidence candidates to existing historical, inheritance, and recorded diff --git a/internal/engine/load_result.go b/internal/engine/load_result.go index 135452b..55cebfd 100644 --- a/internal/engine/load_result.go +++ b/internal/engine/load_result.go @@ -9,14 +9,15 @@ import ( // resolvedLoad carries typed phase outputs needed for validation and publication; // it owns no source access, options, or long-lived schema state. type resolvedLoad[T any] struct { - configuration T - decoded decodedConfiguration - metadata Metadata - final *document.Node - snapshots []resolutionSnapshot - applications []historyApplication - inheritance *inheritanceScope - contexts map[string]ResourceContext + configuration T + decoded decodedConfiguration + metadata Metadata + final *document.Node + snapshots []resolutionSnapshot + applications []historyApplication + versionEvidence []*sourceVersionEvidence + inheritance *inheritanceScope + contexts map[string]ResourceContext } func decodeLoadInput[T any](schema *schemaDescriptor, definition Definition[T], input assembledInput) (decodedConfiguration, []Diagnostic) { @@ -73,7 +74,11 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T], } else if input.document != nil { applications = []historyApplication{{layer: LayerBase, value: input.document}} } - return resolvedLoad[T]{configuration: configuration, decoded: decoded, metadata: metadata, final: final, snapshots: resolutionSnapshots, applications: applications, inheritance: input.inheritance, contexts: input.contexts}, diagnostics + var versionEvidence []*sourceVersionEvidence + if input.versionState != nil { + versionEvidence = append(versionEvidence, input.versionState.evidence...) + } + return resolvedLoad[T]{configuration: configuration, decoded: decoded, metadata: metadata, final: final, snapshots: resolutionSnapshots, applications: applications, versionEvidence: versionEvidence, inheritance: input.inheritance, contexts: input.contexts}, diagnostics } func detachLoadConfiguration[T any](schema *schemaDescriptor, configuration T, mode secretCloneMode) (T, *Diagnostic) { @@ -97,7 +102,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T] decoded, metadata := resolved.decoded, resolved.metadata final, applications, resolutionSnapshots := resolved.final, resolved.applications, resolved.snapshots finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, decoded.final) - secrets := classifySecretPaths(schema, definition.Secret, metadata, final, applications, decoded.baseline, decoded.final) + secrets := classifySecretPathsWithEvidence(schema, definition.Secret, metadata, final, applications, resolved.versionEvidence, decoded.baseline, decoded.final) retainedTrees := make([]*document.Node, 0, len(applications)+2) retainedTrees = append(retainedTrees, decoded.final, final) for _, application := range applications { diff --git a/internal/engine/secret.go b/internal/engine/secret.go index ef45e74..916c904 100644 --- a/internal/engine/secret.go +++ b/internal/engine/secret.go @@ -8,6 +8,10 @@ type secretDependency struct { } func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, projections ...*document.Node) map[string]bool { + return classifySecretPathsWithEvidence(schema, predicate, metadata, final, applications, nil, projections...) +} + +func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, evidence []*sourceVersionEvidence, projections ...*document.Node) map[string]bool { paths := make(map[string]Path) for _, application := range applications { walkDocument(application.value, application.path, func(path Path, _ *document.Node) { @@ -71,6 +75,32 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me classifyProjection(tree) } classifyProjection(final) + evidenceSecrets := make(map[string]bool) + seenEvidence := make(map[string][]Path) + classifyEvidence := func(destination, local Path) { + path := appendDestination(destination, local) + pointer := path.String() + for _, seen := range seenEvidence[pointer] { + if sameSecretPath(seen, path) { + return + } + } + seenEvidence[pointer] = append(seenEvidence[pointer], path.clone()) + classifyEvidencePath(evidenceSecrets, schema, predicate, path) + } + for _, item := range evidence { + if item == nil { + continue + } + candidate := item.resolvedPath + if !candidate.valid() { + candidate = item.path + } + classifyEvidence(item.destination, candidate) + for _, ancestor := range item.ancestors { + classifyEvidence(item.destination, ancestor.path) + } + } for pointer, path := range paths { if root, ok := schemaSecretRoot(schema, path); ok { secret[root.String()] = true @@ -79,7 +109,36 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me secret[pointer] = true } } - return closeSecretDependencies(paths, secret, dependencies, inherited) + classified := closeSecretDependencies(paths, secret, dependencies, inherited) + for pointer, sensitive := range evidenceSecrets { + if sensitive { + classified[pointer] = true + } + } + return classified +} + +func classifyEvidencePath(secret map[string]bool, schema *schemaDescriptor, predicate func(Path) bool, path Path) { + pointer := path.String() + if root, ok := schemaSecretRoot(schema, path); ok { + secret[root.String()] = true + } + if predicate != nil && predicate(path.clone()) { + secret[pointer] = true + } +} + +func sameSecretPath(left, right Path) bool { + if len(left.segments) != len(right.segments) { + return false + } + for index, segment := range left.segments { + other := right.segments[index] + if segment.kind != other.kind || segment.key != other.key || segment.index != other.index { + return false + } + } + return true } func addSecretPath(paths map[string]Path, pointer string) { diff --git a/internal/engine/secret_classification_test.go b/internal/engine/secret_classification_test.go index 54de316..ef0aaf9 100644 --- a/internal/engine/secret_classification_test.go +++ b/internal/engine/secret_classification_test.go @@ -1,6 +1,9 @@ package engine -import "testing" +import ( + "reflect" + "testing" +) func TestSecretPathCollectionKeepsParsedContributions(t *testing.T) { for _, pointer := range []string{"", "/", "/records/0", "/records/a~1b~0/01"} { @@ -77,3 +80,92 @@ func TestSecretPredicateReceivesDetachedHistoricalPath(t *testing.T) { t.Fatalf("classification after predicate mutation = %#v, predicate called: %v", classified, seen) } } + +func TestSourceVersionEvidenceSensitivityUsesOriginalMountedPaths(t *testing.T) { + const canary = "PRIVATE-VERSION-CANARY" + type credential struct { + Value string `yaml:"credential" configkit:"secret"` + Public string `yaml:"public"` + } + type account struct { + Token string `yaml:"token"` + } + type tenant struct { + Groups map[string][]credential `yaml:"groups"` + Account account `yaml:"account" configkit:"secret"` + Settings map[string]string `yaml:"settings"` + Labels map[string]string `yaml:"labels"` + } + type configuration struct { + Tenant tenant `yaml:"tenant"` + } + schema, issue := buildSchema(reflect.TypeFor[configuration](), testSecretBinding()) + if issue != nil { + t.Fatal(issue) + } + + destination := Keys("tenant") + sequencePath := Keys("groups").Key("blue").Index(2).Key("credential") + accountPath := Keys("account", "token") + predicatePath := Keys("settings", "private") + nullPath := Keys("settings", "nullable") + absentPath := Keys("settings", "missing") + publicPath := Keys("labels", "public") + evidence := []*sourceVersionEvidence{ + { + path: sequencePath.clone(), resolvedPath: sequencePath.clone(), destination: destination.clone(), + version: Version{Present: true, Text: canary}, + ancestors: []sourceVersionAncestor{{}, {path: Keys("groups")}, {path: Keys("groups").Key("blue")}, {path: Keys("groups").Key("blue").Index(2)}, {path: sequencePath.clone()}}, + }, + { + path: accountPath.clone(), resolvedPath: accountPath.clone(), destination: destination.clone(), + version: Version{Present: true, Text: canary}, + ancestors: []sourceVersionAncestor{{}, {path: Keys("account")}, {path: accountPath.clone()}}, + }, + {path: predicatePath.clone(), resolvedPath: predicatePath.clone(), destination: destination.clone(), version: Version{Present: true, Text: canary}}, + {path: nullPath.clone(), resolvedPath: nullPath.clone(), destination: destination.clone(), version: Version{Present: true, Null: true}}, + {path: absentPath.clone(), resolvedPath: absentPath.clone(), destination: destination.clone()}, + {path: publicPath.clone(), resolvedPath: publicPath.clone(), destination: destination.clone(), version: Version{Present: true, Text: canary}}, + } + + final := parseCompositionDocument(t, "final", "tenant:\n groups:\n blue:\n - public: shifted-neighbor\n labels:\n public: visible\n") + var sawOriginalSequencePath, sawAncestorPredicate bool + classified := classifySecretPathsWithEvidence(schema, func(path Path) bool { + if path.String() == appendDestination(destination, predicatePath).String() { + return true + } + if path.String() == appendDestination(destination, nullPath).String() { + return true + } + if path.String() == appendDestination(destination, absentPath).String() { + return true + } + if path.String() == appendDestination(destination, Keys("account")).String() { + sawAncestorPredicate = true + return true + } + if path.String() == appendDestination(destination, sequencePath).String() { + sawOriginalSequencePath = len(path.segments) == 5 && path.segments[3].kind == pathSegmentIndex && path.segments[3].index == 2 + } + return false + }, Metadata{}, final, []historyApplication{{path: destination, value: final}}, evidence) + + for _, path := range []Path{ + appendDestination(destination, sequencePath), + appendDestination(destination, Keys("account")), + appendDestination(destination, predicatePath), + appendDestination(destination, nullPath), + appendDestination(destination, absentPath), + } { + if !classified[path.String()] { + t.Errorf("evidence path %s was not classified: %#v", path, classified) + } + } + shiftedNeighbor := appendDestination(destination, Keys("groups").Key("blue").Index(0).Key("public")) + if classified[shiftedNeighbor.String()] { + t.Fatalf("shifted neighboring payload was classified from declaration evidence: %#v", classified) + } + if classified[appendDestination(destination, publicPath).String()] || !sawOriginalSequencePath || !sawAncestorPredicate { + t.Fatalf("unclassified or semantic evidence candidates = %#v; original path seen %v, ancestor predicate seen %v", classified, sawOriginalSequencePath, sawAncestorPredicate) + } +}