pub fn window<T>(items: &mut Vec<T>, offset: usize, limit: usize)Expand description
Cut an already-ordered, already-materialised list down to the window a caller
asked for: skip offset items from the front, then keep at most limit.
This is the one place that decides what limit and offset mean for the
graph’s list lenses — debt_density, config_secrets and coupling
here, and the /nodes and /hotspots endpoints in the roteiro binary. It
exists because the parameter previously had two implementations: three lenses
truncated here and treated 0 as “no limit”, while two HTTP handlers used
Iterator::take and so returned nothing for 0 — the same parameter name
with opposite meanings, and nothing that could make the disagreement visible
(issue #375). A sixth list lens should call this rather than write a third.
One did write a third — see Episodic recall below — and the warning is left
standing because the next one will too.
The contract:
limit == 0means unlimited — every item that survivesoffsetis kept. This is the reading the CLI already documents (roteiro config-secrets --help: “0 shows every secret-named key”), so no published promise is withdrawn by making it universal; and it is the safer of the two, because a caller who passes an unset variable then gets more data than they meant to ask for rather than an empty page that reads like a truthful “nothing found”.offsetapplies first, andlimitto what remains. Sooffset = 20, limit = 0is “skip the first 20, then every remaining item”, not “skip 20, then nothing”. Anoffsetpast the end yields an empty window rather than panicking — a page beyond the last one is empty, not an error.- A caller’s reported
totalmust be taken before this runs: every surface reports the pre-windowing population, so a cut page still says what it was cut from.
Ordering is the caller’s job; this only removes, and only from the ends, so a deterministically-ordered input yields a deterministic window.
§The search channels call this too, and the unit there is per channel
search and the generated/memory channels behind search_channels used
to keep a limit == 0 => no hits guard of their own — a third reading of one
parameter name, in the same crate as the two #375 reconciled (issue #393).
They now window here like every other lens, so limit has one definition and
not a second implementation of it, which is exactly how the first two drifted.
What differs is the unit, not the rule: a search limit bounds each
channel independently, so 0 is “every match, in every channel that was
asked for”, not “every match overall”. That is a bounded request rather than
“dump the graph”: a channel’s ranking only orders a set the query has
already filtered — every token must appear in a hit — and a query with no
tokens returns nothing at any limit, 0 included. Measured on this
repository at 6,685 nodes: an unbounded one-token search returned ~2.7k hits
in 0.24 s and a two-token one returned 3, against a full-population scan that
every limit pays anyway, so unlimited costs no more than the default does.
The MCP tools are the one surface that cannot ask for it, deliberately:
they clamp limit into 1..=25 and advertise "minimum": 1, because their
results are spent against a model’s context window and 0 would be the one
value that escaped the ceiling those clamps exist to impose. That is a
surface declining to offer a value, not a second meaning for it — a model
that sends 0 anyway gets the smallest page, never the silent empty answer
this issue is about. The reasoning is restated where each clamp lives, in
rto_render::mcp::GraphServer::search and the served-chat search arm in
the roteiro binary; if this rule changes, those two must change with it.
§Episodic recall — the third implementation this doc predicted (issue #447)
crate::Store::recall_memory ranked, then called
Vec::truncate directly, so limit = 0 emptied the result: recall --limit 0 returned nothing on a store where five other surfaces returned
everything, and the JSON said "live": 8000 beside "results": [] — not a
lie, and no help at all. It calls this now. Two details are worth keeping:
NoneandSome(0)had to collapse onto one meaning, not two.RecallOptions::limitis anOption, so “unlimited” was already sayable twice; the fix mapsNoneto0rather than adding a branch, because two spellings of one request are how the first divergence started.memory listcuts in SQL and so cannot call this.LIMIT 0in SQL means the opposite of what this function means, somemory::recordsomits the clause entirely for0. That is the contract translated, and it is the only place in the crate where the rule is re-expressed rather than called — worth knowing if it ever has to change.