{"record":{"id":"6b21cec466e707c1","repo":"gastownhall/beads","slug":"loop-q-one-of-count-until-or-range-is-required","errorCode":null,"errorMessage":"loop %q: one of count, until, or range is required","messagePattern":"loop %q: one of count, until, or range is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":75,"sourceCode":"func validateLoopSpec(loop *LoopSpec, stepID string) error {\n\tif len(loop.Body) == 0 {\n\t\treturn fmt.Errorf(\"loop %q: body is required\", stepID)\n\t}\n\n\t// Count the number of loop types specified\n\tloopTypes := 0\n\tif loop.Count > 0 {\n\t\tloopTypes++\n\t}\n\tif loop.Until != \"\" {\n\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","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L57-L93","documentation":"A LoopSpec must specify exactly one repetition driver: Count, Until, or Range. If none is set, validateLoopSpec rejects it with this error because the expansion logic would have no iteration semantics.","triggerScenarios":"ApplyLoops encountering a step whose Loop has a non-empty Body but Count<=0, Until==\"\", and Range==\"\" — e.g. `&LoopSpec{Body: body}` or `&LoopSpec{Max: 5, Body: body}` (Max alone is not a driver).","commonSituations":"Authors assuming Max alone makes a conditional loop (Max only caps Until loops); a Until condition removed by variable substitution leaving an empty string; copy-pasting a loop template and deleting the count/until line.","solutions":["Set exactly one of Loop.Count (fixed repetitions), Loop.Until (condition string, requires Max), or Loop.Range","For a capped conditional loop, set both Until and Max: `&LoopSpec{Until: \"step.status == 'complete'\", Max: 10, Body: body}`","Pre-validate LoopSpec before ApplyLoops: body non-empty plus exactly one of Count>0 / Until / Range"],"exampleFix":"// before\nloop := &formula.LoopSpec{Max: 10, Body: body} // no driver\nsteps, err := formula.ApplyLoops(steps) // loop \"build\": one of count, until, or range is required\n// after\nloop := &formula.LoopSpec{Until: \"step.status == 'complete'\", Max: 10, Body: body}\nsteps, err := formula.ApplyLoops(steps)","handlingStrategy":"validation","validationCode":"func loopDriverSet(l *formula.LoopSpec) int {\n\tn := 0\n\tif l.Count > 0 { n++ }\n\tif l.Until != \"\" { n++ }\n\tif l.Range != \"\" { n++ }\n\treturn n\n}\nif loop != nil && len(loop.Body) > 0 && loopDriverSet(loop) == 0 {\n\treturn fmt.Errorf(\"loop needs count, until, or range\")\n}","typeGuard":"func hasLoopDriver(l *formula.LoopSpec) bool {\n\treturn l.Count > 0 || l.Until != \"\" || l.Range != \"\"\n}","tryCatchPattern":"steps, err := formula.ApplyLoops(steps)\nif err != nil {\n\tif strings.Contains(err.Error(), \"one of count, until, or range is required\") {\n\t\treturn fmt.Errorf(\"loop misconfigured: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Set exactly one iteration driver on every LoopSpec","Remember Max alone is not a driver; pair it with Until","Pre-validate specs with the same rule ApplyLoops enforces"],"tags":["formula","loops","validation"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}