From 4275bdf35582f3aaa44241f16302c4c9caf60809 Mon Sep 17 00:00:00 2001 From: Brandon Harvey Date: Sat, 28 Feb 2026 09:03:02 -0800 Subject: [PATCH] fix(plan): avoid impossible top-level field suggestions Constrain `ergo plan` unknown-field suggestions to top-level schema keys only.\nThis prevents suggesting `after` for unknown top-level keys, which is misleading because `after` is task-scoped only.\nRename the candidate list to clarify intent and preserve existing parser behavior.\nAdd a focused regression test for top-level typo input (`aftr`) to ensure no nested-only suggestion leaks through.\nKeep existing behavior for valid top-level suggestions (like `boddy` -> `body`).\nNo CLI contract changes beyond clearer parse_error messaging for this edge case.\nThis keeps feedback aligned with quickstart/spec input shape and reduces correction loops for agents.\nRisk is low: change is scoped to suggestion candidate selection for plan parsing only.\nTests: go test ./... --- internal/ergo/plan_input.go | 5 ++--- internal/ergo/plan_input_test.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/ergo/plan_input.go b/internal/ergo/plan_input.go index 4b18564..1334f40 100644 --- a/internal/ergo/plan_input.go +++ b/internal/ergo/plan_input.go @@ -14,11 +14,10 @@ import ( "strings" ) -var knownPlanJSONFields = []string{ +var knownPlanTopLevelJSONFields = []string{ "title", "body", "tasks", - "after", } // PlanInput is the JSON schema for `ergo plan`. @@ -61,7 +60,7 @@ func ParsePlanInput() (*PlanInput, *ValidationError) { invalid := map[string]string{ unknownField: "unknown field", } - if suggestion, ok := suggestFieldNameFrom(unknownField, knownPlanJSONFields); ok { + if suggestion, ok := suggestFieldNameFrom(unknownField, knownPlanTopLevelJSONFields); ok { message = fmt.Sprintf("invalid JSON: unknown field %q (did you mean: %s?)", unknownField, suggestion) invalid[unknownField] = fmt.Sprintf("unknown field (did you mean: %s?)", suggestion) } diff --git a/internal/ergo/plan_input_test.go b/internal/ergo/plan_input_test.go index c538fbc..6731d11 100644 --- a/internal/ergo/plan_input_test.go +++ b/internal/ergo/plan_input_test.go @@ -45,6 +45,22 @@ func TestParsePlanInput_RejectsUnknownNestedField_WithSuggestion(t *testing.T) { } } +func TestParsePlanInput_UnknownTopLevelDoesNotSuggestNestedField(t *testing.T) { + restoreStdin := setStdin(t, `{"title":"Epic","tasks":[{"title":"A"}],"aftr":"x"}`) + defer restoreStdin() + + _, err := ParsePlanInput() + if err == nil { + t.Fatal("expected parse error for unknown field, got nil") + } + if err.Error != "parse_error" { + t.Fatalf("expected parse_error, got %q", err.Error) + } + if strings.Contains(err.Message, "did you mean: after") { + t.Fatalf("expected no suggestion for nested-only field, got %q", err.Message) + } +} + func TestParsePlanInput_RejectsMultipleJSONValues(t *testing.T) { restoreStdin := setStdin(t, `{"title":"Epic","tasks":[{"title":"A"}]}{"title":"Extra"}`) defer restoreStdin()