Clarify resolution metadata and sensitivity names
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Pre-v1 Public API Cleanup Implementation Plan
|
||||
|
||||
Status: planned; Stages 1–11 are pending.
|
||||
Status: in progress; Stage 1 is complete and Stages 2–11 are pending.
|
||||
Baseline: `07e4ae5d9236935389e81b207d4a8f00543fafce`.
|
||||
Audience: a Luna coding agent executing the stages sequentially.
|
||||
|
||||
@@ -97,7 +97,7 @@ it is not a reason to leave obvious neighboring comments wrong.
|
||||
|
||||
## Stage 1 — Clarify Resolution Entry Metadata And Sensitivity
|
||||
|
||||
Status: pending. Implements D1 and D2.
|
||||
Status: complete. Implements D1 and D2.
|
||||
|
||||
1. In [lifecycle.go](../../lifecycle.go), rename root `Resolution.InputInfo` to
|
||||
`EntryInfo` and `Definition.Secret` to `Sensitive`, without old aliases.
|
||||
|
||||
@@ -167,7 +167,7 @@ func LoadNotariusPaths(path, invocationDir string, overrides ...configkit.Overri
|
||||
resolveNotariusReference(configuration, resolution, invocationDir)
|
||||
return nil
|
||||
},
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/sensitive" },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func resolveNotariusReference(configuration *NotariusPathConfig, resolution *con
|
||||
if configuration.Reference == "" || filepath.IsAbs(configuration.Reference) {
|
||||
return
|
||||
}
|
||||
info, found := resolution.InputInfo(path)
|
||||
info, found := resolution.EntryInfo(path)
|
||||
if !found {
|
||||
resolution.Warning(path, "relative reference has no entry provenance")
|
||||
return
|
||||
|
||||
@@ -86,7 +86,7 @@ func resolveCollectionResources(configuration *CollectionBundleConfig, resolutio
|
||||
continue
|
||||
}
|
||||
contentPath := configkit.Keys("collections", collectionID, "prompts", promptID, "content_file")
|
||||
info, found := resolution.InputInfo(contentPath)
|
||||
info, found := resolution.EntryInfo(contentPath)
|
||||
if !found || len(info.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("prompt resource has no single declaring source")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configk
|
||||
return nil
|
||||
},
|
||||
},
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/service/version"
|
||||
},
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
if !resolution.IsExplicit(configkit.Keys("input")) {
|
||||
resolution.Error(configkit.Keys("input"), "input is required")
|
||||
}
|
||||
input, ok := resolution.InputInfo(configkit.Keys("input"))
|
||||
input, ok := resolution.EntryInfo(configkit.Keys("input"))
|
||||
if !ok || !input.Explicit {
|
||||
resolution.Error(configkit.Keys("input"), "missing input provenance")
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestFacadeInactiveContextsPreserveClosedFailure(t *testing.T) {
|
||||
call func()
|
||||
}{
|
||||
{"IsExplicit", func() { state.resolution.IsExplicit(path) }},
|
||||
{"InputInfo", func() { state.resolution.InputInfo(path) }},
|
||||
{"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") }},
|
||||
|
||||
@@ -352,7 +352,7 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
Resolve: func(configuration *map[string]any, resolution *configkit.Resolution) error {
|
||||
callbacks.resolve++
|
||||
resourcePath := configkit.Keys("catalog", "entries", "item", "resource")
|
||||
input, ok := resolution.InputInfo(resourcePath)
|
||||
input, ok := resolution.EntryInfo(resourcePath)
|
||||
if !ok || len(input.Origin.Locations) != 1 {
|
||||
return fmt.Errorf("mounted resource input has no single source origin")
|
||||
}
|
||||
@@ -373,8 +373,8 @@ func TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff(t *testing.T) {
|
||||
resolution.Record(configkit.Keys("catalog", "entries", "item", "resource_text"), configkit.ChangeNormalization, "read source-relative summary", resourcePath)
|
||||
return nil
|
||||
},
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
Validate: func(*map[string]any, *configkit.Validation) { callbacks.validate++ },
|
||||
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == tokenPath.String() },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestFacadeDiffKeepsPublicSequenceChangesWithHistoricalDescendants(t *testin
|
||||
t.Helper()
|
||||
result, err := configkit.Load(configkit.Bytes("historical-sequence.yml", []byte("items: [{secret: disposable-canary}, "+public+"]\n")),
|
||||
configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/items/0/secret" },
|
||||
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")
|
||||
@@ -71,7 +71,7 @@ func TestFacadeDiffKeepsPublicScalarAcrossHistoricalAbsence(t *testing.T) {
|
||||
Dynamic map[string]any `yaml:"dynamic"`
|
||||
}
|
||||
options := configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/dynamic/entry/token" },
|
||||
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")
|
||||
@@ -281,7 +281,7 @@ func TestFacadeDiffUsesFrozenFilesAndRunsCallbacksOnlyDuringLoad(t *testing.T) {
|
||||
|
||||
func TestFacadeDiffCombinesSecretSensitivityAndLoadErrors(t *testing.T) {
|
||||
t.Setenv("FACADE_DIFF_SECRET", "credential-one")
|
||||
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Secret: func(path configkit.Path) bool {
|
||||
secretOptions := configkit.Options[facadeDiffSecretConfig]{Definition: configkit.Definition[facadeDiffSecretConfig]{Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/secret"
|
||||
}}}
|
||||
left, err := configkit.Load[facadeDiffSecretConfig](configkit.Bytes("left.yml", []byte("secret: FACADE_DIFF_SECRET\n")), secretOptions)
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestReplacedSecretRemainsAbsentFromSafeOutput(t *testing.T) {
|
||||
}
|
||||
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte("secret: new-secret-canary\n"))}, configkit.LayeredOptions[config]{
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"secret": "old-secret-canary"})}}}},
|
||||
Definition: configkit.Definition[config]{Secret: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
||||
Definition: configkit.Definition[config]{Sensitive: func(path configkit.Path) bool { return path.String() == "/secret" }},
|
||||
})
|
||||
if err != nil || result.Config.Secret != "new-secret-canary" {
|
||||
t.Fatalf("secret load = %#v, %v", result, err)
|
||||
|
||||
@@ -42,7 +42,7 @@ entries:
|
||||
Definition: Definition[map[string]any]{
|
||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "id")},
|
||||
Secret: func(candidate Path) bool {
|
||||
Sensitive: func(candidate Path) bool {
|
||||
return candidate.String() == "/services/records/0/version"
|
||||
},
|
||||
},
|
||||
@@ -107,7 +107,7 @@ entries:
|
||||
Definition: Definition[map[string]any]{
|
||||
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
|
||||
SequenceIdentities: []SequenceIdentity{SequenceIdentityAt(Keys("services", "records"), "version")},
|
||||
Secret: func(candidate Path) bool {
|
||||
Sensitive: func(candidate Path) bool {
|
||||
return candidate.String() == "/services/records/0/version"
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestFacadeInheritancePreservesPresenceOriginsAndHistory(t *testing.T) {
|
||||
if !resolution.IsExplicit(configkit.Keys("entries", "child", "enabled")) {
|
||||
t.Error("inherited false is not explicit")
|
||||
}
|
||||
info, found := resolution.InputInfo(configkit.Keys("entries", "child", "enabled"))
|
||||
info, found := resolution.EntryInfo(configkit.Keys("entries", "child", "enabled"))
|
||||
if !found || len(info.Origin.Inheritance) != 1 {
|
||||
t.Errorf("resolver inherited origin = %#v, %v", info, found)
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestFacadeIdentifiedInheritanceResolvesChainsAndNestedCollections(t *testin
|
||||
result, err := configkit.Load(filesystem.File("config/identified.yml"), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: registrations,
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/jobs/0/credential" || path.String() == "/jobs/1/credential" || path.String() == "/jobs/2/credential"
|
||||
},
|
||||
},
|
||||
@@ -221,7 +221,7 @@ func TestFacadeIdentifiedInheritanceFailureDoesNotRevealSecretCanary(t *testing.
|
||||
_, err := configkit.Load(configkit.Bytes("identified-failure.yml", input), configkit.Options[configuration]{
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("jobs"), "id")},
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/jobs/0/credential" },
|
||||
},
|
||||
})
|
||||
if err == nil || strings.Contains(err.Error(), canary) {
|
||||
@@ -468,7 +468,7 @@ func TestFacadeInheritedSecretsStayRedactedAfterLookup(t *testing.T) {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
}
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("secret.yml", []byte("entries: {parent: {credential: CONFIGKIT_INHERITED_TEST_SECRET, plain: visible}, child: {extends: parent}}\n")), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
}})
|
||||
if err != nil || result.Config.Entries["child"].Credential.Value() != canary {
|
||||
t.Fatalf("inherited credential = %#v, %v", result, err)
|
||||
|
||||
@@ -59,8 +59,8 @@ func TestFacadeLayeredMixedSourcesAndInspection(t *testing.T) {
|
||||
configkit.Default(resolution, configkit.Keys("derived"), &config.Derived, config.Settings["native"], "derive from native", configkit.Keys("settings", "native"))
|
||||
return nil
|
||||
},
|
||||
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
||||
Validate: func(config *layeredFacadeConfig, validation *configkit.Validation) { checks.validation++ },
|
||||
Sensitive: func(path configkit.Path) bool { return path.String() == "/settings/secret" },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestFacadeResolutionPresenceUsesDynamicEntryShape(t *testing.T) {
|
||||
Definition: configkit.Definition[configuration]{
|
||||
Resolve: func(_ *configuration, resolution *configkit.Resolution) error {
|
||||
check := func(path configkit.Path, want bool) error {
|
||||
info, found := resolution.InputInfo(path)
|
||||
info, found := resolution.EntryInfo(path)
|
||||
explicit := resolution.IsExplicit(path)
|
||||
if found != want || explicit != want || (found && info.Explicit != explicit) {
|
||||
return errors.New("resolution presence disagrees with dynamic entry shape")
|
||||
|
||||
@@ -598,7 +598,7 @@ entries:
|
||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath), Imports: true}}}},
|
||||
Definition: configkit.Definition[configuration]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("entries"), "id")},
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return path.String() == "/entries/0/token" || path.String() == "/entries/1/token"
|
||||
},
|
||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||
@@ -611,7 +611,7 @@ entries:
|
||||
if resolution.IsExplicit(configkit.Keys("entries").Index(0).Key("omitted")) {
|
||||
t.Error("resolver treated an omitted field as explicit")
|
||||
}
|
||||
info, found := resolution.InputInfo(configkit.Keys("entries").Index(0).Key("count"))
|
||||
info, found := resolution.EntryInfo(configkit.Keys("entries").Index(0).Key("count"))
|
||||
if !found || info.Origin.Layer != configkit.LayerBase {
|
||||
t.Errorf("resolver current-record input info = %#v, %v", info, found)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestFacadeSourceVersionEvidenceFollowsAssembledSequenceRanges(t *testing.T)
|
||||
definition := configkit.Definition[map[string]any]{
|
||||
SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("records"), "id")},
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("records").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Secret: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
||||
Sensitive: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
||||
}
|
||||
first := "records: [{id: first, version: first-version}]\n"
|
||||
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
||||
@@ -544,8 +544,8 @@ func TestFacadeSourceVersionEvidenceIsSanitizedAndDetached(t *testing.T) {
|
||||
var callbackSource configkit.SourceInfo
|
||||
var callbackContext configkit.ResourceContext
|
||||
definition := configkit.Definition[map[string]any]{
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Secret: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
||||
Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||
Sensitive: func(candidate configkit.Path) bool { return candidate.String() == "/service/version" },
|
||||
Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||
var found bool
|
||||
callbackSource, found = resolution.Source("source-1")
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestUnderlaySequenceDescendantOrigins(t *testing.T) {
|
||||
Definition: configkit.Definition[map[string]any]{Resolve: func(_ *map[string]any, resolution *configkit.Resolution) error {
|
||||
for _, pointer := range paths {
|
||||
path, _ := configkit.ParsePath(pointer)
|
||||
info, ok := resolution.InputInfo(path)
|
||||
info, ok := resolution.EntryInfo(path)
|
||||
if !ok {
|
||||
t.Errorf("missing resolver input %s", pointer)
|
||||
}
|
||||
|
||||
29
lifecycle.go
29
lifecycle.go
@@ -7,14 +7,14 @@ import (
|
||||
)
|
||||
|
||||
// Definition contains reusable application rules for a configuration type.
|
||||
// Secret is a pure predicate that can add sensitivity to semantic paths.
|
||||
// Sensitive is a pure predicate that can add sensitivity to semantic paths.
|
||||
// Definitions should be treated as read-only after being shared.
|
||||
type Definition[T any] struct {
|
||||
Defaults func() T
|
||||
Resolve func(*T, *Resolution) error
|
||||
Validate func(*T, *Validation)
|
||||
Version *VersionRule
|
||||
Secret func(Path) bool
|
||||
Defaults func() T
|
||||
Resolve func(*T, *Resolution) error
|
||||
Validate func(*T, *Validation)
|
||||
Version *VersionRule
|
||||
Sensitive func(Path) bool
|
||||
// SequenceIdentities declares the scalar fields used to combine records and
|
||||
// resolve record inheritance at selected sequence destinations. Registration
|
||||
// errors are reported before source acquisition. Before successful return,
|
||||
@@ -234,14 +234,15 @@ func (r *Resolution) IsExplicit(path Path) bool {
|
||||
return r.engineContext().IsExplicit(toEnginePath(path))
|
||||
}
|
||||
|
||||
// InputInfo returns detached entry-state metadata for path. The entry state is
|
||||
// the decoded configuration, including retained static defaults, immediately
|
||||
// before resolution starts. It is fixed while resolution records mutate the
|
||||
// working configuration; unknown or shape-mismatched paths return false.
|
||||
// EntryInfo returns detached entry-state metadata for path. The entry state is
|
||||
// the decoded configuration, including retained static defaults and CLI
|
||||
// assignments, immediately before resolution starts. It is fixed while
|
||||
// resolution records mutate the working configuration; unknown or
|
||||
// shape-mismatched paths return false.
|
||||
//
|
||||
// InputInfo is valid only during the resolution callback. See the configuration
|
||||
// EntryInfo is valid only during the resolution callback. See the configuration
|
||||
// contract for source and provenance semantics.
|
||||
func (r *Resolution) InputInfo(path Path) (FieldInfo, bool) {
|
||||
func (r *Resolution) EntryInfo(path Path) (FieldInfo, bool) {
|
||||
value, ok := r.engineContext().InputInfo(toEnginePath(path))
|
||||
return fromEngineFieldInfo(value), ok
|
||||
}
|
||||
@@ -368,8 +369,8 @@ func toEngineDefinition[T any](definition Definition[T]) engine.Definition[T] {
|
||||
}
|
||||
}
|
||||
}
|
||||
if definition.Secret != nil {
|
||||
result.Secret = func(path engine.Path) bool { return definition.Secret(fromEnginePath(path)) }
|
||||
if definition.Sensitive != nil {
|
||||
result.Secret = func(path engine.Path) bool { return definition.Sensitive(fromEnginePath(path)) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func BenchmarkStartupConstruction(b *testing.B) {
|
||||
options := configkit.LayeredOptions[startupCatalog]{Definition: configkit.Definition[startupCatalog]{
|
||||
// Include a simple path-based classifier, as catalog consumers use
|
||||
// this callback even when the representative input has no credentials.
|
||||
Secret: func(path configkit.Path) bool {
|
||||
Sensitive: func(path configkit.Path) bool {
|
||||
return strings.HasSuffix(path.String(), "/extra_params/token")
|
||||
},
|
||||
Resolve: func(catalog *startupCatalog, resolution *configkit.Resolution) error {
|
||||
@@ -101,7 +101,7 @@ func BenchmarkStartupConstruction(b *testing.B) {
|
||||
"timeout_seconds", "service_tier", "reasoning_effort", "api_key_env",
|
||||
"api_key_required", "extra_params",
|
||||
} {
|
||||
info, exists := resolution.InputInfo(configkit.Keys("profiles", id, field))
|
||||
info, exists := resolution.EntryInfo(configkit.Keys("profiles", id, field))
|
||||
fields[field] = startupPresence{explicit: exists && info.Explicit, null: exists && info.Null}
|
||||
}
|
||||
captured[id] = fields
|
||||
|
||||
Reference in New Issue
Block a user