Preserve explicitness across resolution shape changes
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Interface Contract Remediation Implementation Plan
|
# Interface Contract Remediation Implementation Plan
|
||||||
|
|
||||||
Status: in progress. Stage 1 is complete; Stages 2–18 remain pending.
|
Status: in progress. Stages 1–2 are complete; Stages 3–18 remain pending.
|
||||||
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||||
Audience: a Luna coding agent executing the stages sequentially.
|
Audience: a Luna coding agent executing the stages sequentially.
|
||||||
|
|
||||||
@@ -315,7 +315,9 @@ and the selected root regression.
|
|||||||
## Stage 2 — Correct Final Explicitness Across Shape Changes
|
## Stage 2 — Correct Final Explicitness Across Shape Changes
|
||||||
|
|
||||||
Finding 2. Owners: `applyResolutionMetadata`, `refreshResolutionFields`,
|
Finding 2. Owners: `applyResolutionMetadata`, `refreshResolutionFields`,
|
||||||
`resolveLoadInput`, and resolution/metadata tests. Status: pending.
|
`resolveLoadInput`, and resolution/metadata tests. Status: complete. Validation:
|
||||||
|
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Resolution|Metadata|Provenance' -count=1`
|
||||||
|
and `GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`.
|
||||||
|
|
||||||
1. Pass the frozen `resolutionInput` to effective metadata refresh rather than
|
1. Pass the frozen `resolutionInput` to effective metadata refresh rather than
|
||||||
attempting to infer entry shape from a final pointer bucket.
|
attempting to infer entry shape from a final pointer bucket.
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ func cloneMetadata(metadata Metadata) Metadata {
|
|||||||
return cloned
|
return cloned
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult) {
|
func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult, entry resolutionInput) {
|
||||||
finalPaths := make(map[string]*document.Node)
|
finalPaths := make(map[string]*document.Node)
|
||||||
indexDocument(resolved.final, Path{}, finalPaths)
|
indexDocument(resolved.final, Path{}, finalPaths)
|
||||||
snapshotCoverage, snapshotPointers := newResolutionCoverageIndexFromSnapshots(resolved.snapshots)
|
snapshotCoverage, snapshotPointers := newResolutionCoverageIndexFromSnapshots(resolved.snapshots)
|
||||||
@@ -303,7 +303,7 @@ func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult) {
|
|||||||
|
|
||||||
appendResolutionDescendantHistory(metadata, resolved.snapshots, snapshotPointers, finalPaths, &deltaCoverage, snapshotIDs)
|
appendResolutionDescendantHistory(metadata, resolved.snapshots, snapshotPointers, finalPaths, &deltaCoverage, snapshotIDs)
|
||||||
applyResolutionFieldOrigins(metadata.fields, resolved.snapshots, &deltaCoverage, snapshotIDs)
|
applyResolutionFieldOrigins(metadata.fields, resolved.snapshots, &deltaCoverage, snapshotIDs)
|
||||||
refreshResolutionFields(metadata.fields, finalPaths, resolved.snapshots, snapshotCoverage)
|
refreshResolutionFields(metadata.fields, finalPaths, resolved.final, entry, resolved.snapshots, snapshotCoverage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendResolutionDescendantHistory(metadata *Metadata, snapshots []resolutionSnapshot, snapshotPointers []string, finalPaths map[string]*document.Node, deltaIndex *pathPrefixIndex, snapshotIDs []int) {
|
func appendResolutionDescendantHistory(metadata *Metadata, snapshots []resolutionSnapshot, snapshotPointers []string, finalPaths map[string]*document.Node, deltaIndex *pathPrefixIndex, snapshotIDs []int) {
|
||||||
@@ -368,22 +368,29 @@ func applyResolutionFieldOrigins(fields map[string]FieldInfo, snapshots []resolu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshResolutionFields(fields map[string]FieldInfo, finalPaths map[string]*document.Node, snapshots []resolutionSnapshot, snapshotCoverage *resolutionCoverageIndex) {
|
func refreshResolutionFields(fields map[string]FieldInfo, finalPaths map[string]*document.Node, final *document.Node, entry resolutionInput, snapshots []resolutionSnapshot, snapshotCoverage *resolutionCoverageIndex) {
|
||||||
for pointer := range fields {
|
for pointer := range fields {
|
||||||
if _, ok := finalPaths[pointer]; !ok {
|
if _, ok := finalPaths[pointer]; !ok {
|
||||||
delete(fields, pointer)
|
delete(fields, pointer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for pointer, node := range finalPaths {
|
walkDocument(final, Path{}, func(path Path, node *document.Node) {
|
||||||
|
pointer := path.String()
|
||||||
|
entryInfo, entryFound := entry.lookup(path)
|
||||||
if info, ok := fields[pointer]; ok {
|
if info, ok := fields[pointer]; ok {
|
||||||
|
info.Explicit = entryFound && entryInfo.Explicit
|
||||||
info.Null = node.Kind == document.Null
|
info.Null = node.Kind == document.Null
|
||||||
fields[pointer] = info
|
fields[pointer] = info
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
origin := Origin{Layer: LayerResolution}
|
origin := Origin{Layer: LayerResolution}
|
||||||
if snapshotID, ok := snapshotCoverage.deepestCoveringPointerID(pointer); ok {
|
if snapshotID, ok := snapshotCoverage.deepestCoveringPointerID(pointer); ok {
|
||||||
origin = cloneOrigin(snapshots[snapshotID].change.Origin)
|
origin = cloneOrigin(snapshots[snapshotID].change.Origin)
|
||||||
}
|
}
|
||||||
fields[pointer] = FieldInfo{Null: node.Kind == document.Null, Origin: origin}
|
fields[pointer] = FieldInfo{
|
||||||
}
|
Explicit: entryFound && entryInfo.Explicit,
|
||||||
|
Null: node.Kind == document.Null,
|
||||||
|
Origin: origin,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T],
|
|||||||
}
|
}
|
||||||
final = resolved.final
|
final = resolved.final
|
||||||
resolutionSnapshots = resolved.snapshots
|
resolutionSnapshots = resolved.snapshots
|
||||||
applyResolutionMetadata(&metadata, resolved)
|
applyResolutionMetadata(&metadata, resolved, prepared.resolution)
|
||||||
referenceInput = documentWithMetadataLocations(final, metadata, input.document)
|
referenceInput = documentWithMetadataLocations(final, metadata, input.document)
|
||||||
}
|
}
|
||||||
applications := []historyApplication(nil)
|
applications := []historyApplication(nil)
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func TestResolutionInputKeepsDetachedEntryMetadata(t *testing.T) {
|
|||||||
if len(resolved.diagnostics) != 0 {
|
if len(resolved.diagnostics) != 0 {
|
||||||
t.Fatalf("resolution diagnostics = %#v", resolved.diagnostics)
|
t.Fatalf("resolution diagnostics = %#v", resolved.diagnostics)
|
||||||
}
|
}
|
||||||
applyResolutionMetadata(&metadata, resolved)
|
applyResolutionMetadata(&metadata, resolved, input)
|
||||||
metadata.effectivePaths = effectivePathIndex(resolved.final)
|
metadata.effectivePaths = effectivePathIndex(resolved.final)
|
||||||
|
|
||||||
again, ok := input.lookup(parsedItem)
|
again, ok := input.lookup(parsedItem)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin
|
|||||||
if len(resolved.diagnostics) != 0 || configuration.Count != 5 || configuration.Derived != 42 {
|
if len(resolved.diagnostics) != 0 || configuration.Count != 5 || configuration.Derived != 42 {
|
||||||
t.Fatalf("resolution = %#v, configuration = %#v", resolved, configuration)
|
t.Fatalf("resolution = %#v, configuration = %#v", resolved, configuration)
|
||||||
}
|
}
|
||||||
applyResolutionMetadata(&prepared.metadata, resolved)
|
applyResolutionMetadata(&prepared.metadata, resolved, prepared.resolution)
|
||||||
effectiveReferenceInput := documentWithMetadataLocations(resolved.final, prepared.metadata)
|
effectiveReferenceInput := documentWithMetadataLocations(resolved.final, prepared.metadata)
|
||||||
if node, ok := documentAtPath(effectiveReferenceInput, Keys("api_key")); !ok || node.Text != "RESOLVED_SECRET" || node.Location.SourceID != cliSource {
|
if node, ok := documentAtPath(effectiveReferenceInput, Keys("api_key")); !ok || node.Text != "RESOLVED_SECRET" || node.Location.SourceID != cliSource {
|
||||||
t.Fatalf("effective reference/location = %#v", node)
|
t.Fatalf("effective reference/location = %#v", node)
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ func TestApplyResolutionMetadataPreservesOverlappingDeltaOrder(t *testing.T) {
|
|||||||
fields: map[string]FieldInfo{"/group/item/leaf": {Origin: oldOrigin}},
|
fields: map[string]FieldInfo{"/group/item/leaf": {Origin: oldOrigin}},
|
||||||
history: make(map[string][]Change),
|
history: make(map[string][]Change),
|
||||||
}
|
}
|
||||||
|
entry := resolutionInput{metadata: Metadata{
|
||||||
|
fields: map[string]FieldInfo{"/group/item/leaf": {Explicit: true, Origin: oldOrigin}},
|
||||||
|
effectivePaths: effectivePathIndex(final),
|
||||||
|
}}
|
||||||
resolved := resolutionResult{
|
resolved := resolutionResult{
|
||||||
final: final,
|
final: final,
|
||||||
deltas: []documentDelta{{path: Keys("group")}, {path: Keys("group", "item")}},
|
deltas: []documentDelta{{path: Keys("group")}, {path: Keys("group", "item")}},
|
||||||
@@ -226,7 +230,7 @@ func TestApplyResolutionMetadataPreservesOverlappingDeltaOrder(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
applyResolutionMetadata(metadata, resolved)
|
applyResolutionMetadata(metadata, resolved, entry)
|
||||||
|
|
||||||
field := metadata.fields["/group/item/leaf"]
|
field := metadata.fields["/group/item/leaf"]
|
||||||
if field.Origin.Operation != childOrigin.Operation {
|
if field.Origin.Operation != childOrigin.Operation {
|
||||||
|
|||||||
@@ -390,6 +390,105 @@ func TestResolutionPresenceUsesEntryShapeAcrossDynamicChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolutionMetadataExplicitnessUsesEntryShape(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Shape any `yaml:"shape"`
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
entryPath Path
|
||||||
|
finalPath Path
|
||||||
|
shapeChanges bool
|
||||||
|
mutate func(*config)
|
||||||
|
wantNull bool
|
||||||
|
wantOrigin ChangeKind
|
||||||
|
wantOriginLayer Layer
|
||||||
|
wantExplicit bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "mapping key becomes sequence index beneath changed ancestor",
|
||||||
|
input: "shape: {'0': {leaf: old}}\n",
|
||||||
|
entryPath: Keys("shape", "0", "leaf"),
|
||||||
|
finalPath: Keys("shape").Index(0).Key("leaf"),
|
||||||
|
shapeChanges: true,
|
||||||
|
mutate: func(configuration *config) { configuration.Shape = []any{map[string]any{"leaf": "new"}} },
|
||||||
|
wantOrigin: ChangeNormalization,
|
||||||
|
wantOriginLayer: LayerResolution,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sequence index becomes mapping key beneath changed ancestor",
|
||||||
|
input: "shape: [{leaf: old}]\n",
|
||||||
|
entryPath: Keys("shape").Index(0).Key("leaf"),
|
||||||
|
finalPath: Keys("shape", "0", "leaf"),
|
||||||
|
shapeChanges: true,
|
||||||
|
mutate: func(configuration *config) { configuration.Shape = map[string]any{"0": map[string]any{"leaf": "new"}} },
|
||||||
|
wantOrigin: ChangeNormalization,
|
||||||
|
wantOriginLayer: LayerResolution,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same-shape ancestor normalization retains presence and null",
|
||||||
|
input: "shape: {nested: {leaf: old}}\n",
|
||||||
|
entryPath: Keys("shape", "nested", "leaf"),
|
||||||
|
finalPath: Keys("shape", "nested", "leaf"),
|
||||||
|
mutate: func(configuration *config) {
|
||||||
|
configuration.Shape = map[string]any{"nested": map[string]any{"leaf": nil}}
|
||||||
|
},
|
||||||
|
wantNull: true,
|
||||||
|
wantOrigin: ChangeNormalization,
|
||||||
|
wantOriginLayer: LayerResolution,
|
||||||
|
wantExplicit: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var callbackErr error
|
||||||
|
result, err := testLoad(Bytes("shape-change.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{
|
||||||
|
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||||
|
checkEntry := func() error {
|
||||||
|
info, found := resolution.InputInfo(test.entryPath)
|
||||||
|
if !found || !info.Explicit || !resolution.IsExplicit(test.entryPath) {
|
||||||
|
return errors.New("resolution entry view lost its original shape")
|
||||||
|
}
|
||||||
|
if test.shapeChanges {
|
||||||
|
if _, found := resolution.InputInfo(test.finalPath); found || resolution.IsExplicit(test.finalPath) {
|
||||||
|
return errors.New("resolution entry view adopted the final shape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := checkEntry(); err != nil {
|
||||||
|
callbackErr = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resolution.Record(Keys("shape"), ChangeNormalization, "normalize shape")
|
||||||
|
test.mutate(configuration)
|
||||||
|
if err := checkEntry(); err != nil {
|
||||||
|
callbackErr = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
info, found := result.Metadata().Lookup(test.finalPath)
|
||||||
|
if !found || info.Explicit != test.wantExplicit || result.Metadata().IsExplicit(test.finalPath) != test.wantExplicit {
|
||||||
|
t.Fatalf("final metadata = %#v, found %v, IsExplicit %v; want explicit %v", info, found, result.Metadata().IsExplicit(test.finalPath), test.wantExplicit)
|
||||||
|
}
|
||||||
|
if info.Null != test.wantNull || info.Origin.Layer != test.wantOriginLayer || info.Origin.Operation != test.wantOrigin {
|
||||||
|
t.Fatalf("final metadata null/origin = %v/%#v", info.Null, info.Origin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user