3f14aae032
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>
34 lines
1.2 KiB
Go
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
|