{"record":{"id":"33272cba67c23967","repo":"gastownhall/beads","slug":"loop-q-count-must-be-positive","errorCode":null,"errorMessage":"loop %q: count must be positive","messagePattern":"loop %q: count must be positive","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":86,"sourceCode":"\t\tloopTypes++\n\t}\n\tif loop.Range != \"\" {\n\t\tloopTypes++\n\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}","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L68-L104","documentation":"validateLoopSpec guards numeric loop bounds: a negative Count would make fixed-count expansion nonsensical (the expansion loop simply wouldn't run, silently dropping the body), so any negative Count is rejected with this error naming the step.","triggerScenarios":"ApplyLoops with a LoopSpec where Count < 0 (e.g. -1 from a subtraction bug or user input like count: -2 in a formula file). Note Count<=0 otherwise falls into the 'one of count/until/range' check; negative values reach this check via loopTypes counting only Count>0.","commonSituations":"User-supplied count from config/UI accepted without range validation; arithmetic producing -1 (e.g. len(items)-1 passed as a repeat count); templating that interpolates an empty value into a number field.","solutions":["Clamp or validate user-supplied counts to >=1 before constructing LoopSpec","Sanitize computed counts: `if n < 1 { n = 1 }` or fall back to an until-loop for data-driven repetition","Validate the formula file schema (count: integer >= 1) at load time with a clear message"],"exampleFix":"// before\ncount := len(items) - 1 // can be -1\nloop := &formula.LoopSpec{Count: count, Body: body}\nsteps, err := formula.ApplyLoops(steps) // count must be positive\n// after\ncount := len(items)\nif count < 1 { count = 1 }\nloop := &formula.LoopSpec{Count: count, Body: body}\nsteps, err := formula.ApplyLoops(steps)","handlingStrategy":"validation","validationCode":"n, err := strconv.Atoi(userCount)\nif err != nil || n < 1 {\n\treturn fmt.Errorf(\"loop count must be a positive integer, got %q\", userCount)\n}\nloop := &formula.LoopSpec{Count: n, Body: body}\nsteps, err := formula.ApplyLoops(steps)","typeGuard":"func positiveInt(v int) bool { return v > 0 }\nif loop != nil && loop.Count < 0 { /* reject before ApplyLoops */ }","tryCatchPattern":"steps, err := formula.ApplyLoops(steps)\nif err != nil {\n\tif strings.Contains(err.Error(), \"count must be positive\") {\n\t\treturn fmt.Errorf(\"bad loop count: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Validate user/config-supplied counts with strconv.Atoi and a >=1 lower bound","Guard computed counts (e.g. len-1) against going negative","Coerce 0/negative counts to 1 or switch to an until-loop for data-driven iteration"],"tags":["formula","loops","validation","input-validation"],"backgroundTag":"invalid-numeric-value","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}