diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 40da092..28d8f25 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -887,7 +887,14 @@ do not replace the agreed scheduling algorithm or add global caches. ## 22. Index Fallback Location Lookup During Effective Projection — P2 -Status: [ ] Open. +Status: [x] Complete. + +Completion: effective location projection lazily builds one read-only fallback +document lookup and shares it across field and entry visits. The ordinary +nearest-location helper retains its existing behavior. Added broad mixed +authored/default mappings, ancestor fallback, literal key/index mismatch, +synthetic CLI coordinates, and detached-projection coverage. Existing secret +and validation diagnostics and `./scripts/check.sh` pass. Owner: `documentWithMetadataLocations` in [`override_context.go`](../../internal/engine/override_context.go), using diff --git a/internal/engine/metadata_locations_test.go b/internal/engine/metadata_locations_test.go new file mode 100644 index 0000000..204a58d --- /dev/null +++ b/internal/engine/metadata_locations_test.go @@ -0,0 +1,79 @@ +package engine + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "gitea.maximumdirect.net/eric/ConfigKit/internal/document" +) + +func TestMetadataLocationsAcrossBroadAuthoredAndDefaultMappings(t *testing.T) { + type config map[string]map[string]string + defaults := config{} + var body strings.Builder + for index := range 96 { + name := fmt.Sprintf("authored-%03d", index) + fmt.Fprintf(&body, "%s: {present: source}\n", name) + defaults[name] = map[string]string{"absent": "default"} + defaults[fmt.Sprintf("default-%03d", index)] = map[string]string{"absent": "default"} + } + input := parseDocumentForDecode(t, body.String()) + decoded, issues := decodeConfiguration(mustSchemaFor[config](t), reflect.ValueOf(defaults), input) + if len(issues) != 0 { + t.Fatal(issues) + } + located := documentWithMetadataLocations(decoded.final, metadataFromDecoded(decoded), input) + inputIndex := buildDocumentLookup(input) + locatedIndex := buildDocumentLookup(located) + for index := range 96 { + authored := Keys(fmt.Sprintf("authored-%03d", index)) + mapping, _ := documentAtPathWithLookup(input, authored, inputIndex) + present, _ := documentAtPathWithLookup(input, authored.Key("present"), inputIndex) + for _, test := range []struct { + path Path + want document.Location + }{ + {authored.Key("present"), present.Location}, + {authored.Key("absent"), mapping.Location}, + {Keys(fmt.Sprintf("default-%03d", index), "absent"), input.Location}, + } { + node, found := documentAtPathWithLookup(located, test.path, locatedIndex) + if !found || node.Location != test.want { + t.Fatalf("location at %s = %#v, want %#v", test.path, node, test.want) + } + } + } + walkDocument(located, Path{}, func(_ Path, node *document.Node) { + for _, entry := range node.Mapping { + if entry.KeyLocation != entry.Value.Location || entry.ValueLocation != entry.Value.Location { + t.Fatal("entry and value locations disagree") + } + } + }) + before := decoded.final.Mapping[0].Value.Mapping[0].Value.Text + located.Mapping[0].Value.Mapping[0].Value.Text = "changed snapshot" + if decoded.final.Mapping[0].Value.Mapping[0].Value.Text != before || input.Mapping[0].Value.Mapping[0].Value.Text != "source" { + t.Fatal("location projection changed a borrowed input") + } +} + +func TestIndexedNearestLocationsPreserveMissingAndSyntheticCoordinates(t *testing.T) { + root := parseDocumentForDecode(t, "mapping: {'0': value}\nitems: [{value: source}]\nscalar: stop\n") + lookup := buildDocumentLookup(root) + for _, path := range []Path{Path{}, Keys("missing"), Keys("mapping", "missing"), Keys("mapping", "0"), Keys("mapping").Index(0), Keys("items").Index(4), Keys("items").Index(0).Key("missing"), Keys("scalar", "child"), Path{}.Index(-1)} { + want, wantOK := nearestDocumentLocation(root, path) + got, gotOK := nearestDocumentLocationWithLookup(root, path, lookup) + if got != want || gotOK != wantOK { + t.Fatalf("nearest location changed for %s: %#v/%t, want %#v/%t", path, got, gotOK, want, wantOK) + } + } + final := parseDocumentForDecode(t, "value: cli\n") + synthetic := Location{SourceID: "cli-1"} + metadata := Metadata{fields: map[string]FieldInfo{"/value": {Origin: Origin{Layer: LayerCLI, Locations: []Location{synthetic}}}}} + located := documentWithMetadataLocations(final, metadata, root) + if location := located.Mapping[0].Value.Location; location.SourceID != "cli-1" || location.Line != 0 || location.Column != 0 { + t.Fatalf("synthetic CLI location was replaced by fallback: %#v", location) + } +} diff --git a/internal/engine/override_context.go b/internal/engine/override_context.go index 4109ed7..df8d76e 100644 --- a/internal/engine/override_context.go +++ b/internal/engine/override_context.go @@ -171,16 +171,23 @@ func documentWithMetadataLocations(root *document.Node, metadata Metadata, fallb if len(fallback) != 0 { fallbackRoot = fallback[0] } + var lookup *documentLookup + fallbackLocation := func(path Path) (document.Location, bool) { + if lookup == nil { + lookup = buildDocumentLookup(fallbackRoot) + } + return nearestDocumentLocationWithLookup(fallbackRoot, path, lookup) + } var visit func(*document.Node, Path) visit = func(node *document.Node, path Path) { pointer := path.String() if info, exists := metadata.fields[pointer]; exists { if location, ok := metadataFieldLocation(metadata, pointer, info.Origin); ok { node.Location = location - } else if location, ok := nearestDocumentLocation(fallbackRoot, path); ok { + } else if location, ok := fallbackLocation(path); ok { node.Location = location } - } else if location, ok := nearestDocumentLocation(fallbackRoot, path); ok { + } else if location, ok := fallbackLocation(path); ok { node.Location = location } switch node.Kind { @@ -192,11 +199,11 @@ func documentWithMetadataLocations(root *document.Node, metadata Metadata, fallb if location, ok := metadataFieldLocation(metadata, childPath.String(), info.Origin); ok { entry.KeyLocation = location entry.ValueLocation = location - } else if location, ok := nearestDocumentLocation(fallbackRoot, childPath); ok { + } else if location, ok := fallbackLocation(childPath); ok { entry.KeyLocation = location entry.ValueLocation = location } - } else if location, ok := nearestDocumentLocation(fallbackRoot, childPath); ok { + } else if location, ok := fallbackLocation(childPath); ok { entry.KeyLocation = location entry.ValueLocation = location } diff --git a/internal/engine/validation.go b/internal/engine/validation.go index 00103d8..740d1f6 100644 --- a/internal/engine/validation.go +++ b/internal/engine/validation.go @@ -124,6 +124,10 @@ func validationMutationDiagnostic(path Path) Diagnostic { } func nearestDocumentLocation(root *document.Node, path Path) (document.Location, bool) { + return nearestDocumentLocationWithLookup(root, path, nil) +} + +func nearestDocumentLocationWithLookup(root *document.Node, path Path, lookup *documentLookup) (document.Location, bool) { if root == nil || !path.valid() { return document.Location{}, false } @@ -135,18 +139,22 @@ func nearestDocumentLocation(root *document.Node, path Path) (document.Location, if segment.kind == pathSegmentIndex { return nearest, locationKnown(nearest) } - found := false - for _, entry := range current.Mapping { - if entry.Key == segment.key { - current = entry.Value - nearest = current.Location - found = true - break + position := -1 + if lookup != nil { + position = lookup.find(current, segment.key) + } else { + for index, entry := range current.Mapping { + if entry.Key == segment.key { + position = index + break + } } } - if !found { + if position < 0 { return nearest, locationKnown(nearest) } + current = current.Mapping[position].Value + nearest = current.Location case document.Sequence: index, ok := schemaIndex(segment) if !ok || index >= len(current.Sequence) {