mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept data and the log entries, the key form, the entries' names and the settings with their validity; the settings file and the account with its directory stay as they are. Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
184 lines
6.4 KiB
Go
184 lines
6.4 KiB
Go
package main
|
|
|
|
// The switch rule (design 39 bus-and-switch §26, §27, §28, §32): after a reading of licence L, when switch_auto is on and
|
|
// L, with one or more consumers bound to it, is at or above switch_at_pct in its weekly window (C4: the session window takes
|
|
// no part), every consumer bound to L moves, in one act, to the other licence with known room whose weekly window is
|
|
// below room_below_pct and that has the most room, the first by name on a tie. The act is said as one licence.switched, a
|
|
// binding.changed per consumer, one audit entry switched, and told to the operator. A consumer is only ever moved off
|
|
// a licence at its threshold, so none flaps between two.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
. "claude-licence-manager/internal/licences"
|
|
)
|
|
|
|
// ByUsage is who moves a consumer by the rule (§26).
|
|
const ByUsage = "usage"
|
|
|
|
// Switch is one act of the rule: from L to the target, the consumers it moves, the window of L that reached its
|
|
// threshold, its percentage and reset, and the target's room.
|
|
type Switch struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Consumers []string `json:"consumers"`
|
|
Window string `json:"window"`
|
|
Pct float64 `json:"pct"`
|
|
ResetsAt string `json:"resetsAt"`
|
|
Room float64 `json:"room"`
|
|
}
|
|
|
|
// why is the cause a moved binding carries: L, the window, its percentage and its reset (§26).
|
|
func (s Switch) why() string {
|
|
return fmt.Sprintf("%s reached %g %% of its %s window, which resets at %s", s.From, s.Pct, s.Window, s.ResetsAt)
|
|
}
|
|
|
|
// fields are the act's fields, the same in licence.switched and the audit entry switched (§27).
|
|
func (s Switch) fields() map[string]any {
|
|
f := switchedFields(s.From, s.To, s.Consumers, Cause{Why: s.why(), By: ByUsage})
|
|
f["window"], f["pct"], f["resetsAt"], f["room"] = s.Window, s.Pct, s.ResetsAt, s.Room
|
|
return f
|
|
}
|
|
|
|
// Rule is what the rule finds for one licence at its threshold: the act, or none with NoTarget when no other licence
|
|
// has room; Over is then the licence's consumers and the window at its threshold, for the condition no-room (§29).
|
|
type Rule struct {
|
|
Switch *Switch
|
|
NoTarget bool
|
|
Over *Switch
|
|
}
|
|
|
|
// consumersOf are the consumers bound to licence, sorted.
|
|
func consumersOf(licence string, bindings []Binding) []string {
|
|
var consumers []string
|
|
for _, b := range bindings {
|
|
if b.Licence == licence {
|
|
consumers = append(consumers, b.Consumer)
|
|
}
|
|
}
|
|
sort.Strings(consumers)
|
|
return consumers
|
|
}
|
|
|
|
// windowAt is l's weekly window when it is at or above pct (C4: the session window takes no part): its name,
|
|
// percentage and reset; ok false when it is not.
|
|
func windowAt(u *UsageReading, pct float64) (window string, at float64, resetsAt string, ok bool) {
|
|
if u == nil || u.WeeklyPct == nil || *u.WeeklyPct < pct {
|
|
return "", 0, "", false
|
|
}
|
|
return "weekly", *u.WeeklyPct, u.WeeklyResetsAt, true
|
|
}
|
|
|
|
// targetFor is the licence §26 moves l's consumers to: among the others with known room whose weekly window is below
|
|
// room_below_pct (C4), the one with the most room, the first by name on a tie; empty when none has room.
|
|
func (m *Manager) targetFor(l Licence, licences []Licence, s Settings) (string, float64) {
|
|
others := append([]Licence(nil), licences...)
|
|
sort.Slice(others, func(i, j int) bool { return others[i].Name < others[j].Name })
|
|
to, best := "", -1.0
|
|
for _, o := range others {
|
|
if o.Name == l.Name {
|
|
continue
|
|
}
|
|
room, known := Room(o, m.Now(), s)
|
|
if !known || *o.Usage.WeeklyPct >= s.RoomBelow || room <= best {
|
|
continue
|
|
}
|
|
best, to = room, o.Name
|
|
}
|
|
return to, best
|
|
}
|
|
|
|
// RuleFor is what §26 gives for licence l among every licence and binding, under the settings s. A licence below its
|
|
// threshold, or with no consumer, gives nothing.
|
|
func (m *Manager) RuleFor(l Licence, licences []Licence, bindings []Binding, s Settings) Rule {
|
|
consumers := consumersOf(l.Name, bindings)
|
|
if len(consumers) == 0 {
|
|
return Rule{}
|
|
}
|
|
sw := Switch{From: l.Name, Consumers: consumers}
|
|
var ok bool
|
|
if sw.Window, sw.Pct, sw.ResetsAt, ok = windowAt(l.Usage, s.SwitchAt); !ok {
|
|
return Rule{}
|
|
}
|
|
if sw.To, sw.Room = m.targetFor(l, licences, s); sw.To == "" {
|
|
sw.Room = 0
|
|
return Rule{NoTarget: true, Over: &sw}
|
|
}
|
|
return Rule{Switch: &sw}
|
|
}
|
|
|
|
// ApplyRule applies §26 to one licence, or to every one when licence is empty: after a reading, by switch_auto, or by
|
|
// hand, even with switch_auto off (§32, §39). With dry it answers the moves and moves nothing. An invalid pair of
|
|
// settings moves no consumer (§33).
|
|
func (m *Manager) ApplyRule(ctx context.Context, licence string, byHand, dry bool) ([]Switch, error) {
|
|
s := m.settings()
|
|
if (!s.SwitchAuto && !byHand) || len(s.Invalid()) > 0 {
|
|
return nil, nil
|
|
}
|
|
licences, err := m.Store.Licences(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bindings, err := m.Store.Bindings(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var acts []Switch
|
|
for _, l := range licences {
|
|
if licence != "" && l.Name != licence {
|
|
continue
|
|
}
|
|
r := m.RuleFor(l, licences, bindings, s)
|
|
if r.Switch == nil {
|
|
continue
|
|
}
|
|
acts = append(acts, *r.Switch)
|
|
if dry {
|
|
continue
|
|
}
|
|
if err := m.act(ctx, *r.Switch, licences); err != nil {
|
|
return acts, err
|
|
}
|
|
// What was moved is bound to the target now, for the next licence's rule.
|
|
if bindings, err = m.Store.Bindings(ctx); err != nil {
|
|
return acts, err
|
|
}
|
|
}
|
|
return acts, nil
|
|
}
|
|
|
|
// act moves every consumer of one act as the switch verb does, then says the act once and tells the operator (§26 to
|
|
// §28).
|
|
func (m *Manager) act(ctx context.Context, sw Switch, licences []Licence) error {
|
|
kind := ""
|
|
for _, l := range licences {
|
|
if l.Name == sw.To {
|
|
kind = l.Kind
|
|
}
|
|
}
|
|
floor := m.floorIn(nil)
|
|
for _, consumer := range sw.Consumers {
|
|
b, err := m.Store.Bind(ctx, consumer, sw.To, Move{Kind: kind, Why: sw.why(), By: ByUsage, Floor: floor})
|
|
if err != nil {
|
|
return fmt.Errorf("moving %s to %s: %w", consumer, sw.To, err)
|
|
}
|
|
m.changed(b, sw.From)
|
|
}
|
|
f := sw.fields()
|
|
if err := m.record(ctx, "switched", fmt.Sprintf("switched %s from %s to %s: %s; by %s",
|
|
strings.Join(sw.Consumers, ", "), sw.From, sw.To, sw.why(), ByUsage), f); err != nil {
|
|
return err
|
|
}
|
|
_ = m.Emit("licence.switched", f)
|
|
if m.Tell != nil {
|
|
line := fmt.Sprintf("%s; %s has %g %% room", sw.why(), sw.To, sw.Room)
|
|
if err := m.Tell(ctx, strings.Join(sw.Consumers, ", ")+" moved to "+sw.To, []string{line}); err != nil {
|
|
m.Log("the switch of %s to %s could not be told: %v", strings.Join(sw.Consumers, ", "), sw.To, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|