diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 53ed5a6..7b823c9 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 16 are complete; follow Stage 17 through Stage 20 in order. +Status: in progress. Stages 1 through 17 are complete; follow Stage 18 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -655,7 +655,7 @@ counter, budget checks, final expansion check, and one policy index remain intac Both mapping and identified-sequence adapters still use the shared executor. Validation group I passed. -## Stage 17 — Extract Substantial Projection Handlers +## Stage 17 — Extract Substantial Projection Handlers (Complete) **Owners:** engine `projection.go`, optionally new `projection_collection.go`. @@ -678,6 +678,12 @@ Validation group I passed. original operation state. Do not refactor `clone`, `decodeValue`, or `valueBuilder.build` as an additional cleanup project. +Progress notes (2026-10-25): delegated struct, map, and sequence projection to +methods on the existing `valueOperation`. The dispatcher retains depth checks, +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 **Owner:** engine `schema.go`, optionally new `schema_fields.go`. diff --git a/internal/engine/projection.go b/internal/engine/projection.go index e759de2..730e6a3 100644 --- a/internal/engine/projection.go +++ b/internal/engine/projection.go @@ -39,89 +39,9 @@ func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor switch descriptor.kind { case descriptorStruct: - result, issue := o.node(document.Mapping, path, depth) - if issue != nil { - return nil, issue - } - result.Mapping = make([]document.Entry, 0, len(descriptor.fields)) - seen := make(map[string]bool, len(descriptor.fields)) - for _, member := range descriptor.members { - memberValue := value.Field(member.index) - if member.inline { - if isNilValue(memberValue) { - continue - } - inline, issue := o.project(memberValue, member.descriptor, path, depth) - if issue != nil { - return nil, issue - } - if inline.Kind != document.Mapping { - return nil, &projectionIssue{code: CodeSchemaUnsupportedType, path: path.clone(), message: "inline value did not project to a mapping"} - } - o.nodes-- // The flattened container is not retained in the output tree. - for _, entry := range inline.Mapping { - if seen[entry.Key] { - return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: path.Key(entry.Key), message: "inline value conflicts with a declared field"} - } - seen[entry.Key] = true - result.Mapping = append(result.Mapping, entry) - } - continue - } - childPath := path.Key(member.name) - if seen[member.name] { - return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: childPath, message: "inline value conflicts with a declared field"} - } - if issue := o.charge(childPath); issue != nil { // mapping key - return nil, issue - } - child, issue := o.project(memberValue, member.descriptor, childPath, depth+1) - if issue != nil { - return nil, issue - } - seen[member.name] = true - result.Mapping = append(result.Mapping, document.Entry{Key: member.name, Value: child}) - } - for _, entry := range result.Mapping { - field := descriptor.inlineMap - if index, ok := descriptor.fieldByName[entry.Key]; ok { - field = &descriptor.fields[index] - } - if field != nil && field.secret { - entry.Value.Sensitive = true - } - } - return result, nil + return o.projectStruct(value, descriptor, path, depth) case descriptorMap: - if value.IsNil() { - return o.node(document.Null, path, depth) - } - leave, issue := o.enter(value, path) - if issue != nil { - return nil, issue - } - defer leave() - result, issue := o.node(document.Mapping, path, depth) - if issue != nil { - return nil, issue - } - result.Mapping = make([]document.Entry, 0, value.Len()) - keys := value.MapKeys() - slices.SortFunc(keys, func(left, right reflect.Value) int { - return cmp.Compare(left.String(), right.String()) - }) - for _, key := range keys { - childPath := path.Key(key.String()) - if issue := o.charge(childPath); issue != nil { - return nil, issue - } - child, issue := o.project(value.MapIndex(key), descriptor.element, childPath, depth+1) - if issue != nil { - return nil, issue - } - result.Mapping = append(result.Mapping, document.Entry{Key: key.String(), Value: child}) - } - return result, nil + return o.projectMap(value, descriptor, path, depth) case descriptorSlice: if value.IsNil() { return o.node(document.Null, path, depth) @@ -131,22 +51,9 @@ func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor return nil, issue } defer leave() - fallthrough + return o.projectSequence(value, descriptor, path, depth) case descriptorArray: - result, issue := o.node(document.Sequence, path, depth) - if issue != nil { - return nil, issue - } - result.Sequence = make([]*document.Node, 0, value.Len()) - for i := 0; i < value.Len(); i++ { - childPath := path.Index(i) - child, issue := o.project(value.Index(i), descriptor.element, childPath, depth+1) - if issue != nil { - return nil, issue - } - result.Sequence = append(result.Sequence, child) - } - return result, nil + return o.projectSequence(value, descriptor, path, depth) case descriptorDynamic: if value.IsNil() { return o.node(document.Null, path, depth) @@ -217,6 +124,111 @@ func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor } } +func (o *valueOperation) projectStruct(value reflect.Value, descriptor *typeDescriptor, path Path, depth int) (*document.Node, *projectionIssue) { + result, issue := o.node(document.Mapping, path, depth) + if issue != nil { + return nil, issue + } + result.Mapping = make([]document.Entry, 0, len(descriptor.fields)) + seen := make(map[string]bool, len(descriptor.fields)) + for _, member := range descriptor.members { + memberValue := value.Field(member.index) + if member.inline { + if isNilValue(memberValue) { + continue + } + inline, issue := o.project(memberValue, member.descriptor, path, depth) + if issue != nil { + return nil, issue + } + if inline.Kind != document.Mapping { + return nil, &projectionIssue{code: CodeSchemaUnsupportedType, path: path.clone(), message: "inline value did not project to a mapping"} + } + o.nodes-- // The flattened container is not retained in the output tree. + for _, entry := range inline.Mapping { + if seen[entry.Key] { + return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: path.Key(entry.Key), message: "inline value conflicts with a declared field"} + } + seen[entry.Key] = true + result.Mapping = append(result.Mapping, entry) + } + continue + } + childPath := path.Key(member.name) + if seen[member.name] { + return nil, &projectionIssue{code: CodeSchemaAmbiguousField, path: childPath, message: "inline value conflicts with a declared field"} + } + if issue := o.charge(childPath); issue != nil { // mapping key + return nil, issue + } + child, issue := o.project(memberValue, member.descriptor, childPath, depth+1) + if issue != nil { + return nil, issue + } + seen[member.name] = true + result.Mapping = append(result.Mapping, document.Entry{Key: member.name, Value: child}) + } + for _, entry := range result.Mapping { + field := descriptor.inlineMap + if index, ok := descriptor.fieldByName[entry.Key]; ok { + field = &descriptor.fields[index] + } + if field != nil && field.secret { + entry.Value.Sensitive = true + } + } + return result, nil +} + +func (o *valueOperation) projectMap(value reflect.Value, descriptor *typeDescriptor, path Path, depth int) (*document.Node, *projectionIssue) { + if value.IsNil() { + return o.node(document.Null, path, depth) + } + leave, issue := o.enter(value, path) + if issue != nil { + return nil, issue + } + defer leave() + result, issue := o.node(document.Mapping, path, depth) + if issue != nil { + return nil, issue + } + result.Mapping = make([]document.Entry, 0, value.Len()) + keys := value.MapKeys() + slices.SortFunc(keys, func(left, right reflect.Value) int { + return cmp.Compare(left.String(), right.String()) + }) + for _, key := range keys { + childPath := path.Key(key.String()) + if issue := o.charge(childPath); issue != nil { + return nil, issue + } + child, issue := o.project(value.MapIndex(key), descriptor.element, childPath, depth+1) + if issue != nil { + return nil, issue + } + result.Mapping = append(result.Mapping, document.Entry{Key: key.String(), Value: child}) + } + return result, nil +} + +func (o *valueOperation) projectSequence(value reflect.Value, descriptor *typeDescriptor, path Path, depth int) (*document.Node, *projectionIssue) { + result, issue := o.node(document.Sequence, path, depth) + if issue != nil { + return nil, issue + } + result.Sequence = make([]*document.Node, 0, value.Len()) + for index := 0; index < value.Len(); index++ { + childPath := path.Index(index) + child, issue := o.project(value.Index(index), descriptor.element, childPath, depth+1) + if issue != nil { + return nil, issue + } + result.Sequence = append(result.Sequence, child) + } + return result, nil +} + func (o *valueOperation) node(kind document.Kind, path Path, depth int) (*document.Node, *projectionIssue) { if issue := o.checkDepth(path, depth); issue != nil { return nil, issue