Classify source version evidence at original paths
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user