Index resolution coverage and document reads
This commit is contained in:
@@ -198,7 +198,7 @@ Scope: one helper plus tests, no caller migration yet.
|
||||
Validate: focused helper tests and the everyday check. Storage follows indexed
|
||||
token material/payloads; tests do not fix representation or allocation counts.
|
||||
|
||||
## Stage 7: Index resolution coverage and record reads
|
||||
## Stage 7: Index resolution coverage and record reads — Complete
|
||||
|
||||
Roadmap candidate 3, first integration. Owner: `internal/engine/resolution.go`.
|
||||
|
||||
|
||||
52
internal/engine/document_lookup.go
Normal file
52
internal/engine/document_lookup.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package engine
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
|
||||
// documentLookup contains read-only mapping positions for a completed document.
|
||||
type documentLookup struct {
|
||||
byNode map[*document.Node]map[string]int
|
||||
}
|
||||
|
||||
func buildDocumentLookup(root *document.Node) *documentLookup {
|
||||
lookup := &documentLookup{byNode: make(map[*document.Node]map[string]int)}
|
||||
visited := make(map[*document.Node]struct{})
|
||||
var visit func(*document.Node)
|
||||
visit = func(node *document.Node) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := visited[node]; ok {
|
||||
return
|
||||
}
|
||||
visited[node] = struct{}{}
|
||||
switch node.Kind {
|
||||
case document.Mapping:
|
||||
entries := make(map[string]int, len(node.Mapping))
|
||||
for index, entry := range node.Mapping {
|
||||
if _, exists := entries[entry.Key]; !exists {
|
||||
entries[entry.Key] = index
|
||||
}
|
||||
visit(entry.Value)
|
||||
}
|
||||
lookup.byNode[node] = entries
|
||||
case document.Sequence:
|
||||
for _, child := range node.Sequence {
|
||||
visit(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(root)
|
||||
return lookup
|
||||
}
|
||||
|
||||
func (lookup *documentLookup) find(node *document.Node, key string) int {
|
||||
if lookup == nil {
|
||||
return -1
|
||||
}
|
||||
if entries := lookup.byNode[node]; entries != nil {
|
||||
if index, ok := entries[key]; ok {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -31,6 +31,10 @@ func indexDocument(node *document.Node, path Path, index map[string]*document.No
|
||||
}
|
||||
|
||||
func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
|
||||
return documentAtPathWithLookup(root, path, nil)
|
||||
}
|
||||
|
||||
func documentAtPathWithLookup(root *document.Node, path Path, lookup *documentLookup) (*document.Node, bool) {
|
||||
if root == nil || !path.valid() {
|
||||
return nil, false
|
||||
}
|
||||
@@ -41,17 +45,21 @@ func documentAtPath(root *document.Node, path Path) (*document.Node, bool) {
|
||||
if segment.kind == pathSegmentIndex {
|
||||
return nil, false
|
||||
}
|
||||
found := false
|
||||
for _, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
current = entry.Value
|
||||
found = true
|
||||
break
|
||||
entryIndex := -1
|
||||
if lookup != nil {
|
||||
entryIndex = lookup.find(current, segment.key)
|
||||
} else {
|
||||
for index, entry := range current.Mapping {
|
||||
if entry.Key == segment.key {
|
||||
entryIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if entryIndex < 0 || entryIndex >= len(current.Mapping) {
|
||||
return nil, false
|
||||
}
|
||||
current = current.Mapping[entryIndex].Value
|
||||
case document.Sequence:
|
||||
index, ok := schemaIndex(segment)
|
||||
if !ok || index >= len(current.Sequence) {
|
||||
|
||||
@@ -272,14 +272,17 @@ func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error
|
||||
}
|
||||
deltas := compareConfigurationValues(decoded.final, after)
|
||||
ordered := orderedResolutionRecords(state.records)
|
||||
coverage := newResolutionCoverageIndexFromRecords(ordered)
|
||||
beforeLookup := buildDocumentLookup(decoded.final)
|
||||
afterLookup := buildDocumentLookup(after)
|
||||
for _, delta := range deltas {
|
||||
if coveringResolutionRecord(ordered, delta.path) == nil {
|
||||
if _, ok := coverage.deepestCoveringID(delta.path); !ok {
|
||||
state.addDiagnostic(CodeResolveUnrecordedChange, delta.path, "application resolution changed a value without a covering record")
|
||||
}
|
||||
}
|
||||
for _, record := range ordered {
|
||||
_, beforePresent := documentAtPath(decoded.final, record.path)
|
||||
afterNode, afterPresent := documentAtPath(after, record.path)
|
||||
_, beforePresent := documentAtPathWithLookup(decoded.final, record.path, beforeLookup)
|
||||
afterNode, afterPresent := documentAtPathWithLookup(after, record.path, afterLookup)
|
||||
if !beforePresent && !afterPresent {
|
||||
state.addDiagnostic(CodeResolveInvalidRecord, record.path, "resolution record does not identify an existing or removed value")
|
||||
continue
|
||||
@@ -294,8 +297,8 @@ func runResolution[T any](configuration *T, resolver func(*T, *Resolution) error
|
||||
|
||||
snapshots := make([]resolutionSnapshot, 0, len(ordered))
|
||||
for _, record := range ordered {
|
||||
beforeNode, _ := documentAtPath(decoded.final, record.path)
|
||||
afterNode, _ := documentAtPath(after, record.path)
|
||||
beforeNode, _ := documentAtPathWithLookup(decoded.final, record.path, beforeLookup)
|
||||
afterNode, _ := documentAtPathWithLookup(after, record.path, afterLookup)
|
||||
snapshots = append(snapshots, resolutionSnapshot{
|
||||
change: Change{
|
||||
Path: record.path.clone(),
|
||||
@@ -331,14 +334,45 @@ func orderedResolutionRecords(records map[string]*resolutionRecord) []*resolutio
|
||||
return result
|
||||
}
|
||||
|
||||
func coveringResolutionRecord(records []*resolutionRecord, path Path) *resolutionRecord {
|
||||
var covering *resolutionRecord
|
||||
for _, record := range records {
|
||||
if pathContains(record.path, path) && (covering == nil || len(record.path.segments) > len(covering.path.segments)) {
|
||||
covering = record
|
||||
type resolutionCoverageIndex struct {
|
||||
prefix pathPrefixIndex
|
||||
depths []int
|
||||
}
|
||||
|
||||
func newResolutionCoverageIndex(paths []Path) *resolutionCoverageIndex {
|
||||
index := &resolutionCoverageIndex{depths: make([]int, len(paths))}
|
||||
for id, path := range paths {
|
||||
index.depths[id] = len(path.segments)
|
||||
if path.valid() {
|
||||
index.prefix.insert(path.String(), id)
|
||||
}
|
||||
}
|
||||
return covering
|
||||
return index
|
||||
}
|
||||
|
||||
func newResolutionCoverageIndexFromRecords(records []*resolutionRecord) *resolutionCoverageIndex {
|
||||
index := &resolutionCoverageIndex{depths: make([]int, len(records))}
|
||||
for id, record := range records {
|
||||
index.depths[id] = len(record.path.segments)
|
||||
if record.path.valid() {
|
||||
index.prefix.insert(record.path.String(), id)
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func (index *resolutionCoverageIndex) deepestCoveringID(path Path) (int, bool) {
|
||||
if !path.valid() {
|
||||
return 0, false
|
||||
}
|
||||
bestID, bestDepth, found := 0, -1, false
|
||||
index.prefix.visitAncestors(path.String(), func(id int) {
|
||||
depth := index.depths[id]
|
||||
if !found || depth > bestDepth || (depth == bestDepth && id < bestID) {
|
||||
bestID, bestDepth, found = id, depth, true
|
||||
}
|
||||
})
|
||||
return bestID, found
|
||||
}
|
||||
|
||||
func pathContains(container, descendant Path) bool {
|
||||
|
||||
73
internal/engine/resolution_index_test.go
Normal file
73
internal/engine/resolution_index_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
func TestResolutionCoverageIndexMatchesOrderedScan(t *testing.T) {
|
||||
paths := []Path{
|
||||
Keys("group"),
|
||||
Keys("group", "field"),
|
||||
Keys("group", "field"),
|
||||
Keys("group", "field", "nested"),
|
||||
Keys("other"),
|
||||
}
|
||||
index := newResolutionCoverageIndex(paths)
|
||||
queries := []Path{
|
||||
Path{},
|
||||
Keys("group"),
|
||||
Keys("group", "field"),
|
||||
Keys("group", "field", "nested", "leaf"),
|
||||
Keys("group", "missing"),
|
||||
Keys("other", "child"),
|
||||
Keys("group").Index(-1),
|
||||
}
|
||||
for _, query := range queries {
|
||||
wantID, wantDepth, wantFound := 0, -1, false
|
||||
for id, candidate := range paths {
|
||||
if pathContains(candidate, query) && (!wantFound || len(candidate.segments) > wantDepth) {
|
||||
wantID, wantDepth, wantFound = id, len(candidate.segments), true
|
||||
}
|
||||
}
|
||||
gotID, gotFound := index.deepestCoveringID(query)
|
||||
if gotID != wantID || gotFound != wantFound {
|
||||
t.Errorf("deepestCoveringID(%s) = %d, %v; want %d, %v", query.String(), gotID, gotFound, wantID, wantFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentLookupMatchesPathScan(t *testing.T) {
|
||||
name := &document.Node{Kind: document.String, Text: "item"}
|
||||
sequence := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{
|
||||
{Kind: document.String, Text: "zero"},
|
||||
{Kind: document.Mapping, Mapping: []document.Entry{{Key: "name", Value: name}}},
|
||||
}}
|
||||
root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{
|
||||
{Key: "0", Value: &document.Node{Kind: document.String, Text: "numeric key"}},
|
||||
{Key: "01", Value: &document.Node{Kind: document.String, Text: "leading zero key"}},
|
||||
{Key: "items", Value: sequence},
|
||||
}}
|
||||
lookup := buildDocumentLookup(root)
|
||||
paths := []Path{
|
||||
Path{},
|
||||
Keys("0"),
|
||||
Keys("01"),
|
||||
Keys("items"),
|
||||
Keys("items").Index(0),
|
||||
Keys("items").Index(1).Key("name"),
|
||||
Keys("items").Index(2),
|
||||
Keys("items").Key("name"),
|
||||
Keys("missing"),
|
||||
Keys("items").Index(-1),
|
||||
(Path{}).Index(0),
|
||||
}
|
||||
for _, path := range paths {
|
||||
wantNode, wantFound := documentAtPath(root, path)
|
||||
gotNode, gotFound := documentAtPathWithLookup(root, path, lookup)
|
||||
if gotNode != wantNode || gotFound != wantFound {
|
||||
t.Errorf("indexed lookup %s = (%p, %v), want (%p, %v)", path.String(), gotNode, gotFound, wantNode, wantFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user