From 633c5cd519682883d3d2c59a6c6f95f38151eb95 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 7 Sep 2026 01:07:02 +0000 Subject: [PATCH] Share semantic document traversal helpers --- docs/internal/overview.md | 3 +- docs/roadmap/remediation.md | 9 ++++- document_walk.go | 66 +++++++++++++++++++++++++++++++++++++ load.go | 35 -------------------- load_metadata.go | 17 ---------- metadata.go | 23 +++---------- secret.go | 15 ++------- 7 files changed, 83 insertions(+), 85 deletions(-) create mode 100644 document_walk.go diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 01c6e7d..551d66e 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -8,7 +8,8 @@ module and package documentation boundary. Its implemented public primitives are - canonical, control-safe diagnostic rendering and structured `LoadError` access; - opaque file, byte, reader, and no-input source construction in `source.go`, with shared regular-file checks and bounded read mechanics in `source_file.go`; -- a parser-independent ordered tree in `internal/document`; +- a parser-independent ordered tree in `internal/document`, with root-private + semantic lookup, indexing, and value traversal in `document_walk.go`; - the sole upstream YAML integration in `internal/yaml`, which translates validated input into that owned tree and exposes no parser types outside its boundary; `parse.go` owns shared translation into detached, ordered public diagnostics; diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 214ff98..b8381af 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -163,7 +163,14 @@ reliably induce an important filesystem failure. Run race tests for this unit. ## Stage 4 — Establish shared document traversal ownership -Status: pending. +Status: complete. + +Completion: `document_walk.go` owns nil-safe preorder value walking, borrowed +indexing, and semantic lookup. Metadata shapes and projected sensitivity use the +walker with defensive path copies. Specialized origins and budget traversals +remain explicit. Full offline tests (including historical path shapes, metadata +detachment, and dynamic secrets), vet, build, and whitespace checks passed with +Go 1.26.7 on Linux/amd64. Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat semantic tree traversal in [load_metadata.go](../../load_metadata.go), diff --git a/document_walk.go b/document_walk.go new file mode 100644 index 0000000..e60f68a --- /dev/null +++ b/document_walk.go @@ -0,0 +1,66 @@ +package configkit + +import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" + +// walkDocument visits value nodes in preorder, preserving stored mapping order +// and sequence index order. Mapping keys are not separate nodes in this walk. +// The callback borrows paths and nodes; clone any mutable state it retains. +// Specialized origin, pruning, and budget traversals retain their own semantics. +func walkDocument(node *document.Node, path Path, visit func(Path, *document.Node)) { + if node == nil { + return + } + visit(path, node) + switch node.Kind { + case document.Mapping: + for _, entry := range node.Mapping { + walkDocument(entry.Value, path.Key(entry.Key), visit) + } + case document.Sequence: + for index, child := range node.Sequence { + walkDocument(child, path.Index(index), visit) + } + } +} + +// indexDocument borrows nodes from its caller-owned tree. +func indexDocument(node *document.Node, path Path, index map[string]*document.Node) { + walkDocument(node, path, func(path Path, node *document.Node) { + index[path.String()] = node + }) +} + +func documentAtPath(root *document.Node, path Path) (*document.Node, bool) { + if root == nil || !path.valid() { + return nil, false + } + current := root + for _, segment := range path.segments { + switch current.Kind { + case document.Mapping: + 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 + } + } + if !found { + return nil, false + } + case document.Sequence: + index, ok := schemaIndex(segment) + if !ok || index >= len(current.Sequence) { + return nil, false + } + current = current.Sequence[index] + default: + return nil, false + } + } + return current, true +} diff --git a/load.go b/load.go index 0e00083..7450682 100644 --- a/load.go +++ b/load.go @@ -400,41 +400,6 @@ func checkVersion(rule *VersionRule, root *document.Node) []Diagnostic { return canonicalDiagnostics(state.diagnostics) } -func documentAtPath(root *document.Node, path Path) (*document.Node, bool) { - if root == nil || !path.valid() { - return nil, false - } - current := root - for _, segment := range path.segments { - switch current.Kind { - case document.Mapping: - 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 - } - } - if !found { - return nil, false - } - case document.Sequence: - index, ok := schemaIndex(segment) - if !ok || index >= len(current.Sequence) { - return nil, false - } - current = current.Sequence[index] - default: - return nil, false - } - } - return current, true -} - func documentLocationPointer(location document.Location) *Location { if !locationKnown(location) { return nil diff --git a/load_metadata.go b/load_metadata.go index 3edebdd..4135195 100644 --- a/load_metadata.go +++ b/load_metadata.go @@ -159,23 +159,6 @@ func collectFieldInfo(node *document.Node, path Path, decoded decodedConfigurati } } -func indexDocument(node *document.Node, path Path, index map[string]*document.Node) { - if node == nil { - return - } - index[path.String()] = node - switch node.Kind { - case document.Mapping: - for _, entry := range node.Mapping { - indexDocument(entry.Value, path.Key(entry.Key), index) - } - case document.Sequence: - for position, child := range node.Sequence { - indexDocument(child, path.Index(position), index) - } - } -} - func cloneMetadata(metadata Metadata) Metadata { if metadata.fields == nil && metadata.history == nil { return Metadata{} diff --git a/metadata.go b/metadata.go index e777862..eb95624 100644 --- a/metadata.go +++ b/metadata.go @@ -85,8 +85,8 @@ func (m Metadata) hasHistoricalPath(path Path) bool { func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) { m.effectivePaths = make(map[string]Path) m.historicalPaths = make(map[string][]Path) - walkDocumentPaths(final, Path{}, func(path Path) { m.effectivePaths[path.String()] = path.clone() }) - add := func(path Path) { + walkDocument(final, Path{}, func(path Path, _ *document.Node) { m.effectivePaths[path.String()] = path.clone() }) + add := func(path Path, _ *document.Node) { pointer := path.String() for _, prior := range m.historicalPaths[pointer] { if matchesPathShape(path, prior) { @@ -95,9 +95,9 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his } m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone()) } - walkDocumentPaths(final, Path{}, add) + walkDocument(final, Path{}, add) for _, tree := range earlier { - walkDocumentPaths(tree, Path{}, add) + walkDocument(tree, Path{}, add) } for _, application := range applications { // Overrides have mapping-only prefixes; parsed tokens are keys in this layer. @@ -105,19 +105,6 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his for _, segment := range application.path.segments { prefix = prefix.Key(segment.key) } - walkDocumentPaths(application.value, prefix, add) - } -} - -func walkDocumentPaths(node *document.Node, path Path, visit func(Path)) { - if node == nil { - return - } - visit(path) - for _, entry := range node.Mapping { - walkDocumentPaths(entry.Value, path.Key(entry.Key), visit) - } - for i, child := range node.Sequence { - walkDocumentPaths(child, path.Index(i), visit) + walkDocument(application.value, prefix, add) } } diff --git a/secret.go b/secret.go index 215221b..2e9f38b 100644 --- a/secret.go +++ b/secret.go @@ -144,21 +144,10 @@ func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) { // is equal. Validators must preserve this inspection metadata as well as values. func projectedSensitivity(root *document.Node) map[string]Path { result := make(map[string]Path) - var visit func(*document.Node, Path) - visit = func(node *document.Node, path Path) { - if node == nil { - return - } + walkDocument(root, Path{}, func(path Path, node *document.Node) { if node.Sensitive { result[path.String()] = path.clone() } - for _, entry := range node.Mapping { - visit(entry.Value, path.Key(entry.Key)) - } - for i, child := range node.Sequence { - visit(child, path.Index(i)) - } - } - visit(root, Path{}) + }) return result }