A simple, honest difficulty measure falls right out of what you built - does the puzzle yield to pure deduction, or does it need a guess? Today you rate a puzzle Easy or Hard on exactly that line, closing the chapter.
Rate a puzzle Easy if propagation alone solves it, Hard if it needs search.
The two engines you built - deduction and search - draw a natural, meaningful line through the space of puzzles. If naked and hidden singles propagate all the way to a full grid, the puzzle needed no guessing at all: call it Easy. If propagation stalls with cells still undecided, so the full solver had to branch and search, it needed at least one guess: call it Hard. It is a coarse rating, but a true one, measured by your own solver’s behaviour rather than a made-up score.
This mirrors how human-oriented graders work - they rank a puzzle by the hardest technique it forces you to use - only here the “techniques” are the two rules you implemented. Richer graders add more rules (naked pairs, box-line reduction, and so on) to spread puzzles across more bands, which is a natural extension. For now, Easy versus Hard captures the essential split, and it completes the toolkit: solve, count, generate, and rate. The capstone puts all of it to work at once.
// Easy if pure propagation finishes the grid; Hard if it needs a guessfunc Difficulty(g [81]int) string {cg, ok := Propagate(CandidateGrid(g))if ok && allSingles(cg) { return "Easy" }return "Hard"}