Package twap is a fixed-window trailing average of an integer series: cheap to keep, cheap to read, and hard to move with a one-block spike.
It answers one question — "what has this number averaged over the last W?" — which is what an on-chain market wants from a price oracle and what a token vote wants from a supply-or-turnout figure: a value that a flash spike cannot shift, because moving the average requires HOLDING the pushed value for a real fraction of the window.
Not checkpoint
The sibling p/kourt/checkpoint answers a different question — "what was this value at sealed epoch N?" — from unbounded, paged history. This is a bounded rolling window with no archive: the last N buckets and nothing older. Different query, different structure, no overlap.
A value, not an object
A Ring is a plain value the caller stores inline on a record it was going to write anyway — a market's own row. It is never a heap object of its own, and never held by pointer in realm state: every method takes a Ring by value and returns an updated one. The reason to keep it a value is purely GAS, not safety: gno charges per object touched, so a Ring that rides its host's write costs nothing extra to keep, whereas a *Ring would be a second object (a second key) — and it would buy nothing, since every method is a value receiver that hands back a copy anyway. There is no mutator to borrow and so no security question here at all; the value shape is a cost decision.
Example
1r = r.Observe(height, price) // store r back onto your own record
2avg, ok := r.Average(height, window)
Buckets, and why a spike does not move the average
Time is quantised into fixed-width buckets. Each bucket remembers the last value seen in it; a bucket with no observation carries the previous value forward, so a quiet series reads as "unchanged", not as a gap. The average over a window of W is the mean of the W/width most recent buckets.
A spike therefore lands in at most one bucket — one part in W/width of the average — and only if it is still the last value in that bucket when the bucket closes. To move the average by a fraction f of the series' range you must keep it moved across about f·(W/width) buckets, i.e. hold the pushed value for f of the whole window against everyone trading back. One block is ~1/(W/width) of that, which for a week of hourly buckets is under a thousandth of a spike.
The freshness contract
That guarantee holds only while observations keep arriving. An empty bucket carries the last value forward, so if the caller STOPS observing, the last value — a spike included — persists across the whole window and the average becomes that single value, still reported mature. So the caller must Observe on every change to the tracked quantity, OR Observe at the height it later reads. StaleBy reports how far a read has drifted from the newest observation, for a caller that cannot guarantee the former. `mature` means "the window is covered", never "the data is recent".