Classify source version evidence at original paths

This commit is contained in:
2026-10-15 15:55:16 +00:00
parent 1f95cf905a
commit fed591183b
4 changed files with 185 additions and 13 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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)
}
}