Prepare reusable CLI override batches
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Sequence Identity And Combination Implementation Plan
|
# Sequence Identity And Combination Implementation Plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–22 complete.
|
Status: implementation in progress; Stages 1–23 complete.
|
||||||
|
|
||||||
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
Audience: a Luna coding agent implementing one numbered stage per prompt, in
|
||||||
order. This plan starts at commit
|
order. This plan starts at commit
|
||||||
@@ -786,6 +786,11 @@ validation entry, without changing current CLI processing order.
|
|||||||
Validation: override preparation/source tests and the everyday check. Keep
|
Validation: override preparation/source tests and the everyday check. Keep
|
||||||
ordinary/layered behavior unchanged; typed assignment is the next stage.
|
ordinary/layered behavior unchanged; typed assignment is the next stage.
|
||||||
|
|
||||||
|
**Completion:** Extracted detached override preparation batches with stable
|
||||||
|
synthetic source ordering and added a privately exercised post-structural
|
||||||
|
directive check for `extends`. Existing early override behavior remains active.
|
||||||
|
Verified focused override tests and `./scripts/check.sh`.
|
||||||
|
|
||||||
## Stage 24 — Implement Schema-Aware Post-Decode CLI Assignment
|
## Stage 24 — Implement Schema-Aware Post-Decode CLI Assignment
|
||||||
|
|
||||||
Scope: a private typed assignment helper using Stage 23's prepared overrides,
|
Scope: a private typed assignment helper using Stage 23's prepared overrides,
|
||||||
|
|||||||
@@ -19,9 +19,40 @@ type preparedOverride struct {
|
|||||||
nodes int
|
nodes int
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) ([]preparedOverride, []SourceInfo, []Diagnostic) {
|
// preparedOverrideBatch owns all override values and synthetic source records
|
||||||
|
// needed to apply one CLI batch after its preparation checks succeed.
|
||||||
|
type preparedOverrideBatch struct {
|
||||||
|
overrides []preparedOverride
|
||||||
|
sources []SourceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type overridePreparationInput struct {
|
||||||
|
root *document.Node
|
||||||
|
inputBytes int64
|
||||||
|
inputNodes int
|
||||||
|
}
|
||||||
|
|
||||||
|
func overridePreparationInputFor(base *composedDocument) overridePreparationInput {
|
||||||
|
if base == nil {
|
||||||
|
return overridePreparationInput{}
|
||||||
|
}
|
||||||
|
return overridePreparationInput{root: base.root, inputBytes: base.inputBytes, inputNodes: base.inputNodes}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) (preparedOverrideBatch, []Diagnostic) {
|
||||||
|
return prepareOverrideBatch(overridePreparationInputFor(base), sources, overrides, rule, limits, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// preparePostStructuralOverrides prepares the same detached batch for the
|
||||||
|
// lifecycle after structural expansion, where record inheritance directives
|
||||||
|
// are no longer valid CLI input.
|
||||||
|
func preparePostStructuralOverrides(base overridePreparationInput, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits) (preparedOverrideBatch, []Diagnostic) {
|
||||||
|
return prepareOverrideBatch(base, sources, overrides, rule, limits, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareOverrideBatch(base overridePreparationInput, sources []SourceInfo, overrides []Override, rule *VersionRule, limits importLimits, rejectInheritanceDirectives bool) (preparedOverrideBatch, []Diagnostic) {
|
||||||
if len(overrides) > maxOverrides || len(sources)+len(overrides) > limits.sources {
|
if len(overrides) > maxOverrides || len(sources)+len(overrides) > limits.sources {
|
||||||
return nil, cloneSourceInfos(sources), []Diagnostic{{
|
return preparedOverrideBatch{sources: cloneSourceInfos(sources)}, []Diagnostic{{
|
||||||
Code: CodeOverrideLimitExceeded, Severity: SeverityError,
|
Code: CodeOverrideLimitExceeded, Severity: SeverityError,
|
||||||
Message: "configuration overrides exceed the record or source-count limit",
|
Message: "configuration overrides exceed the record or source-count limit",
|
||||||
}}
|
}}
|
||||||
@@ -37,7 +68,7 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, cloneSourceInfos(sources), canonicalDiagnostics(diagnostics)
|
return preparedOverrideBatch{sources: cloneSourceInfos(sources)}, canonicalDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared := make([]preparedOverride, len(overrides))
|
prepared := make([]preparedOverride, len(overrides))
|
||||||
@@ -89,7 +120,7 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if pathContainsDirective(override.Path) {
|
if pathContainsDirective(override.Path) || (rejectInheritanceDirectives && pathContainsInheritanceDirective(override.Path)) {
|
||||||
diagnostics = append(diagnostics, Diagnostic{
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
Code: CodeOverrideDirective, Severity: SeverityError, Path: override.Path.clone(),
|
Code: CodeOverrideDirective, Severity: SeverityError, Path: override.Path.clone(),
|
||||||
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
||||||
@@ -106,7 +137,11 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
|
|||||||
diagnostics = append(diagnostics, diagnostic)
|
diagnostics = append(diagnostics, diagnostic)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if relative, ok := overrideDirectivePath(value, Path{}); ok {
|
findDirective := overrideDirectivePath
|
||||||
|
if rejectInheritanceDirectives {
|
||||||
|
findDirective = overrideStructuralDirectivePath
|
||||||
|
}
|
||||||
|
if relative, ok := findDirective(value, Path{}); ok {
|
||||||
diagnostics = append(diagnostics, Diagnostic{
|
diagnostics = append(diagnostics, Diagnostic{
|
||||||
Code: CodeOverrideDirective, Severity: SeverityError, Path: appendOverridePath(override.Path, relative),
|
Code: CodeOverrideDirective, Severity: SeverityError, Path: appendOverridePath(override.Path, relative),
|
||||||
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
Message: "configuration overrides cannot create configuration directives", Primary: location,
|
||||||
@@ -136,10 +171,10 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
|
|||||||
usedNodes += nodes
|
usedNodes += nodes
|
||||||
}
|
}
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, allSources, canonicalDiagnostics(diagnostics)
|
return preparedOverrideBatch{sources: allSources}, canonicalDiagnostics(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prepared, allSources, nil
|
return preparedOverrideBatch{overrides: prepared, sources: allSources}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validOverrideLabel(label string) bool {
|
func validOverrideLabel(label string) bool {
|
||||||
@@ -191,6 +226,15 @@ func pathContainsDirective(path Path) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pathContainsInheritanceDirective(path Path) bool {
|
||||||
|
for _, segment := range path.segments {
|
||||||
|
if segment.key == inheritanceKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func appendOverridePath(base, relative Path) Path {
|
func appendOverridePath(base, relative Path) Path {
|
||||||
result := base.clone()
|
result := base.clone()
|
||||||
result.segments = append(result.segments, clonePathSegments(relative.segments, 0)...)
|
result.segments = append(result.segments, clonePathSegments(relative.segments, 0)...)
|
||||||
@@ -198,19 +242,27 @@ func appendOverridePath(base, relative Path) Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func overrideDirectivePath(node *document.Node, path Path) (Path, bool) {
|
func overrideDirectivePath(node *document.Node, path Path) (Path, bool) {
|
||||||
|
return overrideDirectivePathWithInheritance(node, path, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideStructuralDirectivePath(node *document.Node, path Path) (Path, bool) {
|
||||||
|
return overrideDirectivePathWithInheritance(node, path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideDirectivePathWithInheritance(node *document.Node, path Path, rejectInheritanceDirectives bool) (Path, bool) {
|
||||||
if node.Kind == document.Mapping {
|
if node.Kind == document.Mapping {
|
||||||
for _, entry := range node.Mapping {
|
for _, entry := range node.Mapping {
|
||||||
child := path.Key(entry.Key)
|
child := path.Key(entry.Key)
|
||||||
if entry.Key == directiveKey {
|
if entry.Key == directiveKey || (rejectInheritanceDirectives && entry.Key == inheritanceKey) {
|
||||||
return child, true
|
return child, true
|
||||||
}
|
}
|
||||||
if found, ok := overrideDirectivePath(entry.Value, child); ok {
|
if found, ok := overrideDirectivePathWithInheritance(entry.Value, child, rejectInheritanceDirectives); ok {
|
||||||
return found, true
|
return found, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if node.Kind == document.Sequence {
|
} else if node.Kind == document.Sequence {
|
||||||
for index, child := range node.Sequence {
|
for index, child := range node.Sequence {
|
||||||
if found, ok := overrideDirectivePath(child, path.Index(index)); ok {
|
if found, ok := overrideDirectivePathWithInheritance(child, path.Index(index), rejectInheritanceDirectives); ok {
|
||||||
return found, true
|
return found, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ func (a *assemblyAccumulator) applyOverrides(sources []SourceInfo, overrides []O
|
|||||||
if len(overrides) == 0 {
|
if len(overrides) == 0 {
|
||||||
return cloneSourceInfos(sources), nil
|
return cloneSourceInfos(sources), nil
|
||||||
}
|
}
|
||||||
prepared, allSources, diagnostics := prepareOverrides(a.current, sources, overrides, rule, limits)
|
batch, diagnostics := prepareOverrides(a.current, sources, overrides, rule, limits)
|
||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return allSources, diagnostics
|
return batch.sources, diagnostics
|
||||||
}
|
}
|
||||||
for _, override := range prepared {
|
for _, override := range batch.overrides {
|
||||||
applyPreparedOverride(a.current, override, &a.index)
|
applyPreparedOverride(a.current, override, &a.index)
|
||||||
a.current.inputBytes += override.bytes
|
a.current.inputBytes += override.bytes
|
||||||
a.current.inputNodes += override.nodes
|
a.current.inputNodes += override.nodes
|
||||||
}
|
}
|
||||||
return allSources, nil
|
return batch.sources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyPreparedOverride(result *composedDocument, override preparedOverride, index *mappingIndex) {
|
func applyPreparedOverride(result *composedDocument, override preparedOverride, index *mappingIndex) {
|
||||||
|
|||||||
@@ -446,6 +446,85 @@ func TestOverrideAggregateLimitsFailBeforeApplication(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPreparedOverrideBatchDetachesInputsAndAllocatesSourcesInOrder(t *testing.T) {
|
||||||
|
base, diagnostics := composeDocuments(parseCompositionDocument(t, "base", "settings: {}\n"))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
sources := []SourceInfo{{ID: "source-1", Label: "base", Layer: LayerBase, Kind: SourceKindMapping}}
|
||||||
|
firstValue := map[string]any{"nested": map[string]any{"name": "original"}}
|
||||||
|
secondValue := []any{map[string]any{"enabled": true}}
|
||||||
|
batch, diagnostics := prepareOverrides(base, sources, []Override{
|
||||||
|
{Path: Keys("settings", "first"), Value: firstValue, Flag: "--first"},
|
||||||
|
{Path: Keys("settings", "second"), Value: secondValue, Flag: "--second"},
|
||||||
|
}, nil, defaultImportLimits())
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("prepare overrides: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
if len(batch.overrides) != 2 || len(batch.sources) != 3 {
|
||||||
|
t.Fatalf("prepared counts = %d overrides, %d sources", len(batch.overrides), len(batch.sources))
|
||||||
|
}
|
||||||
|
for index, want := range []string{"source-2", "source-3"} {
|
||||||
|
if batch.overrides[index].source.ID != want || batch.sources[index+1].ID != want {
|
||||||
|
t.Errorf("prepared source %d = %#v / %#v, want %s", index, batch.overrides[index].source, batch.sources[index+1], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if batch.sources[1].Label != "--first" || batch.sources[2].Label != "--second" {
|
||||||
|
t.Fatalf("synthetic source order = %#v", batch.sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstValue["nested"].(map[string]any)["name"] = "changed after preparation"
|
||||||
|
preparedName, found := documentAtPath(batch.overrides[0].value, Keys("nested", "name"))
|
||||||
|
if !found || preparedName.Text != "original" {
|
||||||
|
t.Fatalf("prepared override aliases input value: %#v, %v", preparedName, found)
|
||||||
|
}
|
||||||
|
preparedName.Text = "changed prepared snapshot"
|
||||||
|
if firstValue["nested"].(map[string]any)["name"] != "changed after preparation" {
|
||||||
|
t.Fatal("prepared override mutation changed caller input")
|
||||||
|
}
|
||||||
|
batch.sources[0].Label = "changed batch source"
|
||||||
|
if sources[0].Label != "base" {
|
||||||
|
t.Fatal("prepared source list aliases the input source list")
|
||||||
|
}
|
||||||
|
if len(base.root.Mapping) != 1 || base.root.Mapping[0].Value.Kind != document.Mapping || len(base.root.Mapping[0].Value.Mapping) != 0 {
|
||||||
|
t.Fatalf("preparation mutated assembled input: %#v", base.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostStructuralOverridePreparationRejectsInheritanceDirectives(t *testing.T) {
|
||||||
|
base, diagnostics := composeDocuments(parseCompositionDocument(t, "base", "child: {}\nitems: []\n"))
|
||||||
|
if len(diagnostics) != 0 {
|
||||||
|
t.Fatal(diagnostics)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
override Override
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "supplied path",
|
||||||
|
override: Override{Path: Keys("child", "extends"), Value: "parent", Flag: "--extends"},
|
||||||
|
path: "/child/extends",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested value",
|
||||||
|
override: Override{Path: Keys("items"), Value: []any{map[string]any{"extends": "parent"}}, Flag: "--items"},
|
||||||
|
path: "/items/0/extends",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if _, diagnostics := prepareOverrides(base, nil, []Override{test.override}, nil, defaultImportLimits()); len(diagnostics) != 0 {
|
||||||
|
t.Fatalf("legacy preparation changed before lifecycle activation: %#v", diagnostics)
|
||||||
|
}
|
||||||
|
_, diagnostics := preparePostStructuralOverrides(overridePreparationInputFor(base), nil, []Override{test.override}, nil, defaultImportLimits())
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeOverrideDirective || diagnostics[0].Path.String() != test.path {
|
||||||
|
t.Fatalf("post-structural diagnostics = %#v, want directive at %s", diagnostics, test.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func historyLayers(history []Change) []Layer {
|
func historyLayers(history []Change) []Layer {
|
||||||
layers := make([]Layer, len(history))
|
layers := make([]Layer, len(history))
|
||||||
for index, change := range history {
|
for index, change := range history {
|
||||||
|
|||||||
Reference in New Issue
Block a user