The two rules and the contradiction check are strongest together, applied over and over until nothing more can be deduced. Today you combine them into a single propagate step - the deduction engine the full solver will call at every node.
Alternate naked and hidden singles until neither changes the grid, reporting any contradiction.
Naked singles feed hidden singles and vice versa: assigning a hidden single eliminates from peers, which may expose a naked single, whose assignment may reveal another hidden single. So the strongest deduction step alternates both rules and repeats until a full round changes nothing - the combined fixpoint - bailing out early if a cell ever empties. This one function is the puzzle’s entire logical closure: everything derivable without guessing.
It also draws the honest line between deduction and search. The medium puzzle, which stalled under naked singles alone, now solves completely - hidden singles were the missing rule. But the 21-clue hard puzzle propagates to a consistent state with 60 cells still holding multiple candidates: no contradiction, yet no progress either. Pure logic has run out, and the only way forward is to guess and check - which is exactly where propagation and backtracking finally join, in the next lesson.
// loop both rules until a full round makes no change, or a contradiction appearsfunc Propagate(cg [81]Set) ([81]Set, bool) {for {before := cgcg = PropagateNaked(cg)cg = PropagateHidden(cg)if HasContradiction(cg) { return cg, false }if cg == before { return cg, true }}}