Add detached source lookup to results and errors

This commit is contained in:
2026-10-27 15:26:19 +00:00
parent 24ab623cb2
commit c96197c417
8 changed files with 302 additions and 3 deletions

View File

@@ -50,6 +50,21 @@ func (e *LoadError) Sources() []SourceInfo {
return result
}
// Source returns a detached record from the sanitized source evidence retained
// before loading failed, including placement evidence available at that point.
// IDs are local to the failed load. It does not acquire sources or access the
// filesystem. Empty or unknown IDs return false.
func (e *LoadError) Source(id string) (SourceInfo, bool) {
if e == nil || e.value == nil {
return SourceInfo{}, false
}
value, ok := e.value.Source(id)
if !ok {
return SourceInfo{}, false
}
return fromEngineSourceInfo(value), true
}
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
// control characters and propagates writer errors, including short writes.
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {

View File

@@ -172,6 +172,18 @@ the returned configuration or inspection records cannot alter ConfigKit's privat
snapshot. ConfigKit performs no logging. `Result.WriteEffective` exposes only the
frozen redacted projection.
`Result.Source(id)` returns the detached record for an acquired source already
listed by `Result.Sources()`. Source IDs are local to one load; empty and unknown
IDs return false. This passive lookup uses retained evidence and does not reacquire
the source, access the filesystem, or look up credentials. A source ID can be
obtained from the source records or exact origin locations.
`LoadError.Source(id)` performs the same lookup over the partial, already-sanitized
inventory returned by `LoadError.Sources()`. It preserves the source evidence and
placement available when loading failed, which may be incomplete; it does not
promise a record for every attempted source. Nil or zero-valued errors and empty
or unknown IDs return false.
### Completion and resource lifetime
`Load` is synchronous and complete on return. Success means input assembly,

View File

@@ -1,6 +1,6 @@
# Pre-v1 Public API Cleanup Implementation Plan
Status: in progress; Stages 1–8 are complete and Stages 9–11 are pending.
Status: in progress; Stages 1–9 are complete and Stages 10–11 are pending.
Baseline: `07e4ae5d9236935389e81b207d4a8f00543fafce`.
Audience: a Luna coding agent executing the stages sequentially.
@@ -356,7 +356,7 @@ D8's role check is resolved and its unproduced declaration is removed.
## Stage 9 — Add Detached Source Lookup On Results And Load Errors
Status: pending. Implements A1 and N5.
Status: complete. Implements A1 and N5.
1. Add engine-local `lookupSourceInfo(sources []SourceInfo, id string)
(SourceInfo, bool)` beside source cloning helpers. Return zero/false for

View File

@@ -50,6 +50,16 @@ func (e *LoadError) Sources() []SourceInfo {
return cloneSourceInfos(e.sources)
}
// Source returns a detached record from the source evidence retained before
// loading failed. IDs are local to the failed load. Empty or unknown IDs return
// false.
func (e *LoadError) Source(id string) (SourceInfo, bool) {
if e == nil {
return SourceInfo{}, false
}
return lookupSourceInfo(e.sources, id)
}
// WriteDiagnostics writes diagnostics in canonical order. It escapes terminal
// control characters and propagates writer errors, including short writes.
func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error {

View File

@@ -119,6 +119,16 @@ func (r *Inspection) Sources() []SourceInfo {
return cloneSourceInfos(r.sources)
}
// Source returns a detached record for an acquired source in this load. IDs are
// local to the load; the lookup does not reacquire the source or access the
// filesystem. Empty or unknown IDs return false.
func (r *Inspection) Source(id string) (SourceInfo, bool) {
if r == nil {
return SourceInfo{}, false
}
return lookupSourceInfo(r.sources, id)
}
// SourceContext returns a passive filesystem view for an acquired source.
// Unknown IDs and sources without a filesystem context return false.
func (r *Inspection) SourceContext(id string) (ResourceContext, bool) {

View File

@@ -92,7 +92,7 @@ const (
CodeDecodeUnknownField Code = "decode.unknown_field"
CodeDecodeArrayLength Code = "decode.array_length"
CodeOptionsInvalid Code = "options.invalid"
CodeOptionsInvalid Code = "options.invalid"
CodeImportDisabled Code = "import.disabled"
CodeImportInvalid Code = "import.invalid"
@@ -345,6 +345,18 @@ func cloneSourceInfo(source SourceInfo) SourceInfo {
return source
}
func lookupSourceInfo(sources []SourceInfo, id string) (SourceInfo, bool) {
if id == "" {
return SourceInfo{}, false
}
for _, source := range sources {
if source.ID == id {
return cloneSourceInfo(source), true
}
}
return SourceInfo{}, false
}
func cloneSourceVersionCheck(value *SourceVersionCheck) *SourceVersionCheck {
if value == nil {
return nil

View File

@@ -214,6 +214,20 @@ func (r *Result[T]) Sources() []SourceInfo {
return result
}
// Source returns a detached record for an acquired source in this load. IDs are
// local to the load; the lookup does not reacquire the source or access the
// filesystem. Empty or unknown IDs return false.
func (r *Result[T]) Source(id string) (SourceInfo, bool) {
if r == nil || r.inspection == nil {
return SourceInfo{}, false
}
value, ok := r.inspection.Source(id)
if !ok {
return SourceInfo{}, false
}
return fromEngineSourceInfo(value), true
}
// Diagnostics returns detached nonfatal diagnostics from the load.
func (r *Result[T]) Diagnostics() []Diagnostic {
if r == nil || r.inspection == nil {

226
source_lookup_test.go Normal file
View File

@@ -0,0 +1,226 @@
package configkit_test
import (
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
func TestResultSourceLookup(t *testing.T) {
directory := t.TempDir()
mainPath := writeSourceLookupFile(t, directory, "main.yml", `version: MAIN-SOURCE-CANARY
configkit:
imports:
- path: imported.yml
- dir: absent
main_value: main
`)
importedPath := writeSourceLookupFile(t, directory, "imported.yml", `version: IMPORTED-SOURCE-CANARY
imported_value: imported
`)
underlayPath := writeSourceLookupFile(t, directory, "underlay.yml", "underlay_value: underlay\n")
definition := configkit.Definition[map[string]any]{
Version: &configkit.VersionRule{
Path: configkit.Keys("version"),
Check: func(configkit.Version, *configkit.Validation) error { return nil },
},
Sensitive: func(path configkit.Path) bool { return path.String() == "/version" },
}
result, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath), Imports: true}, configkit.LayeredOptions[map[string]any]{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.File(underlayPath)}}}},
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
})
if err != nil || result == nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) {
t.Fatalf("LoadLayered = %#v, %v, diagnostics %#v", result, err, loadError.Diagnostics())
}
t.Fatalf("LoadLayered = %#v, %v", result, err)
}
sources := result.Sources()
if len(sources) != 3 {
t.Fatalf("sources = %#v", sources)
}
for _, want := range sources {
got, ok := result.Source(want.ID)
if !ok || !reflect.DeepEqual(got, want) {
t.Fatalf("Source(%q) = %#v, %v; Sources entry = %#v", want.ID, got, ok, want)
}
}
main, ok := result.Source(sources[0].ID)
if !ok || main.Layer != configkit.LayerMain || len(main.EmptyImportSelections) != 1 ||
main.VersionEvidence == nil || !main.VersionEvidence.Redacted || main.VersionEvidence.Version.Text != "" ||
main.VersionEvidence.Location == nil || main.VersionEvidence.Location.SourceID != main.ID {
t.Fatalf("main source evidence = %#v, %v", main, ok)
}
var imported, underlay bool
for _, source := range sources {
if source.Path == importedPath {
imported = source.Layer == configkit.LayerMain && source.ImportedFrom != nil &&
source.VersionEvidence != nil && source.VersionEvidence.Location != nil &&
source.VersionEvidence.Location.SourceID == source.ID
}
if source.Path == underlayPath {
underlay = source.Layer == configkit.LayerUnderlay
}
}
if !imported || !underlay {
t.Fatalf("imported or underlay source evidence missing: %#v", sources)
}
if got, ok := result.Source(""); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("empty source ID = %#v, %v", got, ok)
}
if got, ok := result.Source("unknown-source"); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("unknown source ID = %#v, %v", got, ok)
}
var nilResult *configkit.Result[map[string]any]
if got, ok := nilResult.Source("source-1"); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("nil result lookup = %#v, %v", got, ok)
}
if got, ok := (&configkit.Result[map[string]any]{}).Source("source-1"); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("zero result lookup = %#v, %v", got, ok)
}
main.EmptyImportSelections[0].Path = configkit.Keys("mutated")
main.EmptyImportSelections[0].Location.Line = 99
main.VersionEvidence.Path = configkit.Keys("mutated")
main.VersionEvidence.Location.Line = 99
main.VersionEvidence.Version.Text = "mutated"
for _, source := range sources {
if source.ImportedFrom != nil {
found, _ := result.Source(source.ID)
found.ImportedFrom.Line = 99
break
}
}
if got := result.Sources(); !reflect.DeepEqual(got, sources) {
t.Fatalf("lookup mutations changed enumeration: %#v", got)
}
for _, want := range sources {
if got, ok := result.Source(want.ID); !ok || !reflect.DeepEqual(got, want) {
t.Fatalf("repeated Source(%q) = %#v, %v; want %#v", want.ID, got, ok, want)
}
}
for _, path := range []string{mainPath, importedPath, underlayPath} {
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
}
if got, ok := result.Source(sources[0].ID); !ok || !reflect.DeepEqual(got, sources[0]) {
t.Fatalf("source lookup after inputs were removed = %#v, %v", got, ok)
}
}
func TestLoadErrorSourceLookup(t *testing.T) {
directory := t.TempDir()
mainPath := writeSourceLookupFile(t, directory, "main.yml", `configkit:
imports:
- path: item.yml
destination: [items]
placement: sequence-item
`)
writeSourceLookupFile(t, directory, "item.yml", `configkit:
imports:
- path: helper.yml
destination: [metadata]
`)
const canary = "FAILED-SOURCE-LOOKUP-CANARY"
helperPath := writeSourceLookupFile(t, directory, "helper.yml", `version: `+canary+`
configkit:
imports:
- dir: absent
- path: missing.yml
required: true
key: value
`)
definition := configkit.Definition[map[string]any]{
Version: &configkit.VersionRule{
Path: configkit.Keys("version"),
Check: func(configkit.Version, *configkit.Validation) error { return nil },
},
Sensitive: func(path configkit.Path) bool { return strings.HasSuffix(path.String(), "/version") },
}
_, err := configkit.Load(configkit.File(mainPath), configkit.Options[map[string]any]{
Imports: true, Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
})
var loadError *configkit.LoadError
if !errors.As(err, &loadError) {
t.Fatalf("load error = %T %v", err, err)
}
if strings.Contains(err.Error(), canary) {
t.Fatalf("load error disclosed source-version canary: %v", err)
}
sources := loadError.Sources()
if len(sources) < 3 {
t.Fatalf("partial source inventory = %#v", sources)
}
for _, want := range sources {
got, ok := loadError.Source(want.ID)
if !ok || !reflect.DeepEqual(got, want) {
t.Fatalf("Source(%q) = %#v, %v; Sources entry = %#v", want.ID, got, ok, want)
}
}
canonicalHelper, err := filepath.EvalSymlinks(helperPath)
if err != nil {
t.Fatal(err)
}
var helper configkit.SourceInfo
for _, source := range sources {
if source.Path == canonicalHelper {
helper = source
break
}
}
if helper.ID == "" || !helper.DestinationIsFallback || helper.Destination.String() != "/items" ||
len(helper.EmptyImportSelections) != 1 || helper.VersionEvidence == nil ||
!helper.VersionEvidence.Redacted || helper.VersionEvidence.Version.Text != "" ||
helper.VersionEvidence.Location == nil {
t.Fatalf("partial helper source evidence = %#v", helper)
}
if got, ok := loadError.Source(""); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("empty source ID = %#v, %v", got, ok)
}
if got, ok := loadError.Source("unknown-source"); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("unknown source ID = %#v, %v", got, ok)
}
var nilError *configkit.LoadError
if got, ok := nilError.Source(helper.ID); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("nil load error lookup = %#v, %v", got, ok)
}
if got, ok := (&configkit.LoadError{}).Source(helper.ID); ok || !reflect.DeepEqual(got, configkit.SourceInfo{}) {
t.Fatalf("zero load error lookup = %#v, %v", got, ok)
}
mutated, ok := loadError.Source(helper.ID)
if !ok {
t.Fatal("partial helper lookup missing")
}
mutated.Destination = configkit.Keys("mutated")
mutated.ImportedFrom.Line = 99
mutated.EmptyImportSelections[0].Path = configkit.Keys("mutated")
mutated.EmptyImportSelections[0].Location.Line = 99
mutated.VersionEvidence.Path = configkit.Keys("mutated")
mutated.VersionEvidence.Location.Line = 99
mutated.VersionEvidence.Version.Text = "mutated"
if got := loadError.Sources(); !reflect.DeepEqual(got, sources) {
t.Fatalf("lookup mutations changed error enumeration: %#v", got)
}
if got, ok := loadError.Source(helper.ID); !ok || !reflect.DeepEqual(got, helper) {
t.Fatalf("repeated partial lookup = %#v, %v; want %#v", got, ok, helper)
}
}
func writeSourceLookupFile(t *testing.T, directory, name, contents string) string {
t.Helper()
path := filepath.Join(directory, name)
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
return path
}