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

@@ -1,6 +1,6 @@
# Pre-v1 Public API Cleanup Implementation Plan
Status: in progress; Stages 1–5 are complete and Stages 6–11 are pending.
Status: in progress; Stages 1–6 are complete and Stages 7–11 are pending.
Baseline: `07e4ae5d9236935389e81b207d4a8f00543fafce`.
Audience: a Luna coding agent executing the stages sequentially.
@@ -254,7 +254,7 @@ bootstrap behavior remain unchanged.
## Stage 6 — Replace Kind-Selecting Recording With Intent Methods
Status: pending. Implements D6 and N4.
Status: complete. Implements D6 and N4.
1. Replace root and engine `Resolution.Record` with:

View File

@@ -104,9 +104,9 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
}
prompt.ResourceText = string(data)
collection.Prompts[promptID] = prompt
resolution.Record(
resolution.RecordNormalization(
configkit.Keys("collections", collectionID, "prompts", promptID, "resource_text"),
configkit.ChangeNormalization, "read source-relative prompt content", contentPath,
"read source-relative prompt content", contentPath,
)
}
configuration.Collections[collectionID] = collection

View File

@@ -101,8 +101,8 @@ func resolveRollout(configuration *RolloutConfig, resolution *configkit.Resoluti
url := fmt.Sprintf("https://%s:%d", endpoint.Host, endpoint.Port)
if endpoint.URL != url {
endpoint.URL = url
resolution.Record(configkit.Keys("service", "endpoint", "url"),
configkit.ChangeNormalization, "derived endpoint URL",
resolution.RecordNormalization(configkit.Keys("service", "endpoint", "url"),
"derived endpoint URL",
configkit.Keys("service", "endpoint", "host"),
configkit.Keys("service", "endpoint", "port"))
}

View File

@@ -54,7 +54,7 @@ func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Res
},
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
configuration.ResolvedLabel = "for:" + configuration.RunLabel
resolution.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
resolution.RecordNormalization(configkit.Keys("resolved_label"),
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
return nil
},

View File

@@ -245,6 +245,8 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
}
for _, context := range []func(){
func() { retainedResolution.IsExplicit(configkit.Keys("input")) },
func() { retainedResolution.RecordDerivedDefault(configkit.Keys("input"), "too late") },
func() { retainedResolution.RecordNormalization(configkit.Keys("input"), "too late") },
func() { retainedValidation.Warning(configkit.Keys("input"), "too late") },
} {
func() {

View File

@@ -27,7 +27,8 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
{"EntryInfo", func() { state.resolution.EntryInfo(path) }},
{"Source", func() { state.resolution.Source("source") }},
{"SourceContext", func() { state.resolution.SourceContext("source") }},
{"Record", func() { state.resolution.Record(path, configkit.ChangeNormalization, "reason") }},
{"RecordDerivedDefault", func() { state.resolution.RecordDerivedDefault(path, "reason") }},
{"RecordNormalization", func() { state.resolution.RecordNormalization(path, "reason") }},
{"Resolution.Error", func() { state.resolution.Error(path, "message") }},
{"Resolution.Warning", func() { state.resolution.Warning(path, "message") }},
{"Default", func() { configkit.Default(state.resolution, path, &target, 1, "reason") }},

View File

@@ -370,7 +370,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
}
entry := facadeDestinationMap(t, facadeDestinationMap(t, facadeDestinationMap(t, *configuration, "catalog"), "entries"), "item")
entry["resource_text"] = string(data)
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
resolution.RecordNormalization(configkit.Keys("catalog", "entries", "item", "resource_text"), "read source-relative summary", resourcePath)
return nil
},
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },

View File

@@ -32,7 +32,7 @@ func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testin
Sensitive: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
current.Items[0] = "public scalar"
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "normalize first item")
resolution.RecordNormalization(configkit.Keys("items"), "normalize first item")
return nil
},
}})
@@ -74,7 +74,7 @@ func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
Sensitive: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
delete(current.Dynamic, "entry")
resolution.Record(configkit.Keys("dynamic", "entry"), configkit.ChangeNormalization, "remove entry")
resolution.RecordNormalization(configkit.Keys("dynamic", "entry"), "remove entry")
return nil
},
}}
@@ -221,7 +221,7 @@ func TestFacadeDiffIgnoresSourceAndResolutionHistory(t *testing.T) {
resolve := func(value string) configkit.Options[config] {
return configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(current *config, resolution *configkit.Resolution) error {
current.Count = len(value) + 8
resolution.Record(configkit.Keys("count"), configkit.ChangeNormalization, "set effective count")
resolution.RecordNormalization(configkit.Keys("count"), "set effective count")
return nil
}}}
}

View File

@@ -237,7 +237,7 @@ func TestFacadeSearchRunsCallbacksOnceAndKeepsInspectionWithSelectedFile(t *test
Resolve: func(configuration *facadeDiscoveryLifecycleConfig, resolution *Resolution) error {
resolveCalls++
configuration.Resolved = "from:" + configuration.Value
resolution.Record(Keys("resolved"), ChangeNormalization, "derive from selected value", Keys("value"))
resolution.RecordNormalization(Keys("resolved"), "derive from selected value", Keys("value"))
return nil
},
Validate: func(_ *facadeDiscoveryLifecycleConfig, _ *Validation) { validateCalls++ },

View File

@@ -29,7 +29,7 @@ func TestFinalSequenceIdentityTreatsLateExtendsFieldsAsPayload(t *testing.T) {
if phase == "resolution" {
definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["items"] = items
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "supply application payload")
resolution.RecordNormalization(configkit.Keys("items"), "supply application payload")
return nil
}
}
@@ -77,7 +77,7 @@ func TestFinalSequenceIdentityValidatesLateValues(t *testing.T) {
case "resolution":
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["items"] = items
resolution.Record(configkit.Keys("items"), configkit.ChangeNormalization, "create effective records")
resolution.RecordNormalization(configkit.Keys("items"), "create effective records")
return nil
}
}
@@ -134,7 +134,7 @@ func TestFinalSequenceIdentityMatchesLateDynamicNamespaces(t *testing.T) {
case "resolution":
options.Definition.Resolve = func(value *map[string]any, resolution *configkit.Resolution) error {
(*value)["collections"] = map[string]any{name: map[string]any{"items": items}}
resolution.Record(configkit.Keys("collections"), configkit.ChangeNormalization, "create namespaced records")
resolution.RecordNormalization(configkit.Keys("collections"), "create namespaced records")
return nil
}
}

View File

@@ -62,7 +62,7 @@ func TestPublicHistoryUsesResolutionShapeAttribution(t *testing.T) {
Definition: configkit.Definition[map[string]any]{
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
(*configuration)["shape"] = []any{"replacement"}
resolution.Record(configkit.Keys("shape"), configkit.ChangeNormalization, "replace numeric mapping key")
resolution.RecordNormalization(configkit.Keys("shape"), "replace numeric mapping key")
return nil
},
},

View File

@@ -0,0 +1,43 @@
package configkit
import "testing"
func TestFacadeResolutionRecordingIntents(t *testing.T) {
type configuration struct {
Derived string `yaml:"derived"`
Normalized string `yaml:"normalized"`
}
result, err := Load(Bytes("recording.yml", []byte("derived: explicit\nnormalized: before\n")), Options[configuration]{
Definition: Definition[configuration]{Resolve: func(value *configuration, resolution *Resolution) error {
value.Derived = "changed explicit input"
resolution.RecordDerivedDefault(Keys("derived"), "derive explicit input")
value.Normalized = "after"
resolution.RecordNormalization(Keys("normalized"), "normalize configured input")
return nil
}},
})
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
path Path
kind ChangeKind
}{
{path: Keys("derived"), kind: ChangeDerivedDefault},
{path: Keys("normalized"), kind: ChangeNormalization},
} {
info, ok := result.Metadata().Lookup(test.path)
if !ok || info.Origin.Operation != test.kind {
t.Errorf("metadata %s = %#v, %v; want operation %q", test.path, info, ok, test.kind)
}
history := result.Metadata().History(test.path)
if len(history) == 0 || history[len(history)-1].Origin.Operation != test.kind {
t.Errorf("history %s = %#v; want final operation %q", test.path, history, test.kind)
}
}
if !result.Metadata().IsExplicit(Keys("derived")) {
t.Fatal("recording a derived default suppressed explicit-input provenance")
}
}

View File

@@ -807,7 +807,7 @@ func TestFacadeSourceVersionLifecycleKeepsConsumedHeadersOutOfPayloadHistory(t *
},
Resolve: func(current *configuration, resolution *configkit.Resolution) error {
current.Service.Resolved = "resolved once"
resolution.Record(configkit.Keys("service", "resolved"), configkit.ChangeNormalization, "derive resolved value")
resolution.RecordNormalization(configkit.Keys("service", "resolved"), "derive resolved value")
return nil
},
Validate: func(current *configuration, validation *configkit.Validation) {

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
},
},

View File

@@ -259,10 +259,19 @@ func (r *Resolution) Source(id string) (SourceInfo, bool) {
return fromEngineSourceInfo(value), ok
}
// 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) {
r.engineContext().Record(toEnginePath(path), engine.ChangeKind(kind), reason, toEnginePaths(dependencies)...)
// RecordDerivedDefault records 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) {
r.engineContext().RecordDerivedDefault(toEnginePath(path), reason, toEnginePaths(dependencies)...)
}
// RecordNormalization records 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) {
r.engineContext().RecordNormalization(toEnginePath(path), reason, toEnginePaths(dependencies)...)
}
// Error reports a safe, value-free resolution error at path.

View File

@@ -130,7 +130,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"