{"record":{"id":"a9251ff17bae2c7b","repo":"gastownhall/beads","slug":"loop-q-max-must-be-positive","errorCode":null,"errorMessage":"loop %q: max must be positive","messagePattern":"loop %q: max must be positive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":90,"sourceCode":"\t}\n\n\tif loopTypes == 0 {\n\t\treturn fmt.Errorf(\"loop %q: one of count, until, or range is required\", stepID)\n\t}\n\tif loopTypes > 1 {\n\t\treturn fmt.Errorf(\"loop %q: only one of count, until, or range can be specified\", stepID)\n\t}\n\n\tif loop.Until != \"\" && loop.Max == 0 {\n\t\treturn fmt.Errorf(\"loop %q: max is required when until is set\", stepID)\n\t}\n\n\tif loop.Count < 0 {\n\t\treturn fmt.Errorf(\"loop %q: count must be positive\", stepID)\n\t}\n\n\tif loop.Max < 0 {\n\t\treturn fmt.Errorf(\"loop %q: max must be positive\", stepID)\n\t}\n\n\t// Validate until condition syntax if present\n\tif loop.Until != \"\" {\n\t\tif _, err := ParseCondition(loop.Until); err != nil {\n\t\t\treturn fmt.Errorf(\"loop %q: invalid until condition %q: %w\", stepID, loop.Until, err)\n\t\t}\n\t}\n\n\t// Validate range syntax if present\n\tif loop.Range != \"\" {\n\t\tif err := ValidateRange(loop.Range); err != nil {\n\t\t\treturn fmt.Errorf(\"loop %q: invalid range %q: %w\", stepID, loop.Range, err)\n\t\t}\n\t}\n\n\treturn nil\n}","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L72-L108","documentation":"validateLoopSpec rejects a LoopSpec whose Max field is negative when a formula using loops is expanded (via ApplyLoops). Max caps the number of loop iterations, so a negative value is meaningless. Note the check is `loop.Max < 0`, so zero actually passes validation despite the 'must be positive' wording.","triggerScenarios":"Calling ApplyLoops (directly or via formula Apply/expand) on a Step whose Loop.Max is set to a negative number, typically from a YAML/formula file where max was typoed or computed from a negative expression.","commonSituations":"Hand-written formula YAML with `max: -1` intended as 'unlimited'; a max value computed from a variable that resolved negative; copy-paste of count/max config with a stray minus sign.","solutions":["Fix the formula definition so loop.max is zero or a positive integer","If max should mean 'unlimited', omit the field or use 0, since only negative values are rejected","Check whether max is computed from a variable and verify that variable's resolved value"],"exampleFix":"# before\nloop:\n  count: 3\n  max: -1\n# after\nloop:\n  count: 3\n  max: 10","handlingStrategy":"validation","validationCode":"func validateLoopMax(loop *LoopSpec) error {\n\tif loop != nil && loop.Max < 0 {\n\t\treturn fmt.Errorf(\"loop.max must be >= 0, got %d\", loop.Max)\n\t}\n\treturn nil\n}","typeGuard":"func hasValidMax(loop *LoopSpec) bool { return loop == nil || loop.Max >= 0 }","tryCatchPattern":"steps, err := formula.ApplyLoops(steps)\nif err != nil {\n\tvar msg string\n\tif strings.Contains(err.Error(), \"max must be positive\") {\n\t\tmsg = \"formula loop.max is negative; fix the loop spec\"\n\t}\n\treturn fmt.Errorf(\"loop config error: %v %s\", err, msg)\n}","preventionTips":["Never use negative max as a sentinel for 'unlimited'; omit the field instead","Lint formula files for negative numeric fields in loop specs","When computing max from variables, clamp with a lower bound of 0"],"tags":["go","formula","validation","loops"],"backgroundTag":"invalid-loop-config","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}