From 5dcfc2e953ff69f1d9df5898502fae97eb7a4eea Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 5 Nov 2026 14:03:45 +0000 Subject: [PATCH] Merge condition evidence without repeated copies --- docs/roadmap/implementation.md | 6 ++++++ internal/prompt/requirements.go | 31 +++++++++++++++------------- internal/prompt/requirements_test.go | 29 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 96750c2..c97f71f 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -444,6 +444,12 @@ rendering comparisons. Source work counts or focused measurements demonstrate removal of the identified growing-copy pattern; no grammar or expression limit changes, universal complexity promise, or allocation-count assertion is added. +Completion: owned condition sets merge smaller into larger without retaining +operand snapshots. For the documented distinct left-heavy cases, source work is +3, 15, and 127 insertions at N=4, 16, and 128, respectively, excluding leaf +creation (previously 9, 135, and 8,255). Prompt requirements tests pass on Go +1.27.3; these counts are not a general complexity guarantee. + ## Stage 17: Share Requirement Branch Orchestration **Catalog:** 23. **Files:** internal prompt requirement traversal/evaluation. diff --git a/internal/prompt/requirements.go b/internal/prompt/requirements.go index 1167c85..a6373a0 100644 --- a/internal/prompt/requirements.go +++ b/internal/prompt/requirements.go @@ -319,24 +319,27 @@ func mergeConditionEvaluations(left, right conditionEvaluation) conditionEvaluat result := conditionEvaluation{ unsupported: left.unsupported || right.unsupported, inputNameUnknown: left.inputNameUnknown || right.inputNameUnknown, - missingVariables: make(map[string]struct{}, len(left.missingVariables)+len(right.missingVariables)), - contentInputs: make(map[string]struct{}, len(left.contentInputs)+len(right.contentInputs)), - } - for name := range left.missingVariables { - result.missingVariables[name] = struct{}{} - } - for name := range right.missingVariables { - result.missingVariables[name] = struct{}{} - } - for name := range left.contentInputs { - result.contentInputs[name] = struct{}{} - } - for name := range right.contentInputs { - result.contentInputs[name] = struct{}{} + missingVariables: mergeOwnedNames(left.missingVariables, right.missingVariables), + contentInputs: mergeOwnedNames(left.contentInputs, right.contentInputs), } return result } +// Condition results own their sets; callers retain operand values and flags, +// not independent snapshots of these maps after a merge. +func mergeOwnedNames(left, right map[string]struct{}) map[string]struct{} { + if len(left) < len(right) { + left, right = right, left + } + if left == nil { + return right + } + for name := range right { + left[name] = struct{}{} + } + return left +} + func branchReach(parent requirementReachability, known, truth, selected bool) requirementReachability { if parent == requirementInactive { return requirementInactive diff --git a/internal/prompt/requirements_test.go b/internal/prompt/requirements_test.go index d26100c..737a1da 100644 --- a/internal/prompt/requirements_test.go +++ b/internal/prompt/requirements_test.go @@ -406,6 +406,35 @@ func TestAnalyzeRequirementsReportsUnresolvedDecisionAndContentConditions(t *tes } } +func TestConditionEvaluationMergesOwnedEvidence(t *testing.T) { + left := conditionEvaluation{missingVariables: map[string]struct{}{"a": {}, "b": {}}, contentInputs: map[string]struct{}{"document": {}}, unsupported: true} + right := conditionEvaluation{missingVariables: map[string]struct{}{"b": {}, "c": {}}, inputNameUnknown: true} + merged := mergeConditionEvaluations(left, right) + if !merged.unsupported || !merged.inputNameUnknown || len(merged.missingVariables) != 3 || len(merged.contentInputs) != 1 { + t.Fatalf("merged evidence = %+v", merged) + } + transferred := mergeConditionEvaluations(conditionEvaluation{}, conditionEvaluation{missingVariables: map[string]struct{}{"only": {}}}) + if len(transferred.missingVariables) != 1 || transferred.contentInputs != nil { + t.Fatalf("transferred evidence = %+v", transferred) + } +} + +func TestAnalyzeRequirementsRetainsLeftHeavyMissingConditions(t *testing.T) { + compiled := compileRequirementTemplate(t, "nested", `{{if eq (eq (eq .a .b) .c) .d}}{{input "yes"}}{{else}}{{input "no"}}{{end}}`) + result, err := AnalyzeRequirements(context.Background(), &domain.PromptDefinition{}, RequirementSnapshot{Templates: CompiledTemplates{Messages: []*template.Template{compiled}}}, nil, false) + if err != nil { + t.Fatal(err) + } + if result.Complete || len(result.Variables) != 4 || len(result.Inputs) != 2 { + t.Fatalf("requirements = %+v", result) + } + for i, name := range []string{"a", "b", "c", "d"} { + if result.Variables[i].Name != name { + t.Fatalf("variables = %+v", result.Variables) + } + } +} + func TestAnalyzeRequirementsRetainsInactiveEntriesAndPromotesActiveUses(t *testing.T) { description := "declared for a reason" definition := &domain.PromptDefinition{Templates: []domain.PromptMessageTemplate{{