Files
claude abbf0fe60d config: digest and pattern proposals move to their own file (V-410)
Both shape what reaches him unasked, so they read together.

The digest defaults were three literals inline in applyDefaults; they are now
named next to the struct like every other block's, at the same values (30m, 5,
sev 2). normaliseDigest is the one normalise that materialises an absent block
instead of folding it to nil, because the dispatcher reads Digest.Enabled with
no nil check — noted in the comment so the next block does not copy it.
2026-08-06 01:35:49 +04:00

92 lines
3.9 KiB
Go

package config
import "time"
// The two blocks that shape what reaches him unasked: how nudges are batched,
// and whether a routine Maven inferred by herself may be announced at all.
// DigestConfig — notification batching / digest mode. When enabled, eligible
// nudges (severity ≤ SeverityCeiling) are queued in memory instead of sent
// immediately. Every Window duration (or when MaxItems reached), the queue is
// flushed as a single digest notification. nil ⇒ digest disabled (legacy
// behaviour — every nudge is sent as it fires).
type DigestConfig struct {
Enabled bool `json:"enabled,omitempty"`
Window Duration `json:"window,omitempty"` // e.g. "30m"
MaxItems int `json:"max_items,omitempty"` // flush at this count
SeverityCeiling int `json:"severity_ceiling,omitempty"` // max sev batched
}
// Digest defaults, applied in normaliseDigest.
const (
DefaultDigestWindow = 30 * time.Minute
DefaultDigestMaxItems = 5
DefaultDigestSeverityCeiling = 2
)
// normaliseDigest is the one block that does NOT fold an absent block to nil:
// it materialises a disabled one instead, because the dispatcher reads
// c.Digest.Enabled without a nil check.
func (c *Config) normaliseDigest() {
if c.Digest == nil {
c.Digest = &DigestConfig{Enabled: false}
}
if c.Digest.Window == 0 {
c.Digest.Window = Duration(DefaultDigestWindow)
}
if c.Digest.MaxItems == 0 {
c.Digest.MaxItems = DefaultDigestMaxItems
}
if c.Digest.SeverityCeiling == 0 {
c.Digest.SeverityCeiling = DefaultDigestSeverityCeiling
}
}
// PatternProposalConfig — announcement policy for routines the digestion tick
// inferred by itself (Vikunja #247, #43).
//
// Detection is always on and always silent by default: the tick writes a
// proposed_routines row and the /routines page shows it. Notify is what turns
// "she noticed" into "she said something", and it is OFF unless configured —
// Maven is not a nag and not autonomous, so a behaviour that speaks without
// being asked has to be switched on deliberately, like weather and telegram.
//
// When Notify is on, the announcement is still heavily restrained:
// - at most one proposal per tick, however many were detected;
// - at most one per Cooldown across all pairs (not per pair), so a batch of
// freshly-detected patterns cannot turn into a queue of interruptions;
// - through the ordinary care-class gate (quiet hours / away / snooze), at
// sev1 — the lowest severity there is. A proposal is the least urgent
// thing Maven can say.
//
// A pair is only ever announced once, because it is only ever proposed once:
// proposed_routines is UNIQUE(action, object) and the row survives dismissal.
type PatternProposalConfig struct {
// Notify — announce newly inferred routines. Default false.
Notify bool `json:"notify,omitempty"`
// Cooldown — minimum spacing between two proposal announcements. 0 ⇒
// DefaultProposalCooldown (24h).
Cooldown Duration `json:"cooldown,omitempty"`
}
// DefaultProposalCooldown — one inferred-routine announcement per day at
// most. A proposal is never urgent; if two patterns surface in the same
// hour, the second one waits, and the /routines page has it either way.
const DefaultProposalCooldown = 24 * time.Hour
// AnnounceProposals reports whether inferred routines may be announced. Safe
// on a nil receiver — an absent config block means silent detection.
func (p *PatternProposalConfig) AnnounceProposals() bool {
return p != nil && p.Notify
}
// normalisePatternProposals leaves an absent block nil, which means silent
// detection. A present-but-partial one gets the cooldown, so `{"notify": true}`
// is enough to switch announcements on.
func (c *Config) normalisePatternProposals() {
if c.PatternProposals != nil && c.PatternProposals.Cooldown <= 0 {
c.PatternProposals.Cooldown = Duration(DefaultProposalCooldown)
}
}