mcpbeat Sign in

Inclusive Period Boundaries And Offset Reset Agent Skill

Pick one boundary convention for a stepping period navigator and hold it everywhere — a closed upper bound at 23:59:59 drops the last second's sub-second remainder, and half-open bounds handed to an inclusive BETWEEN double-count the shared instant — then reset the step offset whenever the granularity changes, because N periods back at one length is not N periods back at another. Covers clamping at the present, deriving the forward affordance from the same value, and why the current period's totals are not comparable to the previous one's. Use when a period navigator lands on the wrong span after switching granularity, when a boundary event is missing or counted twice, or when a first-of-the-month comparison reads catastrophically low.

2k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill inclusive-period-boundaries-and-offset-reset

The instruction itself

3 sections, as written by the author

One boundary convention, and an offset that resets

The whole navigator is one function called with a different argument. offset counts periods

backwards: 0 is the current one, 1 the one before it.

// adapted
private fun rangeFor(range: DayRange, offset: Int): Pair<LocalDateTime, LocalDateTime> {
    val today = now().date
    val length = range.lengthInDays
    val end = today.minus(DatePeriod(days = offset * length))
    val start = end.minus(DatePeriod(days = length - 1))
    return start.atTime(0, 0) to end.atTime(23, 59, 59)
}

The comparison against the previous period is the same call with offset + 1, which is the entire

reason this shape is worth the indirection.

Traps

Two conventions in one feature is the normal state, not a hypothetical. Measured in a single file

here: the period bounds are closed (atTime(0, 0) … atTime(23, 59, 59)), while the chart's day,

week and month buckets are half-open (start of the unit … start of the unit *after*, the month

one taking a December branch to get there). Both conventions are then handed to

queries whose predicate is BETWEEN — inclusive at both ends, 11 of them, and not one written as

>= … <. Each convention is defensible; together they guarantee that at least one of them is wrong,

and neither produces an error.

A closed upper bound at 23:59:59 loses the rest of that second. Sub-second precision does not

vanish because the bound has none: timestamps are stored to the millisecond, so events between

23:59:59.001 and 23:59:59.999 fall outside every period, forever, and are counted by no span at all.

It is roughly one part in 86 400 of the day, so it is invisible in a total and it is *not* invisible

in a "what happened last" list, where the missing row is the newest one. Use the true last

representable instant, or move to half-open and change the predicate with it.

Half-open bounds fed to an inclusive predicate double-count the seam. [day, day+1] under

BETWEEN counts an event landing exactly on midnight in both buckets. Rare, exact, and the kind of

thing that makes two charts on the same screen disagree by one.

Half-open is the convention to standardise on, and it costs a predicate change. `>= start AND

< nextStart has no seam, no lost remainder, and no dependence on the storage precision. BETWEEN`

cannot express it — so this is a query change, not a bounds change, and doing only half the job makes

things worse than either convention alone.

The two conventions also disagree about a clock-change day. "Start of day" resolved through a

zone returns the first *existing* instant, which is 01:00 where midnight does not exist; a bare

atTime(0, 0) returns a wall-clock value that no event can carry. Neither is wrong; they are simply

different days, twice a year, on one of the two paths.

The offset is a count, so a granularity change carries it over silently. Three periods back at

7 days is 21 days ago; three periods back at 90 days is nine months ago. Nothing type-checks

differently, nothing throws, and the user who switches from weeks to quarters lands somewhere they

never asked for. Reset to the present on every granularity change — it is one field and it is the

only answer that is always defensible:

// adapted
fun setDayRange(range: DayRange) {
    state.update { it.copy(dayRange = range, periodOffset = 0) }
    loadPeriod()
}

Clamp at the present, and return early when the clamp bites. Without the early return, every

press of a disabled-looking arrow re-issues the whole load — a dozen queries for a state that did not

change:

// adapted
fun stepPeriod(delta: Int) {
    val next = (state.value.periodOffset - delta).coerceAtLeast(0)
    if (next == state.value.periodOffset) return
    state.update { it.copy(periodOffset = next) }
    loadPeriod()
}

Derive the forward affordance from the offset, never track it. `val canStepForward get() =

periodOffset > 0` cannot disagree with the clamp. A separate boolean can, and will, on the frame the

granularity resets.

The buckets *inside* a period have their own boundary rule, and it is a different one. Stepping

decides which span is on screen; chopping that span into bars is a separate decision with its own

failure — unequal bucket widths drawing a longer bar for the wider one (equal-buckets-or-no-buckets).

The two interact only through the granularity change that resets the offset.

A length field that is a lie for one member of the enum. Fixed-length ranges step by days; a

calendar year steps by years and takes a different branch entirely — so its lengthInDays is

declared, unused, and wrong. Someone will use it. Either give the calendar member no length, or make

the stepping unit part of the enum rather than a number that only some members honour.

The current period is truncated and the previous one is not, so every delta is biased low. With

fixed lengths the day counts match but the last day is partial, which is a bias of at most one day in

length. With calendar granularity it is brutal: on 2 January the current period is two days against

a full previous year, and every figure on the screen reads as a collapse. Either label the current

period as in-progress, or compare like-for-like by truncating the previous period to the same

elapsed fraction — but decide, because "it recovers by the end of the month" is not a fix.

Verifying it

  • Find every boundary construction and read the convention on each:
   grep -rn --include='*.kt' -E "atTime\(23|atTime\(0, 0\)|atStartOfDayIn" . | grep -v '/build/'

Mixed atTime(23, …) and atStartOfDay… hits in one feature are the split described above.

  • Count how the queries actually compare, because that decides which convention is safe:
   grep -rn --include='*.kt' 'timestamp BETWEEN' . | grep -v '/build/' | wc -l
   grep -rn --include='*.kt' -E 'timestamp >= .* timestamp <' . | grep -v '/build/' | wc -l

Eleven and zero here. Every BETWEEN is a closed comparison, so every half-open bound handed to

one has a seam.

  • Count the events the closed bound drops. Where the stored column is millis of the local wall

clock, this needs no zone function:

   SELECT COUNT(*) FROM activity_event WHERE timestamp % 86400000 > 86399000;

Every row returned is an event no period contains. The same expression = 0 counts the events

sitting exactly on a bucket seam, which are the ones counted twice.

  • Step, then switch. Go three periods back, change the granularity, and read the span label. It

must say the present. Then step forward at the boundary and confirm nothing reloads — a spinner

there is the missing early return.

How to use it

Copy the folder

Take maxrave-dev/inclusive-period-boundaries-and-offset-reset from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.