Computing candidates one cell at a time is fine for the solver, but propagation needs them all at once - a candidate set for every cell. Today you build that candidate grid and use it to spot the puzzle's already-forced cells.
Build a candidate set for all 81 cells, and identify the cells already forced to one digit.
The candidate grid is just the per-cell candidate function applied to all 81 cells at once: an array of candidate sets, indexed the same way as the board. It is the state that constraint propagation will churn on - as digits get placed, sets in this grid shrink, and the goal is to drive every set down to a single digit.
Building it also reveals the puzzle’s free wins. A cell whose set already has size 1 is decided: either a given clue, or a blank so hemmed in by peers that only one digit fits. In the classic puzzle that is 35 cells - the 32 clues plus 3 blanks (such as cell 41) that were forced from the start. Those forced blanks are the seeds the naked-single rule will grow from in the propagation chapter.
// candidates for every cell, indexed the same 0..80 wayfunc CandidateGrid(g [81]int) [81]Set {var cg [81]Setfor i := 0; i < 81; i++ { cg[i] = Candidates(g, i) }return cg}// a cell with Size()==1 is decided (given or forced)