Files
Maven/internal/delivery/channel.go
T
kami e0d0244fa9 Fold SPEC/maven/ROADMAP into DESIGN.md and drop the stale session logs
15 root markdown files, ~4,900 lines against ~33,000 lines of Go, with at least
three pairs contradicting each other. When five documents describe the
architecture, the code becomes the only trustworthy one — which defeats the
point of having them. That drift is why the resident-model question had four
incompatible answers.

SPEC.md, maven.md and ROADMAP.md are deduped into DESIGN.md rather than
concatenated, with a "Superseded" section carrying eight retired decisions and
what replaced each: classifier-owns-the-route (the cascade is still the live
path, but as a stopgap, not a design to extend), faster-whisper/vosk/silero,
the small-model phrasing claim, sqlcipher, the Kotlin/Spring sketches,
obsidian->chroma, script deployment, and FloorEnrollment. Superseded material
is kept and marked rather than deleted, so it cannot read as current.

SESSION-05/06-07-2026.md and PLANS.md are removed outright — git history holds
them, and both were verified tracked before deletion.

Go doc comments citing the deleted files are repointed to the equivalent
DESIGN.md sections. Several asserted designs that were already retired, so the
claims are corrected and not just relinked: stt.go named faster-whisper as
production (it is whisper.cpp), tts.go named silero (it is piper), intent.go
still described the classifier as owning the route, and stale vosk/chroma
vocabulary is replaced. ECOSYSTEM-SPEC.md references are deliberately
untouched — that is a different document, and a naive grep for SPEC.md matches
it.

Root markdown drops from 4,880 to ~3,700 lines. The review's ~1,500 target is
not reachable while keeping the files it also said to keep — those alone are
2,553 lines — so trimming further needs a separate decision on
MAVEN_ECOSYSTEM_ARCHITECTURE.md and PROGRESS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:39:56 +04:00

107 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package delivery is maven's channel-routing + dispatch layer.
//
// Spec contract (from DESIGN.md § Delivery / channel routing):
//
// - routing = f(severity, presence). presence decides REACHABILITY; severity
// decides INSISTENCE. need both.
//
// | | present | away |
// | sev12 (care) | voice | drop |
// | sev3 (ops soft) | voice | ntfy, once |
// | sev4 (ops hard) | voice + ntfy | telegram, repeat til ack |
//
// - sev ≤ 2 drops on away, sev ≥ 3 holds. "a missed water nudge is noise;
// a missed backup-failure isn't."
//
// - away channels (ntfy/telegram) leave the box — the one path that crosses
// "never phones home," through your relay. MINIMAL BODY — "disk low on
// homesrv," not detail. don't make notifications a shoulder-surf exfil
// surface.
//
// - reminders are a SEPARATE class — two delivery paths. reminders bypass
// the restraint gate ("wake me 7" fires in quiet hours; that's the point).
// snooze still applies. voice when present, ntfy when away. fire once.
//
// Architecture mirrors the loop's gather/pure split: the routing table is a
// PURE function of (severity, presence); the Dispatcher holds the impure Sinks
// (one per channel transport) and the recorder seams. ShouldRepeat is pure —
// the daemon's tick loop calls it each cycle for un-acked sev4 telegram sends.
package delivery
import (
"errors"
"github.com/kami/maven/internal/loop"
"github.com/kami/maven/internal/store"
)
// ErrVoiceNoSession — sentinel returned by the voice sink when no live
// client session exists at push time. The dispatcher treats this as a
// skip-continue: the voice channel is unavailable for this delivery, but
// remaining channels (ntfy, telegram) should still fire. The voicesink
// maps voice.ErrNoSession to this sentinel so the dispatcher doesn't need
// to import the voice package.
var ErrVoiceNoSession = errors.New("delivery: voice: no live session")
// Channel — one delivery transport. Drop is an explicit no-op (the routing
// table chose to suppress, which is a decision, not a failure — "a missed
// water nudge is noise"). a nil Sink for a wired channel is a daemon config
// gap, not a Channel value.
type Channel string
const (
ChannelVoice Channel = "voice"
ChannelNtfy Channel = "ntfy"
ChannelTelegram Channel = "telegram"
ChannelDrop Channel = "drop"
)
// ChannelsFor — the PURE routing table: f(severity, presence).
//
// presence decides reachability; severity decides insistence. the loop's gate
// already suppressed care nudges (sev ≤ 2) on away — this table is the
// delivery-side authority for ALL severities, including the ops nudges the
// gate lets through. double authority is intentional: the gate decides whether
// a rule EMITS; delivery decides where it LANDS. they agree on care-away
// (both drop) and diverge only where they must (ops survives away here, not
// because the gate let it through, but because delivery insists).
//
// sev4 present → voice + ntfy: the disk-fire alarm gets voice AND a push —
// you're here, but this is loud enough to also surface on the watch.
// sev4 away → telegram, repeat til ack: the one channel that crosses the relay
// and insists until you respond.
func ChannelsFor(sev loop.Severity, presence store.Bucket) []Channel {
if presence == store.Away {
switch {
case sev <= loop.Sev2:
return []Channel{ChannelDrop}
case sev == loop.Sev3:
return []Channel{ChannelNtfy}
case sev >= loop.Sev4:
return []Channel{ChannelTelegram}
}
return []Channel{ChannelDrop} // unknown sev → fail-closed
}
// present
if sev >= loop.Sev4 {
return []Channel{ChannelVoice, ChannelNtfy}
}
return []Channel{ChannelVoice}
}
// ChannelsForReminder — reminders are a SEPARATE class that bypasses the gate.
// "wake me 7" fires in quiet hours; that's the point. presence still routes
// reachability: voice when present, ntfy when away. fires once — no repeat
// (repeat-til-ack is a sev4 ops-hard behavior, not a reminder behavior).
//
// reminders don't carry a Severity — they're user-stated future intent, not
// loop-derived insistence. the routing is presence-only: reachability without
// the insistence axis. a reminder that must be louder (an alarm) is a future
// per-reminder override, not a table entry.
func ChannelsForReminder(presence store.Bucket) []Channel {
if presence == store.Away {
return []Channel{ChannelNtfy}
}
return []Channel{ChannelVoice}
}