| 253 | // per-phase done/total roll-up. Steps in phases ranked after `activeRank` are |
| 254 | // "ahead of sequence" and the UI flags them so users don't jump the gun. |
| 255 | function computeOrdering(steps) { |
| 256 | const phases = []; |
| 257 | const byKey = {}; |
| 258 | for (const st of steps) { |
| 259 | const key = st.section || "Steps"; |
| 260 | if (!byKey[key]) { |
| 261 | byKey[key] = { name: key, rank: st.rank == null ? null : st.rank, total: 0, done: 0 }; |
| 262 | phases.push(byKey[key]); |
| 263 | } |
| 264 | byKey[key].total++; |
| 265 | if (st.status === "done") byKey[key].done++; |
| 266 | } |
| 267 | let activeRank = null; |
| 268 | let activePhase = null; |
| 269 | for (const st of steps) { |
| 270 | if (st.status !== "done" && st.rank != null) { |
| 271 | if (activeRank == null || st.rank < activeRank) { |
| 272 | activeRank = st.rank; |
| 273 | activePhase = st.section; |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | return { activeRank, activePhase, phases }; |
| 278 | } |
| 279 | |
| 280 | // Derive the five validation gates from progress/summary step titles. |
| 281 | function deriveGates(steps, summaryText) { |