From 0fbce5d1418300619225fab358eddee9ed099afc Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 25 Oct 2026 13:46:51 +0000 Subject: [PATCH] Separate schema field discovery and assembly --- docs/roadmap/implementation.md | 10 ++- internal/engine/schema.go | 71 +-------------------- internal/engine/schema_fields.go | 102 +++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 70 deletions(-) create mode 100644 internal/engine/schema_fields.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7b823c9..3108ab0 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 17 are complete; follow Stage 18 through Stage 20 in order. +Status: in progress. Stages 1 through 18 are complete; follow Stage 19 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -684,7 +684,7 @@ pointer/dynamic handling, and scalar conversion; struct order, map sorting, flattened-node charging, nil slices, and cycle guards remain in their original paths. Validation group R passed. -## Stage 18 — Separate Schema Field Discovery And Assembly +## Stage 18 — Separate Schema Field Discovery And Assembly (Complete) **Owner:** engine `schema.go`, optionally new `schema_fields.go`. @@ -701,6 +701,12 @@ paths. Validation group R passed. Do not unify these views or add alternate field/tag policy. Existing schema, projection, decoding, and cloning expectations remain the behavior baseline. +Progress notes (2026-10-25): kept declaration-order enumeration, exported and +ignored-field decisions, and tag parsing in `describeFields`; moved ordinary +registration, inline pointer inspection, and inline struct/map assembly into +focused helpers in `schema_fields.go`. Physical members remain distinct from +flattened fields and inline-map metadata. Validation group R passed. + **Finish:** field discovery and descriptor assembly have explicit responsibilities without a new registration framework. Run R for both Stages 17–18. diff --git a/internal/engine/schema.go b/internal/engine/schema.go index d9bdc84..acffcd5 100644 --- a/internal/engine/schema.go +++ b/internal/engine/schema.go @@ -241,80 +241,15 @@ func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current refle if name != "" { return &schemaIssue{code: CodeSchemaTag, path: path.clone(), message: "inline field cannot declare a YAML name"} } - inlineType := field.Type - seenPointers := make(map[reflect.Type]bool) - for inlineType.Kind() == reflect.Pointer { - if seenPointers[inlineType] { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"} - } - seenPointers[inlineType] = true - inlineType = inlineType.Elem() - } - if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"} - } - switch inlineType.Kind() { - case reflect.Struct: - if b.building[inlineType] { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "recursive inline struct is not supported"} - } - child, childIssue := b.describe(inlineType, path) - if childIssue != nil { - return childIssue - } - if child.kind != descriptorStruct { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a structural struct or string-keyed map"} - } - for _, nested := range child.fields { - nested.index = prependIndex(i, nested.index) - nested.secret = secret || nested.secret - if issue := addField(descriptor, nested, path); issue != nil { - return issue - } - } - if child.inlineMap != nil { - nested := *child.inlineMap - nested.index = prependIndex(i, nested.index) - nested.secret = secret || nested.secret - if descriptor.inlineMap != nil { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"} - } - descriptor.inlineMap = &nested - } - memberType, memberIssue := b.describe(field.Type, path) - if memberIssue != nil { - return memberIssue - } - descriptor.members = append(descriptor.members, structMember{index: i, descriptor: memberType, inline: true}) - case reflect.Map: - if inlineType.Key().Kind() != reflect.String { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline map must have string keys"} - } - if descriptor.inlineMap != nil { - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"} - } - fieldPath := path.Key("*") - fieldType, childIssue := b.describe(inlineType, fieldPath) - if childIssue != nil { - return childIssue - } - descriptor.inlineMap = &fieldDescriptor{index: []int{i}, descriptor: fieldType, secret: secret, inlineMap: true} - descriptor.members = append(descriptor.members, structMember{index: i, descriptor: fieldType, inline: true}) - default: - return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a struct, pointer to struct, or string-keyed map"} + if issue := b.describeInlineField(descriptor, field, i, secret, path); issue != nil { + return issue } continue } - fieldPath := path.Key(name) - fieldType, childIssue := b.describe(field.Type, fieldPath) - if childIssue != nil { - return childIssue - } - if issue := addField(descriptor, fieldDescriptor{name: name, index: []int{i}, descriptor: fieldType, secret: secret}, path); issue != nil { + if issue := b.describeOrdinaryField(descriptor, field, i, name, secret, path); issue != nil { return issue } - descriptor.members = append(descriptor.members, structMember{index: i, name: name, descriptor: fieldType}) } return nil } diff --git a/internal/engine/schema_fields.go b/internal/engine/schema_fields.go new file mode 100644 index 0000000..148977b --- /dev/null +++ b/internal/engine/schema_fields.go @@ -0,0 +1,102 @@ +package engine + +import "reflect" + +func (b *schemaBuilder) describeOrdinaryField(owner *typeDescriptor, field reflect.StructField, index int, name string, secret bool, path Path) *schemaIssue { + fieldPath := path.Key(name) + fieldType, issue := b.describe(field.Type, fieldPath) + if issue != nil { + return issue + } + if issue := addField(owner, fieldDescriptor{name: name, index: []int{index}, descriptor: fieldType, secret: secret}, path); issue != nil { + return issue + } + owner.members = append(owner.members, structMember{index: index, name: name, descriptor: fieldType}) + return nil +} + +func (b *schemaBuilder) describeInlineField(owner *typeDescriptor, field reflect.StructField, index int, secret bool, path Path) *schemaIssue { + inlineType, issue := inlineFieldType(field.Type, path) + if issue != nil { + return issue + } + if inlineType.Kind() == reflect.Map && field.Type.Kind() == reflect.Pointer { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline maps cannot be pointers"} + } + + switch inlineType.Kind() { + case reflect.Struct: + return b.describeInlineStruct(owner, field, inlineType, index, secret, path) + case reflect.Map: + return b.describeInlineMap(owner, inlineType, index, secret, path) + default: + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a struct, pointer to struct, or string-keyed map"} + } +} + +func inlineFieldType(fieldType reflect.Type, path Path) (reflect.Type, *schemaIssue) { + current := fieldType + seenPointers := make(map[reflect.Type]bool) + for current.Kind() == reflect.Pointer { + if seenPointers[current] { + return nil, &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline pointer chain must terminate in a struct"} + } + seenPointers[current] = true + current = current.Elem() + } + return current, nil +} + +func (b *schemaBuilder) describeInlineStruct(owner *typeDescriptor, field reflect.StructField, inlineType reflect.Type, index int, secret bool, path Path) *schemaIssue { + if b.building[inlineType] { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "recursive inline struct is not supported"} + } + child, issue := b.describe(inlineType, path) + if issue != nil { + return issue + } + if child.kind != descriptorStruct { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline field must be a structural struct or string-keyed map"} + } + for _, nested := range child.fields { + nested.index = prependIndex(index, nested.index) + nested.secret = secret || nested.secret + if issue := addField(owner, nested, path); issue != nil { + return issue + } + } + if child.inlineMap != nil { + nested := *child.inlineMap + nested.index = prependIndex(index, nested.index) + nested.secret = secret || nested.secret + if owner.inlineMap != nil { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"} + } + owner.inlineMap = &nested + } + + memberType, issue := b.describe(field.Type, path) + if issue != nil { + return issue + } + owner.members = append(owner.members, structMember{index: index, descriptor: memberType, inline: true}) + return nil +} + +func (b *schemaBuilder) describeInlineMap(owner *typeDescriptor, inlineType reflect.Type, index int, secret bool, path Path) *schemaIssue { + if inlineType.Key().Kind() != reflect.String { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "inline map must have string keys"} + } + if owner.inlineMap != nil { + return &schemaIssue{code: CodeSchemaInline, path: path.clone(), message: "configuration object has more than one inline map"} + } + + fieldPath := path.Key("*") + fieldType, issue := b.describe(inlineType, fieldPath) + if issue != nil { + return issue + } + owner.inlineMap = &fieldDescriptor{index: []int{index}, descriptor: fieldType, secret: secret, inlineMap: true} + owner.members = append(owner.members, structMember{index: index, descriptor: fieldType, inline: true}) + return nil +}