Files
steve 9116abcae2 P4: budget battery — DBBudget (rolling 7-day) over run.Budget
Second Tier-2 battery, plugging into run.Ports.Budget:
- budget.go: skillexec's BudgetTracker / NoOpBudget / DBBudget moved clean
  (stdlib only). Check/Commit match run.Budget exactly (compile-time proof in
  run.go: NoOpBudget and *DBBudget are run.Budget).
- storage.go: the BudgetStorage seam + SkillBudget domain, split out of mort's
  GORM file (the GORM impl stays in mort).
- memory.go: NewMemory() — zero-dependency in-process BudgetStorage with the
  7-day rolling-window rollover in Add.

Tests: per-user cap enforced, window rolls over after 7 days, NoOp always
allows. CI invariant: core imports ZERO from the budget battery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 00:12:19 -04:00

34 lines
1.2 KiB
Go

package budget
import (
"context"
"time"
)
// BudgetStorage is the persistence seam behind DBBudget: one budget row per
// user, with an atomic Add that rolls the 7-day window over transparently. Mort
// backs this with GORM/MySQL (the skill_budgets table); Memory() is the
// zero-dependency default; contrib/store adds a durable SQLite one.
type BudgetStorage interface {
// Initialize runs any schema setup. Safe to call repeatedly.
Initialize(ctx context.Context) error
// Get returns the user's current budget row, or (nil, nil) if none exists.
Get(ctx context.Context, userID string) (*SkillBudget, error)
// Add increments seconds_used + runs_count atomically, rolling the window
// over when WindowStart is older than 7 days (reset to now, fresh count).
// Creates the row if absent.
Add(ctx context.Context, userID string, secondsUsed float64, now time.Time) error
}
// SkillBudget is one user's rolling-window usage row.
type SkillBudget struct {
UserID string
WindowStart time.Time
SecondsUsed float64
RunsCount int
UpdatedAt time.Time
}
// budgetWindow is the rolling window length the storage rolls over at.
const budgetWindow = 7 * 24 * time.Hour