"""Split the ordered discovery rows into the serial chunks of one sweep. A *sweep* is one orchestrator run. It deploys its rows in serial chunk blocks, each small enough that its last runner wave still starts inside the queue window (:mod:`cli.meta.ci.slots` sizes them). Two rules shape the split: * **Priority first, never mixed.** Priority rows sort to the head of the list and the split forces a chunk boundary at the priority/regular seam, so a chunk is either all priority or all regular. The seam chunk stays short rather than being topped up with regular rows -- that is what guarantees every priority row is done before the first regular one starts. Priority is never subject to the sweep offset: it leads every sweep. * **The head leads unless an operator says otherwise.** Chunk 0 starts at the top of the ranking. The list is ordered coverage-first, so its head is what a run most needs proven, and a sweep that cannot afford the whole list leaves the tail behind rather than sliding a window over it. ``offset`` moves that start deliberately: it is how an operator reaches the tail without waiting for anything. It defaults to 0, so every run that does not ask for it deploys the same head and can be compared against the last one. This used to be derived from the sweep number instead, which bought tail coverage at the price of the head -- chunk 0 began wherever the arithmetic pointed, the rows that matter most ran only every few sweeps, and no two runs were comparable. """ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Sequence from typing import TypeVar T = TypeVar("T") def slice_chunks(rows: Sequence[T], size: int) -> list[list[T]]: """Cut *rows* into consecutive blocks of at most *size*.""" if size < 1: raise ValueError(f"chunk size must be >= 1, got {size}") return [list(rows[start : start + size]) for start in range(0, len(rows), size)] def plan( priority: Sequence[T], regular: Sequence[T], *, size: int, blocks: int, budget: int, offset: int = 0, ) -> list[list[T]]: """The chunks one sweep deploys, priority blocks first. Args: priority: rows of the priority line, in query order. regular: rows of the regular line, in query order. size: rows one chunk may hold (``slots.chunk_size``). blocks: chunk blocks the workflow declares (``slots.chunk_blocks``). budget: rows the run job cap allows in total (``slots.available``). offset: how many regular rows to skip before filling the chunks. ``0`` (the default) starts at the head of the ranking. A value past the end of the list leaves the regular chunks empty rather than wrapping: an operator who asks for a window beyond the tail gets an empty run, not a silent restart at the top. Returns: at most *blocks* chunks, none larger than *size*, together holding at most *budget* rows. Priority chunks come first and are never topped up with regular rows. """ priority_chunks = slice_chunks(priority[:budget], size)[:blocks] spent = sum(len(chunk) for chunk in priority_chunks) capacity = max(min((blocks - len(priority_chunks)) * size, budget - spent), 0) if not capacity or not regular: return priority_chunks start = max(offset, 0) return priority_chunks + slice_chunks(regular[start : start + capacity], size)