hub v0.91.1 — observation may only WIDEN a tier's window, never tighten it (R-86)
gates / gates (push) Successful in 7s

Found by checking v0.91.0 against the live box, not by review. demo-felhom's two
retained PBS snapshots sit 8h54m apart (one is a healing artefact), so the
mean-gap estimator reads a WEEKLY tier as nine-hourly: x4 = 36h, the 7-day floor
lifts it to 168h, and a weekly tier proved weekly reaches ~8.25d of proof age.
The false alarm this task exists to prevent would have returned within a week, on
the box it had just shipped to.

restoreProvenWindow now takes max(observed, declared). A gap SHORTER than the
declared rhythm is routine and means nothing (a retry, a manual run, a heal, a
catch-up); a gap LONGER than it is real information. Cost stated: a tier running
faster than its declared rhythm gets a slower stale signal — the right direction
for a signal that means 'unverified', since 'broken now' is a different event.
This commit is contained in:
2026-08-03 15:16:59 +02:00
parent 687fedd8ee
commit 046df303b6
3 changed files with 51 additions and 1 deletions
+18 -1
View File
@@ -99,9 +99,26 @@ func declaredArchiveInterval(tier string) time.Duration {
// this tier, so the declared rhythm is used. That fallback matters most for exactly the tier this
// task is about: a fresh box with a weekly offsite tier has one snapshot and no observable
// interval, and falling back to the FLOOR there would recreate the false alarm.
//
// OBSERVATION MAY ONLY WIDEN, NEVER TIGHTEN — and this is not caution, it is a live measurement.
// On demo-felhom (2026-08-03) the offsite tier's two retained snapshots are `2026-07-27T19:55:41Z`
// and `2026-07-28T04:49:43Z`: **8 h 54 m apart**, because one is a healing artefact and the other a
// real weekly run. A mean-gap estimate therefore reads a WEEKLY tier as nine-hourly, ×4 gives 36 h,
// the floor lifts it to 7 days — and a weekly tier proved weekly reaches ~8.25 days of proof age, so
// the false alarm this whole task exists to prevent would have returned within a week, on the very
// box it shipped to.
//
// The asymmetry is right on its own terms too. A gap SHORTER than the declared rhythm is routine and
// means nothing — a retry, a manual run, a heal, a catch-up after an outage. A gap LONGER than the
// declared rhythm is real information: this tier genuinely receives archives less often than the
// model says, and its window must widen or it alarms. So observation refines the rhythm upward and
// is ignored downward. The cost is stated plainly: a tier that truly runs FASTER than its declared
// rhythm gets a wider window than it strictly needs, i.e. a slower stale signal. That is the right
// direction for a signal whose message is "unverified" — "broken NOW" is `restore_test_failed`, and
// that one is immediate and unaffected.
func restoreProvenWindow(tier string, observed time.Duration, observedOK bool) time.Duration {
interval := declaredArchiveInterval(tier)
if observedOK && observed > 0 {
if observedOK && observed > interval {
interval = observed
}
w := time.Duration(restoreProvenGenerations) * interval