Simplify empty import selection evidence

This commit is contained in:
2026-10-27 15:05:22 +00:00
parent 31ae30372e
commit 791cf16136
11 changed files with 58 additions and 87 deletions

View File

@@ -46,21 +46,21 @@ func fromEngineSourceInfo(value engine.SourceInfo) SourceInfo {
return SourceInfo{
ID: value.ID, Label: value.Label, Path: value.Path, Destination: fromEnginePath(value.Destination),
PlacementMode: PlacementMode(engine.SourceInfoPlacementModeForFacade(value)),
DestinationUnresolved: engine.SourceInfoDestinationUnresolvedForFacade(value),
DestinationIsFallback: engine.SourceInfoDestinationUnresolvedForFacade(value),
Layer: Layer(value.Layer), Kind: SourceKind(value.Kind), UnderlayIndex: value.UnderlayIndex,
ImportedFrom: fromEngineLocationPointer(value.ImportedFrom), VersionEvidence: fromEngineSourceVersionEvidence(value.VersionCheck),
ImportSelections: fromEngineImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
EmptyImportSelections: fromEngineEmptyImportSelections(engine.SourceInfoImportSelectionsForFacade(value)),
}
}
func fromEngineImportSelections(values []engine.ImportSelectionForFacade) []ImportSelection {
func fromEngineEmptyImportSelections(values []engine.ImportSelectionForFacade) []EmptyImportSelection {
if values == nil {
return nil
}
result := make([]ImportSelection, len(values))
result := make([]EmptyImportSelection, len(values))
for index, value := range values {
result[index] = ImportSelection{
result[index] = EmptyImportSelection{
Path: fromEnginePath(value.Path), Location: fromEngineLocation(value.Location),
Selector: ImportSelector(value.Selector), Outcome: ImportSelectionOutcome(value.Outcome),
Selector: ImportSelector(value.Selector),
}
}
return result

View File

@@ -1,6 +1,6 @@
# Pre-v1 Public API Cleanup Implementation Plan
Status: in progress; Stages 1–2 are complete and Stages 3–11 are pending.
Status: in progress; Stages 1–3 are complete and Stages 4–11 are pending.
Baseline: `07e4ae5d9236935389e81b207d4a8f00543fafce`.
Audience: a Luna coding agent executing the stages sequentially.
@@ -161,7 +161,7 @@ with existing version behavior intact.
## Stage 3 — Simplify Empty-Selection And Destination Evidence
Status: pending. Implements D4 and D5.
Status: complete. Implements D4 and D5.
1. Replace root `ImportSelection` with `EmptyImportSelection` and
`SourceInfo.ImportSelections` with `EmptyImportSelections`. Retain `Path`,

View File

@@ -24,8 +24,8 @@ func Example_identifiedPromptCatalog() {
}
emptySearches := 0
for _, source := range result.Sources() {
for _, selection := range source.ImportSelections {
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
for _, selection := range source.EmptyImportSelections {
if selection.Selector == configkit.ImportSelectorDirectory {
emptySearches++
}
}

View File

@@ -111,7 +111,7 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
t.Fatalf("empty pattern = %#v, %v", empty, err)
}
emptySources := empty.Sources()
if len(emptySources) != 1 || len(emptySources[0].ImportSelections) != 1 || emptySources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].ImportSelections[0].Selector != ImportSelectorDirectory || emptySources[0].ImportSelections[0].Outcome != ImportSelectionNone {
if len(emptySources) != 1 || len(emptySources[0].EmptyImportSelections) != 1 || emptySources[0].EmptyImportSelections[0].Path.String() != "/configkit/imports/0" || emptySources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
t.Fatalf("empty-pattern evidence = %#v", emptySources)
}
required := true
@@ -120,7 +120,7 @@ func TestImportDefaultsPreserveEmptyPatternAndRequiredness(t *testing.T) {
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != CodeImportMissing {
t.Fatalf("required empty selection error = %T %v", err, err)
}
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].ImportSelections) != 0 {
if sources := loadError.Sources(); len(sources) != 1 || len(sources[0].EmptyImportSelections) != 0 {
t.Fatalf("required empty selection was recorded as optional: %#v", sources)
}
}
@@ -230,15 +230,15 @@ value: loaded
callbackPath := ""
loaded, err := Load(File(mainPath), Options[config]{Imports: true, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
first, ok := resolution.Source("source-1")
if !ok || len(first.ImportSelections) != 1 {
if !ok || len(first.EmptyImportSelections) != 1 {
t.Fatalf("resolution source = %#v, %v", first, ok)
}
first.ImportSelections[0].Path = Keys("callback mutation")
first.EmptyImportSelections[0].Path = Keys("callback mutation")
second, ok := resolution.Source("source-1")
if !ok || second.ImportSelections[0].Path.String() != "/configkit/imports/0" {
if !ok || second.EmptyImportSelections[0].Path.String() != "/configkit/imports/0" {
t.Fatalf("resolution snapshot changed after mutation: %#v", second)
}
callbackPath = second.ImportSelections[0].Path.String()
callbackPath = second.EmptyImportSelections[0].Path.String()
return nil
}}})
if err != nil {
@@ -248,11 +248,11 @@ value: loaded
t.Fatalf("callback path = %q", callbackPath)
}
sources := loaded.Sources()
if len(sources) != 1 || len(sources[0].ImportSelections) != 1 || sources[0].ImportSelections[0].Location.SourceID != sources[0].ID || sources[0].ImportSelections[0].Selector != ImportSelectorDirectory {
if len(sources) != 1 || len(sources[0].EmptyImportSelections) != 1 || sources[0].EmptyImportSelections[0].Location.SourceID != sources[0].ID || sources[0].EmptyImportSelections[0].Selector != ImportSelectorDirectory {
t.Fatalf("result source evidence = %#v", sources)
}
sources[0].ImportSelections[0].Path = Keys("result mutation")
if got := loaded.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
sources[0].EmptyImportSelections[0].Path = Keys("result mutation")
if got := loaded.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
t.Fatalf("result snapshot changed after mutation: %q", got)
}
@@ -268,11 +268,11 @@ value: loaded
t.Fatalf("failed load error = %T %v", err, err)
}
errorSources := loadError.Sources()
if len(errorSources) != 1 || len(errorSources[0].ImportSelections) != 1 || errorSources[0].ImportSelections[0].Path.String() != "/configkit/imports/0" {
if len(errorSources) != 1 || len(errorSources[0].EmptyImportSelections) != 1 || errorSources[0].EmptyImportSelections[0].Path.String() != "/configkit/imports/0" {
t.Fatalf("error source evidence = %#v", errorSources)
}
errorSources[0].ImportSelections[0].Path = Keys("error mutation")
if got := loadError.Sources()[0].ImportSelections[0].Path.String(); got != "/configkit/imports/0" {
errorSources[0].EmptyImportSelections[0].Path = Keys("error mutation")
if got := loadError.Sources()[0].EmptyImportSelections[0].Path.String(); got != "/configkit/imports/0" {
t.Fatalf("error snapshot changed after mutation: %q", got)
}
}

View File

@@ -211,7 +211,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
if got := ordinary.Config["entries"].([]any); len(got) != 1 || got[0].(map[string]any)["id"] != "one" {
t.Fatalf("ordinary item placement = %#v", ordinary.Config)
}
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationUnresolved {
if sources := ordinary.Sources(); len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeSequenceItem || sources[0].Destination.String() != "/entries" || sources[0].DestinationIsFallback {
t.Fatalf("ordinary item source = %#v", sources)
}
@@ -230,7 +230,7 @@ func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
t.Fatalf("layered item placement = %#v", layered.Config)
}
for _, source := range layered.Sources() {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
t.Errorf("layered item source = %#v", source)
}
}
@@ -334,7 +334,7 @@ items:
t.Fatalf("callback snapshots = %#v, contexts %#v", callbackSources, callbackContexts)
}
for index, source := range callbackSources {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/items" || source.DestinationIsFallback {
t.Errorf("callback source = %#v", source)
}
if callbackContexts[index].FS == nil || callbackContexts[index].BaseDir != "records" {
@@ -349,7 +349,7 @@ items:
t.Errorf("shifted item origin at %s = %#v, %v", path.String(), info, ok)
}
}
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationUnresolved {
if got := result.Sources(); len(got) != 3 || got[1].Destination.String() != "/items" || got[1].DestinationIsFallback {
t.Fatalf("final item sources = %#v", got)
}
}
@@ -372,9 +372,9 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
t.Fatalf("early failure = %v", err)
}
sources := loadError.Sources()
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
sources[0].PlacementMode != configkit.PlacementModeSequenceItem ||
sources[1].Destination.String() != "/items" || !sources[1].DestinationUnresolved ||
sources[1].Destination.String() != "/items" || !sources[1].DestinationIsFallback ||
sources[1].PlacementMode != configkit.PlacementModeMapping {
t.Fatalf("early source snapshots = %#v", sources)
}
@@ -399,13 +399,13 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
t.Fatalf("post-placement failure = %v", err)
}
sources = loadError.Sources()
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationUnresolved ||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationUnresolved {
if len(sources) != 2 || sources[0].Destination.String() != "/items" || sources[0].DestinationIsFallback ||
sources[1].Destination.String() != "/items/0/metadata" || sources[1].DestinationIsFallback {
t.Fatalf("post-placement source snapshots = %#v", sources)
}
firstSnapshot := sources[1]
sources[1].Destination = configkit.Keys("mutated")
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationUnresolved {
if again := loadError.Sources(); len(again) != 2 || again[1].Destination.String() != firstSnapshot.Destination.String() || again[1].DestinationIsFallback {
t.Fatalf("failure source snapshots were aliased: %#v", again)
}
@@ -420,7 +420,7 @@ func TestFacadeItemDestinationFallbackEvidenceSurvivesFailures(t *testing.T) {
if err != nil {
t.Fatalf("successful item load: %v", err)
}
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationUnresolved || callback.PlacementMode != configkit.PlacementModeMapping {
if callback.Destination.String() != "/items/0/metadata" || callback.DestinationIsFallback || callback.PlacementMode != configkit.PlacementModeMapping {
t.Fatalf("callback source snapshot = %#v", callback)
}
}
@@ -450,13 +450,13 @@ func TestFacadeNestedItemFallbackUsesNearestResolvedContainer(t *testing.T) {
if len(sources) != 3 {
t.Fatalf("nested source snapshots = %#v", sources)
}
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
if got := sources[0]; got.Destination.String() != "/groups" || got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
t.Errorf("outer item snapshot = %#v", got)
}
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeSequenceItem {
if got := sources[1]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeSequenceItem {
t.Errorf("nested item fallback = %#v", got)
}
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationUnresolved || got.PlacementMode != configkit.PlacementModeMapping {
if got := sources[2]; got.Destination.String() != "/groups" || !got.DestinationIsFallback || got.PlacementMode != configkit.PlacementModeMapping {
t.Errorf("nested helper fallback = %#v", got)
}
}
@@ -496,7 +496,7 @@ func TestFacadeSequenceIdentityRegistrationIsDetached(t *testing.T) {
func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
zero := configkit.SourceInfo{}
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationUnresolved {
if zero.PlacementMode != configkit.PlacementModeMapping || zero.DestinationIsFallback {
t.Fatalf("zero source info = %#v", zero)
}
loaded, err := configkit.Load[map[string]any](configkit.Mapping("mapping", map[string]any{"value": true}))
@@ -504,12 +504,12 @@ func TestFacadeSourceInfoPlacementCopiesAndZeroValues(t *testing.T) {
t.Fatal(err)
}
sources := loaded.Sources()
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationUnresolved {
if len(sources) != 1 || sources[0].PlacementMode != configkit.PlacementModeMapping || sources[0].DestinationIsFallback {
t.Fatalf("mapping source evidence = %#v", sources)
}
first := sources[0]
first.Destination = configkit.Keys("caller")
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationUnresolved {
if again := loaded.Sources(); len(again) != 1 || again[0].Destination.String() != "" || again[0].DestinationIsFallback {
t.Fatalf("source evidence changed after caller mutation: %#v", again)
}
}
@@ -646,7 +646,7 @@ entries:
t.Fatalf("callback sources = %#v; contexts = %#v", callbackItems, callbackContexts)
}
for _, source := range callbackItems {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationUnresolved {
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/entries" || source.DestinationIsFallback {
t.Errorf("callback item source = %#v", source)
}
}

View File

@@ -289,7 +289,7 @@ func (s *importTraversal) recordEmptyImportSelection(sourceIndex int, spec impor
// missing-source rollback, unlike a pointer into the source slice.
s.sources[sourceIndex].importSelections = append(s.sources[sourceIndex].importSelections, importSelectionEvidence{
path: spec.path.clone(), location: spec.location,
selector: spec.selector, outcome: importSelectionNone,
selector: spec.selector,
})
}

View File

@@ -43,7 +43,7 @@ func TestBroadEmptyImportSelectionsKeepDeclaringSourceOwnership(t *testing.T) {
}
for selectionIndex, selection := range source.importSelections {
if selection.path.String() != wantPaths[selectionIndex] || selection.location.SourceID != source.ID ||
selection.selector != importSelectorPath || selection.outcome != importSelectionNone {
selection.selector != importSelectorPath {
t.Errorf("source %s selection %d = %#v", source.ID, selectionIndex, selection)
}
}
@@ -90,7 +90,7 @@ func TestOptionalEmptyImportSelectionsRemainSourceLocalAndDetached(t *testing.T)
}
for index, selection := range evidence {
if selection.path.String() != wantPaths[index] || selection.location.SourceID != sources[0].ID ||
selection.selector != wantSelectors[index] || selection.outcome != importSelectionNone {
selection.selector != wantSelectors[index] {
t.Errorf("selection %d = %#v", index, selection)
}
}
@@ -137,7 +137,7 @@ value: main
t.Fatalf("failed-load sources = %#v", sources)
}
selection := sources[0].importSelections[0]
if selection.path.String() != "/configkit/imports/0" || selection.location.SourceID != sources[0].ID || selection.selector != importSelectorGlob || selection.outcome != importSelectionNone {
if selection.path.String() != "/configkit/imports/0" || selection.location.SourceID != sources[0].ID || selection.selector != importSelectorGlob {
t.Fatalf("retained selection = %#v", selection)
}
sources[0].importSelections[0].path.segments[0].key = "changed"

View File

@@ -9,13 +9,6 @@ import (
"strings"
)
type importSelectionOutcome uint8
const (
importSelectionNone importSelectionOutcome = iota
importSelectionHasCandidates
)
type selectedImportCandidate struct {
path string
accessPath string

View File

@@ -206,7 +206,6 @@ type importSelectionEvidence struct {
path Path
location Location
selector importSelector
outcome importSelectionOutcome
}
// ImportSelectionForFacade is a root-converted source-selection transport
@@ -215,7 +214,6 @@ type ImportSelectionForFacade struct {
Path Path // source-local import declaration path
Location Location // authored location and declaring source ID
Selector string // path, dir, or glob
Outcome string // none for a completed empty optional selection
}
// SourceInfoImportSelectionsForFacade returns detached selection records for
@@ -229,7 +227,6 @@ func SourceInfoImportSelectionsForFacade(source SourceInfo) []ImportSelectionFor
result[index] = ImportSelectionForFacade{
Path: selection.path.clone(), Location: selection.location,
Selector: importSelectorString(selection.selector),
Outcome: importSelectionOutcomeString(selection.outcome),
}
}
return result
@@ -250,13 +247,6 @@ func importSelectorString(value importSelector) string {
}
}
func importSelectionOutcomeString(value importSelectionOutcome) string {
if value == importSelectionNone {
return "none"
}
return ""
}
// SourceInfoPlacementModeForFacade converts private placement evidence to its
// root-owned numeric representation.
func SourceInfoPlacementModeForFacade(source SourceInfo) uint8 {

View File

@@ -192,16 +192,16 @@ type SourceInfo struct {
Label string
Path string
// Destination is the effective mapping or sequence-container path. It is a
// detached snapshot. A fallback container is marked by DestinationUnresolved.
// detached snapshot. A fallback container is marked by DestinationIsFallback.
Destination Path
// PlacementMode describes how this source was placed. Mapping is the zero
// value; CLI and overlay sources report mapping placement unless item placement
// was applied to that source.
PlacementMode PlacementMode
// DestinationUnresolved marks Destination as the nearest resolved enclosing
// DestinationIsFallback marks Destination as the nearest resolved enclosing
// container when an enclosing item index is not yet assigned. It is false for
// exact destinations, including an item source's own sequence container.
DestinationUnresolved bool
DestinationIsFallback bool
Layer Layer
Kind SourceKind
UnderlayIndex int
@@ -212,12 +212,12 @@ type SourceInfo struct {
// and failed loads, text is conservatively withheld; successful final source
// records reflect completed sensitivity classification.
VersionEvidence *SourceVersionEvidence
// ImportSelections records completed optional declarations that selected no
// EmptyImportSelections records completed optional declarations that selected no
// sources, in declaration order. Paths are local to this declaring source.
// Each record identifies the declaration mapping, selector, and empty outcome;
// Each record identifies the declaration mapping and selector;
// it does not list candidates or explain why a path was absent. Result, error,
// and callback snapshots detach these records and their paths.
ImportSelections []ImportSelection
EmptyImportSelections []EmptyImportSelection
}
// ImportSelector identifies the selector used by an import declaration.
@@ -232,22 +232,13 @@ const (
ImportSelectorGlob ImportSelector = "glob"
)
// ImportSelectionOutcome identifies the established result of import selection.
type ImportSelectionOutcome string
const (
// ImportSelectionNone means the optional declaration selected no sources.
ImportSelectionNone ImportSelectionOutcome = "none"
)
// ImportSelection records an optional import declaration that selected no
// EmptyImportSelection records an optional import declaration that selected no
// sources. Path is local to the declaring source; Location identifies its
// declaration mapping and source ID.
type ImportSelection struct {
type EmptyImportSelection struct {
Path Path
Location Location
Selector ImportSelector
Outcome ImportSelectionOutcome
}
// SourceVersionEvidence describes a source-local version declaration observed

View File

@@ -35,7 +35,7 @@ func TestSourceInfoDestinationConversion(t *testing.T) {
}
internal := engineSourceInfoFromLoad(t, path, "source-1", options)
converted := fromEngineSourceInfo(internal)
if converted.Destination.String() != destination.String() || converted.PlacementMode != PlacementModeSequenceItem || converted.DestinationUnresolved {
if converted.Destination.String() != destination.String() || converted.PlacementMode != PlacementModeSequenceItem || converted.DestinationIsFallback {
t.Fatalf("public source evidence = %#v", converted)
}
}
@@ -70,18 +70,15 @@ func TestImportSelectionConversion(t *testing.T) {
file := writeRecordConfig(t, "configkit:\n imports:\n - glob: absent/*.yml\nvalue: main\n")
internal := engineSourceInfoFromLoad(t, file, "source-1", engine.Options[recordConfig]{Imports: true})
converted := fromEngineSourceInfo(internal)
if len(converted.ImportSelections) != 1 || converted.ImportSelections[0].Path.String() != "/configkit/imports/0" ||
converted.ImportSelections[0].Location.SourceID != internal.ID || converted.ImportSelections[0].Location.Line != 3 ||
converted.ImportSelections[0].Selector != ImportSelectorGlob || converted.ImportSelections[0].Outcome != ImportSelectionNone {
t.Fatalf("converted selection evidence = %#v", converted.ImportSelections)
if len(converted.EmptyImportSelections) != 1 || converted.EmptyImportSelections[0].Path.String() != "/configkit/imports/0" ||
converted.EmptyImportSelections[0].Location.SourceID != internal.ID || converted.EmptyImportSelections[0].Location.Line != 3 ||
converted.EmptyImportSelections[0].Selector != ImportSelectorGlob {
t.Fatalf("converted selection evidence = %#v", converted.EmptyImportSelections)
}
if converted.ImportSelections[0].Outcome != ImportSelectionNone {
t.Fatalf("source selection outcome = %q", converted.ImportSelections[0].Outcome)
}
if fromEngineImportSelections(nil) != nil {
if fromEngineEmptyImportSelections(nil) != nil {
t.Fatal("converting nil engine selections returned a non-nil slice")
}
if fromEngineImportSelections([]engine.ImportSelectionForFacade{}) == nil {
if fromEngineEmptyImportSelections([]engine.ImportSelectionForFacade{}) == nil {
t.Fatal("converting an empty engine selection slice returned nil")
}
}