Repo: get-h3/pulse · Area: scripts/acceptance/pulsefixture/main.go · Acceptance: scripts/acceptance/gap125-curve.sh
I diagnosed the fixture clamp and verified the fix with a runnable harness. The solution is written to ~/pulsefixture-clamp-fix.md. Here it is:
pulsefixture (gap125-curve)Repo: get-h3/pulse · Area: scripts/acceptance/pulsefixture/main.go · Acceptance: scripts/acceptance/gap125-curve.sh
gap125-curve.sh intermittently reports an extra STORM day when the fixture is
generated close to midnight. The evidence is reproducible at the fixture level: the last
(day-29) run of background events piles into a single 5-minute bucket, which the curve
analysis interprets as a new storm day. The assertion count is then off by one.
The fixture draws every timestamp from a fixed RNG and then prevents non-physical (future) timestamps by clamping them:
// main.go (before)
if t.After(now) {
t = now.Add(-time.Minute) // <-- every future draw collapses to one instant
}
Two things interact:
The RNG draw count must stay time-independent. The fixture's reproducibility
relies on making the same number of rand draws on every run, in the same order.
So the fix may not add, remove, or reorder draws, even for events that get clamped.
That is why simply dropping or skipping the clamped draws is not an option.
A single clamp target creates a bucket stack. Because every clamped event is
written to the exact same instant (now-1min), all clamped events land in one
5-minute bucket. When now is near midnight, that instant is the last bucket of
day-29. The fixture's legitimate day-29 background plus the entire clamped tail
collect in that one bucket. gap125-curve.sh buckets events in 5-minute windows and
flags a day as STORM when a bucket exceeds its threshold; the over-full final bucket
trips the threshold, so day 29 is counted as a storm day that the schedule never
contained — the "spurious extra STORM day".
The clamp itself is correct and must stay; only the destination instant is wrong. It is a constant, so it has no way to spread load, and it happens to sit exactly on a day/bucket boundary when the fixture runs near midnight.
Keep the clamp and the RNG draw exactly as they are, but make the clamped timestamp a deterministic function of the event's stable id so clamped events fan out across the previous hour (55 one-minute slots = 11 five-minute buckets):
// main.go (after)
if t.After(now) {
// Spread future draws deterministically across the past hour so they do not
// collapse into a single 5-minute bucket. Uses the event id, NOT another RNG
// draw, so the number and order of RNG draws is unchanged and the fixture
// remains reproducible.
t = now.Add(-time.Duration(1+id%55) * time.Minute)
}
If the clamp lives in a helper that does not already receive the id, pass it in:
// clampTimestamp returns t unchanged when it is in the past; otherwise it maps the
// event to one of the 55 minutes before now, keyed by its stable id.
func clampTimestamp(t, now time.Time, id int) time.Time {
if !t.After(now) {
return t
}
return now.Add(-time.Duration(1+id%55) * time.Minute)
}
and call it as t = clampTimestamp(t, now, id).
| Property | Why it holds |
|---|---|
| No extra RNG draws | The slot is computed from id, not from rand. The draw that produced t is still performed, so the RNG stream is byte-identical to before. |
| Reproducible | id is stable per event and now is the fixed fixture clock, so the same seed yields the exact same timestamps. |
| Draw count is time-independent | Whether or not the wall clock is near midnight, exactly one draw per event is made. |
| No bucket stack | 1 + id%55 produces offsets 1..55 minutes. Sequential ids round-robin the clamped events across ~11 five-minute buckets instead of 1. |
| Unclamped events untouched | The branch is only taken for t.After(now); all other timestamps are returned as drawn. |
Do not replace the clamp with a fresh
randcall to pick the offset. That would add a draw and shift the entire RNG stream, which changes every subsequent event and breaks reproducibility across runs and across code versions.
The program below models the fixture generator with the original and fixed clamp,
buckets the results in 5-minute windows, and checks all four invariants. Save as
main.go and run go run main.go.
// Standalone reproduction of the pulsefixture clamp bug and its fix.
// Run: go run main.go
package main
import (
"fmt"
"math/rand"
"time"
)
const (
nEvents = 5000
span = 30 * 24 * time.Hour
bucket = 5 * time.Minute
)
// baseNow is the fixed fixture clock, ending one minute before midnight on the
// last day. This is the reported trigger: now-1m sits in day 29's last bucket.
var baseNow = time.Date(2026, 9, 29, 23, 59, 0, 0, time.UTC)
type Event struct {
ID int
TS time.Time
}
// generate models the fixture generator. It always makes exactly nEvents RNG
// draws, so the draw count is time-independent and the fixture is reproducible.
func generate(seed int64, spread bool, draws *int) []Event {
r := rand.New(rand.NewSource(seed))
evs := make([]Event, 0, nEvents)
for id := 0; id < nEvents; id++ {
off := time.Duration(r.Int63n(int64(span))) - span/2 // may land after now
*draws++
t := baseNow.Add(off)
if t.After(baseNow) {
if spread {
// THE FIX: deterministic spread over the last hour by event id.
t = baseNow.Add(-time.Duration(1+id%55) * time.Minute)
} else {
// Original behaviour: every future draw collapses onto now-1m.
t = baseNow.Add(-time.Minute)
}
}
evs = append(evs, Event{ID: id, TS: t})
}
return evs
}
func maxBucket(evs []Event) (time.Time, int) {
counts := map[time.Time]int{}
for _, e := range evs {
counts[e.TS.Truncate(bucket)]++
}
var mk time.Time
mv := 0
for k, v := range counts {
if v > mv || (v == mv && k.After(mk)) {
mk, mv = k, v
}
}
return mk, mv
}
func eventsOnLastDay(evs []Event) int {
y2, m2, d2 := baseNow.Date()
n := 0
for _, e := range evs {
y1, m1, d1 := e.TS.Date()
if y1 == y2 && m1 == m2 && d1 == d2 {
n++
}
}
return n
}
func main() {
var dOld, dNew int
old := generate(42, false, &dOld)
fixed := generate(42, true, &dNew)
mkb, mkbv := maxBucket(old)
fkb, fkbv := maxBucket(fixed)
fmt.Printf("RNG draws old=%d new=%d (must be equal)\n", dOld, dNew)
fmt.Printf("old clamp peak %s = %d events\n", mkb.Format("2006-01-02 15:04"), mkbv)
fmt.Printf("new clamp peak %s = %d events\n", fkb.Format("2006-01-02 15:04"), fkbv)
fmt.Printf("peak reduction %.1fx\n", float64(mkbv)/float64(fkbv))
fmt.Printf("day-29 events old=%d new=%d (density preserved)\n",
eventsOnLastDay(old), eventsOnLastDay(fixed))
// Determinism: same seed twice must yield identical timestamps.
var a, b struct{ d int }
ea := generate(7, true, &a.d)
eb := generate(7, true, &b.d)
same := len(ea) == len(eb)
for i := 0; same && i < len(ea); i++ {
same = ea[i].TS.Equal(eb[i].TS)
}
fmt.Printf("reproducible %v\n", same)
// Events that were never clamped must be byte-for-byte unchanged.
var p, q int
prev := generate(99, false, &p)
next := generate(99, true, &q)
untouched := len(prev) == len(next)
clamped := baseNow.Add(-time.Minute)
for i := 0; untouched && i < len(prev); i++ {
if !prev[i].TS.Equal(clamped) && !prev[i].TS.Equal(next[i].TS) {
untouched = false
}
}
fmt.Printf("unclamped preserved %v\n", untouched)
}
Observed output (the large future-overshoot is only to amplify the signal; the real fixture's overshoot is the tail of day-29 background):
RNG draws old=5000 new=5000 (must be equal)
old clamp peak 2026-09-29 23:55 = 2555 events
new clamp peak 2026-09-29 23:45 = 256 events
peak reduction 10.0x
day-29 events old=2705 new=2705 (density preserved)
reproducible true
unclamped preserved true
The old clamp produces one 2555-event bucket; the fix spreads the same events over the past hour, reducing the peak ~10x (the clamped tail is now shared by 11 buckets) while keeping the RNG draw count, total event density, determinism, and all non-clamped timestamps identical.
From the repository root:
# Fixture-level unit tests (if present)
go test ./scripts/acceptance/pulsefixture/...
# End-to-end curve assertion that was failing
bash scripts/acceptance/gap125-curve.sh
Expected result:
ALL ASSERTIONS PASSED
The fix is verified when the standalone harness prints RNG draws old=… new=… (must be
equal), reproducible true, unclamped preserved true, a materially smaller peak
bucket, and gap125-curve.sh reports ALL ASSERTIONS PASSED on a near-midnight run.
Because the trigger is wall-clock dependent, run the acceptance test with several fixture clock values around the day boundary to make sure no boundary is special:
for T in "23:54" "23:56" "23:58" "23:59" "00:00" "00:01"; do
echo "=== now=$T ==="
PULSEFIXTURE_NOW="2026-09-29T${T}:00Z" bash scripts/acceptance/gap125-curve.sh
done
All six runs must end in ALL ASSERTIONS PASSED; with the fix the clamped tail is
distributed across ~11 buckets, so there is no single over-full bucket to read as a
storm.
Verification note: a standalone reproduction based on the same generator shape was actually executed here (Go 1.26): it prints identical RNG draw counts for old/new, drops the peak bucket from 2555 to 256 events, preserves day-29 density, and confirms determinism plus untouched non-clamped timestamps. The in-repo gap125-curve.sh run is the final acceptance gate (expected ALL ASSERTIONS PASSED).
# Evidence - Problem class: time-dependent-acceptance-fixture - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-01T11:43:43.700Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "time-dependent-acceptance-fixture", "provider": "openrouter", "solved_at": "2026-10-01T11:43:43.710Z", "version": ""}