Replace generic resolution recording with intent methods

This commit is contained in:
2026-10-27 15:12:38 +00:00
parent eafbd32489
commit 9833ca3aa3
30 changed files with 124 additions and 63 deletions

View File

@@ -20,9 +20,9 @@ func TestCallbackAssignmentsAreDetachedAcrossLoads(t *testing.T) {
definition := Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
Normalize(r, Keys("map"), &c.Map, shared, "populate")
c.Pointer = external
r.Record(Keys("pointer"), ChangeNormalization, "populate")
r.RecordNormalization(Keys("pointer"), "populate")
c.Secret = SecretEnv("CONFIGKIT_CALLBACK_SECRET")
r.Record(Keys("secret"), ChangeNormalization, "reference")
r.RecordNormalization(Keys("secret"), "reference")
return nil
}}
if validatorAssignment {

View File

@@ -70,7 +70,7 @@ func TestRelativeImportsFeedFinalInheritanceAndInspectionLifecycle(t *testing.T)
child := config.Catalog.Entries["child"]
child.Derived = child.Label + ":" + child.Dynamic + ":" + fmt.Sprint(child.Height)
config.Catalog.Entries["child"] = child
resolution.Record(Keys("catalog", "entries", "child", "derived"), ChangeNormalization, "derive final child summary", Keys("catalog", "entries", "child", "label"), Keys("catalog", "entries", "child", "dynamic"), Keys("catalog", "entries", "child", "height"))
resolution.RecordNormalization(Keys("catalog", "entries", "child", "derived"), "derive final child summary", Keys("catalog", "entries", "child", "label"), Keys("catalog", "entries", "child", "dynamic"), Keys("catalog", "entries", "child", "height"))
return nil
},
Validate: func(config *configuration, validation *Validation) {

View File

@@ -287,7 +287,7 @@ func TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape(t *testi
Resolve: func(configuration *destinationLifecycleConfig, resolution *Resolution) error {
observed.resolve++
configuration.Tenant.Derived = configuration.Tenant.Dynamic
resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from mounted input", Keys("tenant", "dynamic"))
resolution.RecordNormalization(Keys("tenant", "derived"), "derive from mounted input", Keys("tenant", "dynamic"))
return nil
},
Validate: func(configuration *destinationLifecycleConfig, _ *Validation) {
@@ -425,7 +425,7 @@ func TestLayeredDestinationDiffUsesBilateralSensitivity(t *testing.T) {
definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
configuration.Tenant.Derived = configuration.Tenant.Dynamic
resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from dynamic input", Keys("tenant", "dynamic"))
resolution.RecordNormalization(Keys("tenant", "derived"), "derive from dynamic input", Keys("tenant", "dynamic"))
return nil
},
Secret: func(path Path) bool { return classify && path.String() == "/tenant/dynamic" },

View File

@@ -29,7 +29,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) {
Resolve: func(configuration *diffIntegrationConfig, resolution *Resolution) error {
resolveCalls++
configuration.Derived = configuration.Dynamic["visible"].(string)
resolution.Record(Keys("derived"), ChangeNormalization, "derive from effective values", Keys("credential"))
resolution.RecordNormalization(Keys("derived"), "derive from effective values", Keys("credential"))
return nil
},
}}
@@ -203,7 +203,7 @@ func TestDiffProtectsHistoricalSensitiveRemovalAndReplacement(t *testing.T) {
t.Run("removed historical value", func(t *testing.T) {
left := load("dynamic: {token: removed-canary}\n", func(configuration *configuration, resolution *Resolution) error {
delete(configuration.Dynamic, "token")
resolution.Record(secretPath, ChangeNormalization, "remove token")
resolution.RecordNormalization(secretPath, "remove token")
return nil
})
right := load("dynamic: {token: current-canary}\n", nil)
@@ -228,7 +228,7 @@ func TestDiffProtectsHistoricalSensitiveRemovalAndReplacement(t *testing.T) {
t.Run("replaced historical value", func(t *testing.T) {
left := load("dynamic: {token: original-canary}\n", func(configuration *configuration, resolution *Resolution) error {
configuration.Dynamic["token"] = "replacement-canary"
resolution.Record(secretPath, ChangeNormalization, "replace token")
resolution.RecordNormalization(secretPath, "replace token")
return nil
})
right := load("dynamic: {token: other-canary}\n", nil)

View File

@@ -525,10 +525,10 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
result, err := testLoad(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
c.M["new"] = map[string]any{"child": int64(7)}
delete(c.M, "old")
r.Record(Keys("m", "new"), ChangeDerivedDefault, "add nested", Keys("m", "old"))
r.Record(Keys("m", "old"), ChangeNormalization, "remove old")
r.RecordDerivedDefault(Keys("m", "new"), "add nested", Keys("m", "old"))
r.RecordNormalization(Keys("m", "old"), "remove old")
if overlap {
r.Record(Keys("m"), ChangeNormalization, "update container")
r.RecordNormalization(Keys("m"), "update container")
}
return nil
}}})
@@ -587,7 +587,7 @@ func TestExplanationReplayDescriptorsRemainDetachedAcrossQueries(t *testing.T) {
result, err := testLoad(Bytes("resolution-explanation.yml", []byte("count: 1\n")), Options[config]{
Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
value.Count = 2
resolution.Record(Keys("count"), ChangeNormalization, "normalized", Keys("count"))
resolution.RecordNormalization(Keys("count"), "normalized", Keys("count"))
return nil
}},
})
@@ -635,7 +635,7 @@ func TestConcurrentReadOnlyInspectionQueries(t *testing.T) {
result, err := testLoad(Bytes("concurrent-inspection.yml", []byte("group: {value: input}\n")), Options[config]{
Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
value.Group.Value = "resolved"
resolution.Record(Keys("group", "value"), ChangeNormalization, "normalized", Keys("group", "value"))
resolution.RecordNormalization(Keys("group", "value"), "normalized", Keys("group", "value"))
return nil
}},
})

View File

@@ -149,8 +149,8 @@ func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) {
definition: Definition[historyBudgetLayeredConfig]{
Resolve: func(configuration *historyBudgetLayeredConfig, resolution *Resolution) error {
configuration.Child.Value += "-resolved"
resolution.Record(Keys("child", "value"), ChangeNormalization, "normalize child")
resolution.Record(Keys("child"), ChangeDerivedDefault, "record enclosing child state")
resolution.RecordNormalization(Keys("child", "value"), "normalize child")
resolution.RecordDerivedDefault(Keys("child"), "record enclosing child state")
return nil
},
Validate: func(*historyBudgetLayeredConfig, *Validation) { counts.validations++ },

View File

@@ -290,7 +290,7 @@ func TestResolutionInputKeepsDetachedEntryMetadata(t *testing.T) {
configurationAtEntry := decoded.value.Interface().(configuration)
resolved := runResolution(&configurationAtEntry, func(value *configuration, resolution *Resolution) error {
value.Items[0] = "normalized"
resolution.Record(Keys("items"), ChangeNormalization, "normalized item")
resolution.RecordNormalization(Keys("items"), "normalized item")
return nil
}, schema, decoded, document, input)
if len(resolved.diagnostics) != 0 {

View File

@@ -85,7 +85,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin
resolvedContext, resolveDiagnostics := resolveLoadInput(schema, Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
value.Count = 6
resolution.Record(Keys("count"), ChangeNormalization, "normalize count")
resolution.RecordNormalization(Keys("count"), "normalize count")
return nil
}}, input, preparePostDecodeOverrideContext(input, decoded, assigned))
if len(resolveDiagnostics) != 0 {
@@ -130,7 +130,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin
Default(resolution, Keys("count"), &value.Count, 99, "fill missing count")
Default(resolution, Keys("derived"), &value.Derived, 42, "derive default")
value.APIKey = SecretEnv("RESOLVED_SECRET")
resolution.Record(Keys("api_key"), ChangeNormalization, "select effective secret reference")
resolution.RecordNormalization(Keys("api_key"), "select effective secret reference")
return nil
}
resolved := runResolution(&configuration, resolver, schema, prepared.decoded, prepared.referenceInput, prepared.resolution)

View File

@@ -39,7 +39,7 @@ func TestCLIHistoryFollowsInheritanceAndPrecedesResolution(t *testing.T) {
} else {
(*value)["child"].(map[string]any)["value"] = "resolved"
}
resolution.Record(recorded, ChangeNormalization, "normalize after CLI", parent)
resolution.RecordNormalization(recorded, "normalize after CLI", parent)
return nil
}
}

View File

@@ -62,7 +62,7 @@ empty: {}
Normalize(resolution, Keys("values"), &configuration.Values, []int{5},
"bounded values", Keys("shared", "imported"))
delete(configuration.Items, "remove")
resolution.Record(Keys("items"), ChangeNormalization, "removed disabled items")
resolution.RecordNormalization(Keys("items"), "removed disabled items")
Default(resolution, Keys("derived"), &configuration.Derived, 7,
"derived from equal", Keys("shared", "equal"), Keys("shared", "equal"))
resolution.Warning(Keys("values"), "values were bounded", Keys("shared", "imported"))
@@ -434,7 +434,7 @@ func TestResolutionInputRemainsFixedAcrossRecordedMutations(t *testing.T) {
Normalize(resolution, Keys("existing"), &value.Existing, "normalized", "normalized existing value")
value.Values["destination"] = value.Values["original"]
delete(value.Values, "original")
resolution.Record(Keys("values"), ChangeNormalization, "moved configured value")
resolution.RecordNormalization(Keys("values"), "moved configured value")
againOriginal, againOriginalFound := resolution.InputInfo(Keys("values", "original"))
againDestination, againDestinationFound := resolution.InputInfo(Keys("values", "destination"))
againExisting, againExistingFound := resolution.InputInfo(Keys("existing"))

View File

@@ -129,7 +129,6 @@ const (
CodeResolveWarning Code = "resolve.warning"
CodeResolveHookFailed Code = "resolve.hook_failed"
CodeResolveInvalidPath Code = "resolve.invalid_path"
CodeResolveInvalidKind Code = "resolve.invalid_kind"
CodeResolveInvalidRecord Code = "resolve.invalid_record"
CodeResolveInvalidTarget Code = "resolve.invalid_target"
CodeResolveInvalidValue Code = "resolve.invalid_value"

View File

@@ -112,11 +112,21 @@ func (r *Resolution) SourceContext(id string) (ResourceContext, bool) {
return r.activeState().input.sourceContext(id)
}
// Record declares the intent for a resolver mutation. A container path covers
// mutations to its descendants.
func (r *Resolution) Record(path Path, kind ChangeKind, reason string, dependencies ...Path) {
// RecordDerivedDefault declares a derived-default intent for a resolver
// mutation already performed by application code. A container path covers
// mutations to its descendants. This does not assign a value or suppress work
// for explicit input.
func (r *Resolution) RecordDerivedDefault(path Path, reason string, dependencies ...Path) {
state := r.activeState()
state.addRecord(path, kind, reason, dependencies, nil)
state.addRecord(path, ChangeDerivedDefault, reason, dependencies, nil)
}
// RecordNormalization declares a normalization intent for a resolver mutation
// already performed by application code. A container path covers mutations to
// its descendants. This does not assign a value.
func (r *Resolution) RecordNormalization(path Path, reason string, dependencies ...Path) {
state := r.activeState()
state.addRecord(path, ChangeNormalization, reason, dependencies, nil)
}
// Error reports a safe, value-free resolution error at path.
@@ -219,10 +229,6 @@ func (state *resolutionState) addRecord(path Path, kind ChangeKind, reason strin
if !state.validPath(path) {
return
}
if kind != ChangeDerivedDefault && kind != ChangeNormalization {
state.addDiagnostic(CodeResolveInvalidKind, path, "resolution record has an invalid change kind")
return
}
if reason == "" {
state.addDiagnostic(CodeResolveInvalidRecord, path, "resolution record requires a safe reason")
return

View File

@@ -167,7 +167,7 @@ func TestResolutionContainerRecordCoversSubtreeRemovalAndSnapshotsAreDetached(t
result, err := testLoad(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{
Resolve: func(configuration *config, resolution *Resolution) error {
delete(configuration.Items, "remove")
resolution.Record(Keys("items"), ChangeNormalization, "removed disabled entries")
resolution.RecordNormalization(Keys("items"), "removed disabled entries")
return nil
},
}})
@@ -259,12 +259,8 @@ func TestResolutionRejectsUnrecordedAndMismatchedMutations(t *testing.T) {
Normalize(resolution, Keys("value"), &unrelated, 2, "changed value")
return nil
}, code: CodeResolveInvalidTarget},
{name: "invalid record kind", resolve: func(_ *config, resolution *Resolution) error {
resolution.Record(Keys("value"), ChangeKind("other"), "invalid kind")
return nil
}, code: CodeResolveInvalidKind},
{name: "invalid path", resolve: func(_ *config, resolution *Resolution) error {
resolution.Record(Keys("unknown"), ChangeNormalization, "unknown field")
resolution.RecordNormalization(Keys("unknown"), "unknown field")
return nil
}, code: CodeResolveInvalidPath},
}
@@ -361,7 +357,7 @@ func TestResolutionPresenceUsesEntryShapeAcrossDynamicChanges(t *testing.T) {
}
}
resolution.Record(Keys("dynamic"), ChangeNormalization, "change dynamic shape")
resolution.RecordNormalization(Keys("dynamic"), "change dynamic shape")
child := ""
if test.keyPresent {
configuration.Dynamic = []any{}
@@ -466,7 +462,7 @@ func TestResolutionMetadataExplicitnessUsesEntryShape(t *testing.T) {
callbackErr = err
return err
}
resolution.Record(Keys("shape"), ChangeNormalization, "normalize shape")
resolution.RecordNormalization(Keys("shape"), "normalize shape")
test.mutate(configuration)
if err := checkEntry(); err != nil {
callbackErr = err
@@ -605,6 +601,8 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
}()
call()
}
requireClosed(t, func() { (*Resolution)(nil).RecordDerivedDefault(Keys("value"), "late") })
requireClosed(t, func() { (*Resolution)(nil).RecordNormalization(Keys("value"), "late") })
requireClosed(t, func() { (*Resolution)(nil).InputInfo(Keys("value")) })
requireClosed(t, func() { (&Resolution{}).Source("source-1") })
@@ -617,6 +615,8 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
}
requireClosed(t, func() { successful.InputInfo(Keys("value")) })
requireClosed(t, func() { successful.Source("source-1") })
requireClosed(t, func() { successful.RecordDerivedDefault(Keys("value"), "late") })
requireClosed(t, func() { successful.RecordNormalization(Keys("value"), "late") })
var failed *Resolution
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
@@ -627,6 +627,8 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
}
requireClosed(t, func() { failed.InputInfo(Keys("value")) })
requireClosed(t, func() { failed.Source("source-1") })
requireClosed(t, func() { failed.RecordDerivedDefault(Keys("value"), "late") })
requireClosed(t, func() { failed.RecordNormalization(Keys("value"), "late") })
}
func TestValidationContextPanicsAfterVersionCallback(t *testing.T) {

View File

@@ -436,7 +436,7 @@ func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) {
resolved := definition
resolved.Resolve = func(value *config, resolution *Resolution) error {
value.Items[1].ID = value.Items[0].ID
resolution.Record(Keys("items"), ChangeNormalization, "registered identities collide")
resolution.RecordNormalization(Keys("items"), "registered identities collide")
return nil
}
result, err = testLoad[config](Bytes("main", []byte("items: [{id: first}, {id: second}]\n")), Options[config]{Definition: resolved})

View File

@@ -1252,7 +1252,7 @@ func TestSourceVersionEvidencePropagatesThroughRecordedResolutionDependency(t *t
},
Resolve: func(value *configuration, resolution *Resolution) error {
value.Derived = value.Credentials.Password
resolution.Record(Keys("derived"), ChangeNormalization, "derive from credentials", Keys("credentials"))
resolution.RecordNormalization(Keys("derived"), "derive from credentials", Keys("credentials"))
return nil
},
},