Documentation
¶
Overview ¶
Package teams is the one store for teams: named sets of conversations, the file they live in, each member's short handle, a team's manager and the Traffic log a team's members and manager write to. The conversations view (internal/tui3) and the team tools a model calls (internal/session) both read and write through it, so the two never keep two accounts of one team.
THE MODEL. A team has a random id minted once, a name the person gave it, at most one parent team, an ordered list of members and at most one manager. A member is a conversation, known by its conversation key, with enough beside the key (its file, workspace and title) to open it again. A conversation may be in any number of teams. Everything that names a team names it by id, never by its place in the list or by its name.
THE LAWS.
- The file is <profile>/teams.json, resolved with config.ProfilePath, so an empty profile directory is the ordinary launch and means this process's own profile, never "no profile". It is never config.json.
- A missing file is no teams and no error. A file that is there but unreadable is an error, and nothing here overwrites it: SetAside moves it out of the way when the caller decides to start again.
- Every field a later build wrote survives a load and a save.
- A chain of parents never loops, and a parent always exists.
- A handle is lowercase, 2 to 12 characters, unique within its team, derived from the member's title once and never changed automatically.
- The manager is always a member. Removing it from the team clears it.
- Every write is a read-modify-write under an exclusive file lock (Update), so two processes writing the same file never lose each other's change. Reading takes no lock and never waits.
- The Traffic log is append-only JSON lines, one file per team, rotated once past a few megabytes.
WHAT THIS PACKAGE DOES NOT KNOW. It imports neither the interface nor the session. A team's colour is kept here as data (a hue angle and a tier) with the pure arithmetic that spaces colours apart; which hues a palette reserves for meaning is the caller's to say. The live state of a member (running, asking, finished) is the caller's too, handed to Digest as MemberState.
Index ¶
- Constants
- Variables
- func AcceptClosing(profileDir string, p Packet) (bool, error)
- func AppendTraffic(profileDir, teamID string, e Entry) error
- func AppendTrafficID(profileDir, teamID string, e Entry) (string, error)
- func DecisionsPath(profileDir, teamID string) string
- func Delete(profileDir, id string) ([]string, error)
- func DeriveHandle(title string) string
- func Digest(team Team, states map[string]MemberState, recent []Entry, budget int) string
- func FallbackHandle(t Team, key string) string
- func HueAllowed(h float64, reserved []float64) bool
- func HueGap(a, b float64) float64
- func Index(teams []Team, id string) int
- func IsRuling(e Entry) bool
- func IsWrapUp(e Entry) bool
- func Money(usd float64) string
- func NewID() string
- func PacketsStamp(profileDir string) string
- func ParentLoops(teams []Team, id, parent string) bool
- func Path(profileDir string) string
- func Quiet(profileDir string, f *File, now time.Time, idle time.Duration) ([]string, error)
- func RaiseTo(ceiling float64) float64
- func RoundMoney(usd float64) float64
- func Save(profileDir string, teams []Team) error
- func SetAside(profileDir string) (string, error)
- func SpendStamp(profileDir, ledger string) string
- func Stamp(profileDir string) string
- func TeamDir(profileDir, teamID string) string
- func TeamSpendStamp(profileDir, teamID, day string) string
- func ThreadID(s string) (string, bool)
- func ThreadNumber(id string) string
- func TierFor(n int) int
- func Today() string
- func Told(profileDir, id string) error
- func TrafficPath(profileDir, teamID string) string
- func TrafficStamp(profileDir, teamID string) string
- func Update(profileDir string, fn func(*File) error) error
- func UsageLedgerPath() string
- func ValidHandle(h string) error
- func WriteMoveNotices(profileDir string, notes []MoveNotice) error
- type CapFacts
- type ClosingReport
- type Defaults
- type Effective
- type Entry
- type File
- func (f *File) AddMember(id string, m Member) error
- func (f *File) Ancestors(id string) []Team
- func (f *File) CanNest(parent string, d Defaults) bool
- func (f *File) Children(id string) []Team
- func (f *File) ChooseHandle(id, key string, choices []string, title string) (old, now string, err error)
- func (f *File) ClearManager(id string) error
- func (f *File) ClearWrap(id string) error
- func (f *File) Close(id string, at time.Time, report string) error
- func (f *File) ClosedTeams() []Team
- func (f *File) Colour(reserved []float64) bool
- func (f *File) Depth(id string) int
- func (f *File) Descendants(id string) []Team
- func (f *File) DissolveRoot()
- func (f *File) Effective(id string, d Defaults) Effective
- func (f *File) Home(key string) (Report, bool)
- func (f *File) LCA(keys ...string) (Team, bool)
- func (f *File) Links(key string) []Report
- func (f *File) MakeRoot(at time.Time) string
- func (f *File) Move(ids []string, parent string) error
- func (f *File) MoveCheck(ids []string, parent string, d Defaults) (MoveBlock, bool)
- func (f *File) MoveEffects(ids []string, parent string, d Defaults) (MoveEffect, error)
- func (f *File) MoveRoots(ids []string) []string
- func (f *File) MoveTarget(parent string) string
- func (f *File) Open() []Team
- func (f *File) RemoveMember(id, key string) error
- func (f *File) Reopen(id string) error
- func (f *File) Root() (Team, bool)
- func (f *File) SetHandle(id, key, h string) error
- func (f *File) SetHome(key, id string) error
- func (f *File) SetManager(id, key string) error
- func (f *File) SetParent(id, parent string) error
- func (f *File) SetSettings(id string, change func(*Settings)) error
- func (f *File) SetWrap(id string, started time.Time, bound time.Duration) error
- func (f *File) SubTeamCap(parent string, d Defaults) float64
- func (f *File) Team(id string) (Team, bool)
- func (f *File) TopManagers() []Member
- type Hop
- type HueSpec
- type JudgeMove
- type Member
- type MemberState
- type MoveBlock
- type MoveEffect
- type MoveNotice
- type Option
- type Origin
- type Packet
- func Decide(profileDir, id, by, decision, reason string) (Packet, error)
- func Escalate(profileDir, id, by, to, reason string) (Packet, error)
- func OpenPackets(profileDir, scope string) ([]Packet, string, error)
- func PacketByID(profileDir, id string) (Packet, error)
- func Packets(profileDir, teamID string) ([]Packet, error)
- func Raise(profileDir string, p Packet) (Packet, error)
- type Party
- type PoolMove
- type Recommendation
- type Report
- type ReportMove
- type Settings
- type Spend
- type Team
- func (t Team) ByHandle(handle string) (Member, bool)
- func (t Team) Clone() Team
- func (t Team) Closed() bool
- func (t Team) Holds(key string) bool
- func (t Team) HueSpec() HueSpec
- func (t Team) Hued() bool
- func (t Team) MarshalJSON() ([]byte, error)
- func (t Team) Member(key string) (Member, bool)
- func (t *Team) SetHue(h HueSpec)
- func (t *Team) UnmarshalJSON(raw []byte) error
- func (t Team) Wakes() bool
- type Thread
- type Watch
- type Wrap
Constants ¶
const ( PacketQuestion = "question" // a question nobody below could answer PacketConflict = "conflict" // parties disagree; raised by team_raise PacketCap = "cap" // a team reached its daily cap PacketJudgement = "judgement" // a call a manager will not make alone PacketClosing = "closing" // a manager's closing report before a close )
Packet kinds.
const ( PacketOpen = "open" PacketDecided = "decided" PacketEscalated = "escalated" )
Packet states. Escalated is still waiting: it is waiting at Team, which is no longer where it was raised.
const ( OptionClose = "close" // close the team OptionCloseNow = "close-now" // close although the wrap-up did not finish OptionKeepGoing = "keep-going" // do not close OptionRaiseCap = "raise" // raise the cap (the option says to what) OptionStopToday = "stop" // stop the team until tomorrow )
The option ids of a closing packet and a cap packet, which the interface and the session both act on.
const ( StateRunning = "running" StateIdle = "idle" StateAsking = "asking" StateFinished = "finished" StateFailed = "failed" )
Member states a caller reports in MemberState.
const ( HandleByWords = "words" // the word list's instant guess ([DeriveHandle]) HandleByModel = "model" // the title model's word ([File.ChooseHandle]) HandleByTyped = "typed" // given by a person or the manager; never replaced )
Who chose a member's handle (Member.HandleBy).
const ( HandleMin = 2 HandleMax = 12 )
Handle lengths, in characters.
const ( TeamOpen = "open" TeamClosed = "closed" )
Team states.
const ( // MoveBlockSelf is a team asked to go inside itself. MoveBlockSelf = "self" // MoveBlockInside is a team asked to go inside a team under it. MoveBlockInside = "inside" // MoveBlockClosed is a closed target. MoveBlockClosed = "closed" // MoveBlockDepth is a move that would stand a team past the target's // depth limit. MoveBlockDepth = "depth" // MoveBlockHere is a target every moved team is already directly under. MoveBlockHere = "here" // MoveBlockRoot is the root team asked to move (it holds every team). MoveBlockRoot = "root" // MoveBlockGone is a team or a target that is not in the file. MoveBlockGone = "gone" )
The kinds of MoveBlock.
const ( FileName = "teams.json" LegacyFileName = "spaces.json" )
The file's name inside the profile directory, and the first build's.
const ( // OriginTeam is the team's own override. OriginTeam = "team" // OriginAncestor is an override on a team up the chain, named by Team. OriginAncestor = "ancestor" // OriginSettings is the profile's `teams.` default. OriginSettings = "settings" // OriginClosed is a closed team's cap, which is none. OriginClosed = "closed" )
Where a value came from.
const ( KindEvent = "event" // something a conversation did: finished, failed, asked KindNote = "note" // a message from one party to another KindDirective = "directive" // an instruction from the manager or the person KindStop = "stop" // a member was stopped KindStart = "start" // a member was started KindYou = "you" // the person spoke to the team // The delegation kinds (DESIGN.md section 8). KindQuestion = "question" // a member asks its home manager a clarifying question KindAnswer = "answer" // the manager answers one; Reply is the question's id KindPacket = "packet" // a decision packet was raised, decided or escalated KindClose = "close" // the team was closed KindReopen = "reopen" // the team was reopened )
Entry kinds.
const ( FromManager = "manager" FromYou = "you" FromSystem = "system" ToEveryone = "everyone" ToManager = "manager" ToRoom = "room" // ToYou is the person, as the one a question or a packet is put to. ToYou = "you" // ToSeveral is a message to more than one member and fewer than all of // them, by name: [Entry.Handles] lists who (thread.go). ToSeveral = "several" )
Addresses that are not a member's handle.
const HueBand = 25
HueBand is how many degrees either side of a reserved hue no team may take.
const HueTierFree = 6
HueTierFree is how many teams are all drawn at tier 0 before the tiers start to alternate.
const MissingStamp = "-"
MissingStamp is the stamp of a file that does not exist.
const Person = "you"
Person is the decider that is not a team: the person. It is the Team of a packet waiting on them, the by of a decision they made, and a scope.
const QuietAfter = 7 * 24 * time.Hour
QuietAfter is how long a team goes without activity before Organize may propose closing it (ruling c-9's "about seven days").
const RootName = "All teams"
RootName is the root team's name.
const ScopeAll = ""
ScopeAll is every packet still waiting, whoever decides it.
const StateWrapUp = "wrap-up"
StateWrapUp marks a KindDirective as the person's `Wrap up first`.
const Version = 2
Version is the file format this build writes.
const WrapUpText = "Wrap up first: ask everyone to finish and commit, then bring me a closing report."
WrapUpText is the words a wrap-up request carries when the interface gives none.
Variables ¶
var ( ErrDecided = errors.New("teams: that packet was already decided") ErrNoPacket = errors.New("teams: no such packet") ErrNotDecider = errors.New("teams: only the manager it waits on, or you, can decide or escalate it") ErrSideways = errors.New("teams: a packet goes up the tree or to you, never sideways or down") )
Errors a packet write can answer.
var ErrBusy = errors.New("teams: the file is being written by somebody else; try again")
ErrBusy is another process holding the teams file's lock for longer than a write should take. Nothing was written.
var ErrNoManagerAbove = errors.New("teams: no manager above that team to report to")
ErrNoManagerAbove is File.SetHome asked for a membership with no manager anywhere up its chain.
var ErrNotClosing = errors.New("teams: that packet is not a closing report")
ErrNotClosing is AcceptClosing handed a packet that is not a closing report.
var ErrOpen = errors.New("teams: only a closed team can be deleted; close it first")
ErrOpen is Delete asked to delete a team that is not closed.
var ErrParentClosed = errors.New("teams: its parent team is closed; reopen that first")
ErrParentClosed is File.Reopen asked to reopen a team whose parent is closed.
var ErrRoot = errors.New("teams: All teams holds every team; remove its manager or dissolve it instead")
ErrRoot is a change the root cannot take: closing it (dissolve it instead) or putting it under another team.
var ErrSetting = errors.New("teams: a cap is 0 or more, a depth 1 to 10, a share above 0 and at most 1")
ErrSetting is an override outside its band.
var ErrStale = errors.New("teams: the file changed since it was read")
ErrStale is ChangeIf finding the file changed since the stamp it was given. Nothing was written; the caller reads again and makes its change again.
var ErrWrap = errors.New("teams: a wrap-up needs a start and a bound")
ErrWrap is File.SetWrap handed a start or a bound that cannot be resumed.
Functions ¶
func AcceptClosing ¶
AcceptClosing closes the team a decided closing packet was raised from when its decision is OptionClose or OptionCloseNow, records the packet as the team's report, and appends a KindClose line to its Traffic. It reports whether this call closed the team: false for a decision to keep going, for a team already closed (by the other side, or by hand), and for a packet still waiting.
func AppendTraffic ¶
AppendTraffic adds e to the end of team teamID's log, under the log's lock, giving it the next id and, when it has none, the time now.
func AppendTrafficID ¶
AppendTrafficID is AppendTraffic, and the id the entry was given, which a writer that will be answered keeps so the answer can name it.
func DecisionsPath ¶
DecisionsPath is team teamID's packet file.
func Delete ¶
Delete forgets closed team id and every team under it (closed with it, by the cascade): their entries in the teams file, then their Traffic and packet files. It answers the ids it deleted. A team that is open is ErrOpen and nothing is changed; so is one with an open team under it, which only a hand-edited file can have. The conversations are not touched.
THE FILE IS WRITTEN FIRST. A crash between the two leaves directories no team names, which cost a few kilobytes and are harmless; the other order could leave a team whose Traffic was gone.
func DeriveHandle ¶
DeriveHandle is the handle a title suggests, before collisions and reserved words are taken into account.
IT IS THE TITLE'S HEAD NOUN, as nearly as a word list can find it: the last word that is not a function word, a filler word ([fillerWords]) or a fragment, because an English title names its subject last ("checking codeaf branches for qa binary" is about the binary). A generic last word ("bug", "support") gives way to the one before it. A short qualifier right in front of the head rides with it when both fit (@qa-binary, @api-docs). The result is lowercased and cut to HandleMax. A title with no usable word gives "chat"; an empty title gives "".
func Digest ¶
Digest is the text block for team's manager. states is keyed by conversation key; recent is Traffic, oldest first, of which the newest lines that fit are shown. The members always come before any Traffic line; Traffic is cut from the oldest end first, and a digest whose members alone do not fit is cut at the budget with a closing "…". A budget of 0 or less is no limit.
func FallbackHandle ¶
FallbackHandle returns a deterministic, valid handle for a member of t (such as an untitled manager) that is unique within t.
func HueAllowed ¶
HueAllowed reports whether a hue is clear of every reserved band.
func IsRuling ¶
IsRuling reports whether e is a conflict's ruling ([rule]): a directive that carries its packet's id. A session delivers it to the party it names whoever wrote it, and it wakes that party.
func Money ¶
Money is a figure as a person reads it, and it is the figure itself: `$5` for whole dollars, `$5.50` for whole cents, `$1,234.50` in the thousands, and otherwise the shortest spelling that is still the number, `$0.001`, `$0.015`. A positive figure is never spelled `$0.00`. A measured spend goes through RoundMoney first; a cap is spelled as it was chosen.
func NewID ¶
func NewID() string
NewID is a fresh random team id: twelve hex digits, never derived from the name, so a rename is a rename and two teams once called the same are two.
func PacketsStamp ¶
PacketsStamp is one stamp over every team's packet file: one directory listing and a stat per team. It moves when any packet file is written, made or removed.
func ParentLoops ¶
ParentLoops reports whether making parent the parent of id would close a loop: whether id is parent itself or one of parent's ancestors. The walk is bounded by the list, so a loop already in the list cannot hang it.
func Path ¶
Path is where the teams live. An empty profile directory is the ordinary launch and resolves to this process's own profile (config.ProfilePath).
func Quiet ¶
Quiet is every open team in f, the root aside, that Organize may propose closing at now: nothing in its Traffic, its packets or its members' transcripts for idle, and no packet waiting on it or raised from it. It is a proposal's input and closes nothing. It reads one Traffic line and one packet fold per team and stats each member's transcript, so it is asked off the loop, when Organize is.
func RaiseTo ¶
RaiseTo is the ceiling a cap packet offers for a pool at ceiling: twice it, to the millionth, and ALWAYS larger than the ceiling it raises, because a raise to the same figure (a sub-cent cap rounded to the cent was `Raise to $0`) is a button that lifts nothing.
func RoundMoney ¶
RoundMoney is a MEASURED figure (a pool's spend, a share of a cap) rounded the way it is kept and said: to the cent from a cent up, and to a millionth of a dollar below it, so a positive figure under a cent is never rounded away to nothing.
func Save ¶
Save writes teams as the whole file, under the lock. It puts the list in order first, in place (ids, parents, handles, the manager), so the caller's memory and the disk agree afterwards.
IT REPLACES WHATEVER IS ON DISK. A caller that loaded the file a while ago and saves its copy undoes any change another process made in between; a caller that shares the file should change it with Update instead.
func SetAside ¶
SetAside moves an unreadable teams file out of the way, to <name>.unreadable-<nanos> beside it, so that starting again does not overwrite a file that may hold every team a person made. It moves teams.json when there is one and spaces.json otherwise, and returns the new path.
func SpendStamp ¶
SpendStamp is one stamp over what a team's spend is read from: the teams file (membership) and the ledger. Equal stamps on one day are one answer.
func TeamDir ¶
TeamDir is team id's own directory, <profile>/teams/<id>, which holds its Traffic and its decision packets.
func TeamSpendStamp ¶
TeamSpendStamp is the stamp of one team's day as TeamSpend reads it: the day, the team, and SpendStamp over the machine's ledger. The engine and a local window both answer "same" from it.
func ThreadID ¶
ThreadID reads what a model wrote for a thread ("#42", "42", or a whole id) back into an entry id, and false when it is not one.
func ThreadNumber ¶
ThreadNumber is an entry id as a person and a model write it: "#42".
func Told ¶
Told records that a decided packet's answer reached its raiser. A duplicate delivery changes nothing, and the append uses the packet file's own lock.
func TrafficPath ¶
TrafficPath is the log of team id: <profile>/teams/<id>/traffic.jsonl.
func TrafficStamp ¶
TrafficStamp is the stamp of team teamID's current Traffic log. A rotation starts a new current file, so it moves this stamp as an append does.
func Update ¶
Update is the one read-modify-write: under the exclusive lock it loads the file fresh (repaired, and migrated if it has not been), hands it to fn, and saves what fn left. An error from fn writes nothing and is returned. An unreadable file is returned as its error and fn is not called. A file that did not exist and that fn left with no teams is not created.
Teams stay as coloured as they were; a team fn adds without Team.SetHue is coloured by the next LoadHued.
func UsageLedgerPath ¶
func UsageLedgerPath() string
UsageLedgerPath is the machine's usage ledger, the same path internal/session's UsageLedgerPath names.
func ValidHandle ¶
ValidHandle says what is wrong with h as a handle, or nil. A handle is lowercase letters, digits and hyphens, starts with a letter or digit, is HandleMin to HandleMax characters, and is not a reserved address.
func WriteMoveNotices ¶
func WriteMoveNotices(profileDir string, notes []MoveNotice) error
WriteMoveNotices appends each notice to that team's Traffic, once per notice. A refused move hands none, and this writes none.
Types ¶
type CapFacts ¶
type CapFacts struct {
Team string `json:"team"`
Day string `json:"day"`
CapUSD float64 `json:"cap_usd"`
SpentUSD float64 `json:"spent_usd"`
RaiseTo float64 `json:"raise_to"`
}
CapFacts is what a PacketCap packet says about the pool: whose cap it is (Team, the pool's owner), the local day, the cap reached, what the pool had spent when it was raised, and the figure the `raise` option raises it to for the rest of that day. The session reads RaiseTo back when the person picks `raise`, so the amount on the button is the amount that holds.
type ClosingReport ¶
type ClosingReport struct {
Done string `json:"done"`
Left string `json:"left,omitempty"`
Files []string `json:"files,omitempty"`
SpendUSD float64 `json:"spend_usd,omitempty"`
Incomplete bool `json:"incomplete,omitempty"`
}
ClosingReport is what a PacketClosing packet says about the team: what was done, what is left, where the files are and what it spent. Incomplete says the wrap-up ran out of time or money before it finished.
type Defaults ¶
type Defaults struct {
QuestionsUp bool `json:"questions_up"`
CapUSDDay float64 `json:"cap_usd_day"`
DepthLimit int `json:"depth_limit"`
SubShare float64 `json:"sub_share"`
// Wake is whether team traffic wakes idle conversations.
Wake bool `json:"wake"`
}
Defaults are the profile's `teams.` rows as the resolver takes them.
func DefaultsAt ¶
DefaultsAt reads the five rows from profileDir's config.json in one read (config's TeamDefaultsAt). An empty profileDir is the ordinary launch.
type Effective ¶
type Effective struct {
QuestionsUp bool `json:"questions_up"`
QuestionsUpFrom Origin `json:"questions_up_from"`
CapUSDDay float64 `json:"cap_usd_day"`
// CapFrom names the team whose cap this is. A cap is a POOL: a team's
// spend counts every team under it ([TeamSpend]), so an inherited cap is
// the ancestor's one pool, shared, and not a second allowance of the same
// size; the spend to set beside it is CapFrom.Team's.
CapFrom Origin `json:"cap_from"`
DepthLimit int `json:"depth_limit"`
DepthFrom Origin `json:"depth_from"`
// Wake is whether team traffic wakes the team's idle conversations: a
// directive its member, a member's reply or event its manager.
Wake bool `json:"wake"`
WakeFrom Origin `json:"wake_from"`
}
Effective is a team's five settings resolved, each with its origin.
type Entry ¶
type Entry struct {
// ID is assigned by [AppendTraffic]; any ID given is replaced.
ID string `json:"id"`
// At is when it happened; [AppendTraffic] fills a zero one with now.
At time.Time `json:"at"`
// Kind is one of the Kind constants.
Kind string `json:"kind"`
// From is a member's handle, or manager, you or system.
From string `json:"from"`
// To is a member's handle, or everyone, manager or room, or several with
// the handles in Handles.
To string `json:"to"`
Text string `json:"text"`
// Handles is who a message to several members is for, in the order the
// sender named them; empty on every other entry (thread.go).
Handles []string `json:"handles,omitempty"`
// Answers is the id of the entry this one answers, which is what threads
// the log: a member's reply names the manager's message it replies to, and
// the events its turn raises name the same one. Empty on an entry that
// answers nothing, and on every entry written before threads (thread.go).
Answers string `json:"answers,omitempty"`
// Files are paths the entry is about, when it is about any.
Files []string `json:"files,omitempty"`
// Member is the conversation key the entry concerns, when there is one.
Member string `json:"member,omitempty"`
// State is what a [KindEvent] says the member it concerns is now: one of
// the State constants ([StateFinished], [StateFailed], [StateAsking],
// [StateIdle] for a turn that was stopped, [StateRunning] for one that
// carried on after its question was answered). It is empty on every other
// kind but [KindPacket]. A reader colours by it rather than by reading Text, which is the
// words a person reads.
State string `json:"state,omitempty"`
// Packet is the decision packet a [KindPacket] entry is about, and State
// is then the packet's state after the change it records.
Packet string `json:"packet,omitempty"`
// Reply is the id of the [KindQuestion] entry a [KindAnswer] answers.
Reply string `json:"reply,omitempty"`
// Team is, on a [KindStart], the sub-team the started conversation is to
// manage (a manager's `team_start` of kind team): the interface opens and
// adds the member as for any start, and the new conversation, reading its
// brief, makes itself that team's manager. It is empty on every other
// start and every other kind.
Team string `json:"team,omitempty"`
// Approval is, on a [KindStart], the approval posture the started
// conversation inherits from the manager unless overridden.
Approval string `json:"approval,omitempty"`
}
Entry is one line of the Traffic log. This shape is the contract between the interface and the team tools.
func ReadTraffic ¶
ReadTraffic is team teamID's log after the entry with id after, oldest first, from both the current file and the rotated one before it.
It is made for two readers. With after "" it is the TAIL: the last limit entries, for a digest or a first look. With an id it PAGES FORWARD: the first limit entries after that id, so a reader that keeps the last id it saw never skips one. A limit of 0 or less is every entry. A log that does not exist is no entries and no error. It takes no lock; a line still being written is skipped and read next time.
IT READS WHAT IT RETURNS AND LITTLE ELSE. A log is up to two files of [trafficRotateBytes], and a reader asking at every step of a turn must not parse megabytes to learn that nothing is new. So a tail is read backwards from the end in windows until it holds limit entries ([tailTraffic]), and a page forward first asks each file for its last id (one window at its end, [lastIDIn]): a file with nothing past the cursor is not read at all, and one with something is entered where the cursor is, found by a binary search over byte offsets on the ids, which are in file order ([forwardTraffic]). Only a limit of 0 or less reads the files whole.
func WrapUpRequest ¶
WrapUpRequest is the Traffic entry the interface appends to a team's log to ask its manager to wrap up. text may be "" for WrapUpText.
func (Entry) Addressed ¶
Addressed reports whether e is for the member with handle: to that handle, to everyone, or to several of which it is one.
func (Entry) Recipients ¶
Recipients is the members e is addressed to by handle: its one handle, or its several. It is nil for an address that is not a member's (everyone, the room, the manager); a reader that wants everyone's names has the team.
type File ¶
File is the whole of teams.json.
func Change ¶
Change is Update that answers what it wrote and the file's stamp after the write, both taken under the lock, so the caller can hold the list and know which version of the file it is.
func ChangeIf ¶
ChangeIf is Change only while the file is still at stamp base: the compare-and-swap a writer on the far side of a wire needs, because it read the file in one call and writes it in another and the lock cannot be held across the two. A file that moved in between is ErrStale, nothing is written, and fn is not called.
func Load ¶
Load reads the teams from profileDir and puts them in order ([tidy]). It takes no lock and does not wait: a repair it had to make is written back only when the lock is free at that moment, and is otherwise made again on the next load. Teams the file left without a colour stay without one; the interface, which knows the palette, uses LoadHued.
A missing file is an empty File and no error. A file that is there but unreadable is an error and is left exactly as it was.
THE FIRST BUILD'S FILE IS MIGRATED HERE, once. With no teams.json and a spaces.json beside it, the old list is read, written as teams.json, and only then is spaces.json renamed to spaces.json.migrated.
func LoadHued ¶
LoadHued is Load that also gives every uncoloured team a colour around the reserved hues (File.Colour) and writes that back as a repair.
func (*File) AddMember ¶
AddMember puts m into team id after the members it has, and gives it a handle if it has a title. A key already there is left as it is.
func (*File) Ancestors ¶
Ancestors is id's parent, its parent's parent, and so on to the top, nearest first.
func (*File) CanNest ¶
CanNest reports whether a new sub-team may be made under parent: the parent is open and one more level stays inside the parent's effective depth limit.
func (*File) Children ¶
Children is every team whose parent is id, in stored order; "" is the top level.
func (*File) ChooseHandle ¶
func (f *File) ChooseHandle(id, key string, choices []string, title string) (old, now string, err error)
ChooseHandle gives the member with key in team id the title model's word: the first of choices, best first, that no other member of the team has. When every choice is taken, the first is qualified by a word of the title in front of it (@api-security), as DeriveHandle qualifies; only when nothing fits is it numbered. old is the handle the member had and now the one it has; a member whose handle was given (HandleByTyped) or already chosen by the model keeps it, and so does a member whose handle is already the choice.
func (*File) ClearManager ¶
ClearManager leaves team id without a manager. The conversation stays a member.
func (*File) Close ¶
Close closes team id at at, with report the id of its closing report packet ("" for none), and every open team under it. A team already closed is left as it was, and so is a sub-team already closed on its own.
func (*File) ClosedTeams ¶
ClosedTeams is the teams that are closed, most recently closed first, for the folded `Closed · N` section.
func (*File) Colour ¶
Colour gives every team without a colour one from the generator, in file order, around the ones that have theirs, and reports whether it gave any. It depends on nothing but the list and reserved, so the same file is coloured the same way on every load.
func (*File) Depth ¶
Depth is how many levels team id stands at, the top level being 1, and 0 for an id not in the file. The root team (root.go) is not a level: it is 0, and a team directly under it is 1.
func (*File) Descendants ¶
Descendants is every team under id, at any depth, parents before children.
func (*File) DissolveRoot ¶
func (f *File) DissolveRoot()
DissolveRoot removes the root team and puts every team under it back at the top level. A file with no root is left as it is.
func (*File) Effective ¶
Effective resolves team id's settings: each from the team's own override, else the nearest open ancestor's, else d. An id not in the file is d throughout.
func (*File) Home ¶
Home is the manager the conversation with key reports to, false when it has none (it is an ordinary chat).
func (*File) LCA ¶
LCA is the team whose manager decides between the conversations keys (ruling c-6): the lowest team that is at or above some membership of every one of them, is open, and has a manager who is not one of them. The lowest is the deepest; among equally deep teams the first in the file. false is nobody, and then the person decides. One key is that conversation's nearest manager, which for a conversation in one team is its home.
A MANAGER WHO IS A PARTY DOES NOT JUDGE ITS OWN CASE: a team whose manager is one of keys is passed over for the next team up that holds them all.
func (*File) Links ¶
Links is every other manager the conversation with key can be reached by: the nearest manager up each of its memberships, in the pick order, each managed team once, the home left out.
func (*File) MakeRoot ¶
MakeRoot makes the root team, when there is none, and moves every other top-level team under it. It answers the root's id. The caller then makes the global manager with File.AddMember and File.SetManager, in the same write.
func (*File) Move ¶
Move puts every team in ids inside parent ("" the top level), each team carried along inside another moved one left where it is. It checks nothing File.SetParent does not; ask File.MoveCheck first.
func (*File) MoveCheck ¶
MoveCheck reports whether the teams ids may go inside parent ("" the top level), and when not, why. Among several teams the first that cannot go is the answer; a team already under parent is not moved and does not block, unless every one of them is (MoveBlockHere).
func (*File) MoveEffects ¶
MoveEffects is what moving ids inside parent would change, computed on copies of the file tidied as the store's write tidies them. The file is not changed. An error is a move File.SetParent refuses.
func (*File) MoveRoots ¶
MoveRoots is ids with every id that sits under another of them left out, in the order given: a team moved together with its parent goes along inside it and is not moved on its own.
func (*File) MoveTarget ¶
MoveTarget is the id a move into parent really writes: "" is the top level, which is the root team when there is one (root.go keeps every other top-level team under it).
func (*File) RemoveMember ¶
RemoveMember takes the conversation with key out of team id. The team is kept even when it is left empty, and a manager removed is no longer one.
func (*File) Reopen ¶
Reopen opens team id again, and every team under it that its own close closed. Its closing report stays recorded; a team reopened and closed again gets the new one. A team whose parent is closed is ErrParentClosed.
func (*File) SetHandle ¶
SetHandle gives the member with key in team id the handle h. It must be a valid handle (ValidHandle) that no other member of the team has. It is a handle given, never guessed, and nothing replaces it (HandleByTyped).
func (*File) SetHome ¶
SetHome makes the membership of key in team id its home. The membership must exist and have a manager up its chain, else ErrNoManagerAbove.
func (*File) SetManager ¶
SetManager makes the conversation with key team id's manager. A team has one manager, so this replaces any other. A conversation that is not a member is added first, with only its key; call File.AddMember before this to add it with its title and file.
func (*File) SetParent ¶
SetParent puts team id under parent, or at the top level for "". The parent must exist and may not be the team or anything under it.
func (*File) SetSettings ¶
SetSettings changes team id's overrides with change, which sets a field to override it and nils it to reset it to inherit. A result outside a band is ErrSetting and nothing is changed.
func (*File) SetWrap ¶
SetWrap records a wrap-up on team id, once. A wrap-up already recorded is left as it was, so a second request keeps the first clock. started and bound must both be set.
func (*File) SubTeamCap ¶
SubTeamCap is the cap a new sub-team under parent is made with: the parent's effective cap times its effective share, rounded by RoundMoney (to the cent, or finer under a cent, so a sub-cent share is not zero). A parent with no cap gives none (0), and the sub-team then shares whatever pool is above it. The caller writes the answer on the new team (File.SetSettings), so a later change to the share moves no team that exists.
func (*File) TopManagers ¶
TopManagers is the members of the root who manage an open top-level team right now, in the root's member order: the global manager's own members. Anything else the root holds (a manager who stopped being one, a conversation the person put there) is not one of them.
type Hop ¶
type Hop struct {
From string `json:"from"`
To string `json:"to"`
By string `json:"by"`
Reason string `json:"reason,omitempty"`
At time.Time `json:"at"`
}
Hop is one escalation: from the decider it left to the one it went to.
type HueSpec ¶
HueSpec is a team's colour as stored: a hue angle in degrees and a lightness tier, 0 or 1.
func HueChoices ¶
HueChoices is k colours a new team could take, best first: each the farthest from the used hues and from the choices before it.
type JudgeMove ¶
JudgeMove is one moved team whose conflicts another manager decides: Before and After are the deciding teams (File.LCA), "" for the person.
type Member ¶
type Member struct {
Key string `json:"key"`
File string `json:"file"`
Where string `json:"where"`
Word string `json:"word"`
// Handle is the member's short name inside this team (handle.go). It is
// empty only while the member has no title to derive one from.
Handle string `json:"handle,omitempty"`
// Home marks the one membership, among all of this conversation's, that
// names the manager it reports to (home.go): the nearest manager up this
// team's chain. At most one membership of a key carries it, and only one
// with a manager somewhere up its chain. [File.SetHome] moves it; tidy
// picks it when there is none and never moves a valid one.
Home bool `json:"home,omitempty"`
// Started says this membership was made by the team manager's team_start
// (a [KindStart] the interface carried out), which is the second rule a
// home is picked by.
Started bool `json:"started,omitempty"`
// HandleBy is who chose Handle: [HandleByWords] for the word list's
// instant guess, [HandleByModel] for the title model's word, and
// [HandleByTyped] for a handle a person or the manager gave. "" is a
// handle written before this was kept, and is read as the word list's
// ([Member.HandleDerived]).
HandleBy string `json:"handle_by,omitempty"`
}
Member is one conversation a team holds, with enough to reopen it when no window has it open.
func (Member) HandleDerived ¶
HandleDerived reports whether m's handle is the word list's guess, which the title model may replace once (File.ChooseHandle). A handle written before Member.HandleBy was kept reads as one: the only handles then were derived, but for the manager's own starts, which it names again the same way.
type MemberState ¶
type MemberState struct {
// State is one of the State constants.
State string
// SinceActive is how long ago the member last did anything; 0 is not
// known.
SinceActive time.Duration
// Question is the question the member is waiting on a person for, if any.
Question string
// Files are the files the member has touched.
Files []string
// ReportsTo is the name of the team whose manager this member reports to
// when that is not this team (home.go): a shared member, which this
// team's manager may read and send a note to, and not direct. A shared
// member that is running is drawn busy for that team.
ReportsTo string
}
MemberState is what the caller knows about one member right now. Every field may be left empty.
type MoveBlock ¶
type MoveBlock struct {
Kind string
Team string
Name string
Depth int
Need int
Limit int
LimitFrom Origin
}
MoveBlock is why teams cannot go inside a target. Team names the team the reason is about: the moved team for self, inside and root, the target for closed, depth and here. For depth, Depth is how many levels deep the target stands, Need how many levels the moved teams take up (a team with one level of sub-teams under it needs two), Limit the target's effective depth limit and LimitFrom where that limit came from.
type MoveEffect ¶
type MoveEffect struct {
Reports []ReportMove
Pools []PoolMove
Judges []JudgeMove
}
MoveEffect is what a move changes.
func (MoveEffect) Changes ¶
func (e MoveEffect) Changes() bool
Changes reports whether the move changes anything a person is asked about.
type MoveNotice ¶
MoveNotice is one Traffic line a committed move writes: Team is whose log, Entry the line. The store appends it; a frame never does.
func MemberMoveNotices ¶
func MemberMoveNotices(before, after *File) []MoveNotice
MemberMoveNotices is the Traffic for conversations that left one team and joined another between before and after. A team move that only changes a parent is not one of these (that is MoveNotices). A file that did not change membership writes nothing, so a refused transfer writes nothing. Each team left says `@handle moved to harbor`. Each team joined says `@handle joined from ops`.
func MoveNotices ¶
func MoveNotices(before, after *File, ids []string) []MoveNotice
MoveNotices is the Traffic written when ids have moved, read off the file before the move and the file after it. A team whose parent did not change contributes nothing, so a refused move (the file left as it was) writes nothing, and asking twice about the same pair does not invent a second move.
EACH AFFECTED TEAM GETS ONE KindEvent PER MEMBER THAT MOVED WITH THE TEAM. The team that was left, and the moved team, say `@handle moved to harbor`. The team that was joined says `@handle joined from ops`. A manager reads those on its next turn through the ordinary Traffic read. The lines are from codeaf to everyone and carry no member state, so they do not start a wake of their own. A member with no handle is named by the team's name, once, so a team of unnamed conversations is still told.
type Option ¶
type Option struct {
ID string `json:"id"`
Label string `json:"label"`
Consequence string `json:"consequence"`
}
Option is one answer and what happens if it is chosen.
type Origin ¶
type Origin struct {
Kind string `json:"kind"`
Team string `json:"team,omitempty"`
Name string `json:"name,omitempty"`
}
Origin is where one resolved value came from: Kind is one of the Origin constants, and Team and Name name the team that set it for OriginTeam and OriginAncestor.
type Packet ¶
type Packet struct {
// ID is minted by [Raise].
ID string `json:"id"`
// Team is who decides it now: a team id, whose manager decides, or
// [Person].
Team string `json:"team"`
// Origin is the team it was raised from, whose file holds it.
Origin string `json:"origin"`
// Kind is one of the Packet kinds.
Kind string `json:"kind"`
// RaisedBy is the raiser's handle, or [FromManager] or [Person].
RaisedBy string `json:"raised_by"`
// Parties are the sides, each with its own context. A question or a
// judgement may have one.
Parties []Party `json:"parties,omitempty"`
Question string `json:"question"`
Options []Option `json:"options,omitempty"`
// Recommendation is optional; when there is one it names an option.
Recommendation *Recommendation `json:"recommendation,omitempty"`
// Report is a closing packet's report, and nil on every other kind.
Report *ClosingReport `json:"report,omitempty"`
// Cap is a cap packet's facts, and nil on every other kind.
Cap *CapFacts `json:"cap,omitempty"`
// State is one of the Packet states.
State string `json:"state"`
// DecidedBy is the deciding manager's handle, or [Person].
DecidedBy string `json:"decided_by,omitempty"`
// Decision is the chosen option's id, or the person's own words when none
// fitted.
Decision string `json:"decision,omitempty"`
// Told records that the raiser was handed a decided answer.
Told bool `json:"told,omitempty"`
// Reason is the decider's reason, or the last escalation's.
Reason string `json:"reason,omitempty"`
// Trail is every escalation, oldest first.
Trail []Hop `json:"trail,omitempty"`
// Raised is when it was raised, At when it last changed.
Raised time.Time `json:"raised"`
At time.Time `json:"at"`
}
Packet is one decision, as it stands.
func Decide ¶
Decide records by's decision on packet id: an option's id, or the person's own words. by is the handle of the manager of the team the packet waits on, or Person, who may decide any packet. A packet already decided is ErrDecided, and nothing is written.
func Escalate ¶
Escalate sends packet id up: to an open team above the one it waits on, or to Person. by is as for Decide. Down, sideways or to a closed team is ErrSideways; a decided packet is ErrDecided.
func OpenPackets ¶
OpenPackets is every packet waiting on scope (a team id, Person, or ScopeAll for every waiting packet), oldest first, and the stamp of the packet files it was read from: equal stamps are the same answer, so a reader can hand the stamp back and be told nothing moved (PacketsStamp).
func PacketByID ¶
PacketByID is packet id as it stands.
func Packets ¶
Packets is every packet raised from team teamID, decided or not, oldest first: a team's history, and its closing reports.
func Raise ¶
Raise records p as a new packet and answers it as written: with an id, open, raised now. p.Team is who decides (a team id or Person; the caller finds it, with File.LCA for parties or File.Home for a question), and p.Origin the team it is raised from, which must be p.Team or a team under it; an empty Origin is p.Team. An option with no id is given its place ("1", "2", ...). The kind, the question, the raiser, and a label and a consequence on every option are required; a question may have no options (the answer is words), every other kind must have one.
A CAP PACKET IS RAISED ONCE PER CROSSING. The crossing is the pool, the local day and the ceiling ([capCrossing]). The check and the append happen under the decisions file's lock, so two processes that meet the same crossing write one line: the second is handed the packet already there, and no second line of Traffic is written. Any other kind is a new packet every time it is raised.
type Party ¶
type Party struct {
Key string `json:"key"`
Handle string `json:"handle,omitempty"`
Team string `json:"team,omitempty"`
Context string `json:"context,omitempty"`
}
Party is one side of a packet: a conversation, its handle and team, and the context it gave in its own words.
type PoolMove ¶
PoolMove is one moved team whose spend counts toward another capped pool: Before and After are the teams that own the pool above it ("" none), and BeforeCap and AfterCap those pools' daily caps.
type Recommendation ¶
Recommendation is the option the raiser would choose, and why.
type Report ¶
type Report struct {
// Via is the team of the membership the line runs through.
Via string `json:"via"`
// Team is the managed team whose manager this is: Via itself or an
// ancestor of it.
Team string `json:"team"`
// Manager is that manager's conversation key.
Manager string `json:"manager"`
// Distance is how many levels up from Via the manager's team is.
Distance int `json:"distance"`
}
Report is one manager a conversation can be reached by.
type ReportMove ¶
ReportMove is one conversation whose manager a move changes: the manager it reports to before and after, each with whether there is one.
type Settings ¶
type Settings struct {
QuestionsUp *bool `json:"questions_up,omitempty"`
CapUSDDay *float64 `json:"cap_usd_day,omitempty"`
DepthLimit *int `json:"depth_limit,omitempty"`
// Wake is stored as "wake". A file from before wake was inheritable wrote
// only "wake": false (on was written as nothing), and that spelling reads
// here unchanged as an override to off.
Wake *bool `json:"wake,omitempty"`
}
Settings are a team's own overrides. A nil field is unset.
type Spend ¶
type Spend struct {
Team string `json:"team"`
Day string `json:"day"`
USD float64 `json:"usd"`
Calls int `json:"calls"`
// ByMember is each counted conversation's share, by conversation key.
ByMember map[string]float64 `json:"by_member,omitempty"`
}
Spend is what a team and every team under it spent on one day.
type Team ¶
type Team struct {
// ID is random and minted once ([NewID]).
ID string
Name string
// Parent is the id of the team this one sits under, "" at the top level.
Parent string
// Members are the conversations, in the order the person stored them.
Members []Member
// Manager is the conversation key of the member that manages the team, ""
// for none. It is always one of Members.
Manager string
// Hue and Tier are the team's colour (hue.go).
Hue float64
Tier int
Made time.Time
// State is [TeamOpen] or [TeamClosed] (lifecycle.go); the empty string a
// file from before the lifecycle wrote reads as open. ClosedAt is when it
// closed, ClosedWith the id of the team whose close closed it (itself, or
// the ancestor a cascade came from), and Report the id of its closing
// report packet, "" for a team closed without one.
State string
ClosedAt time.Time
ClosedWith string
Report string
// Root marks the one team that holds every other (root.go): the `All
// teams` row, made when the person gives it a manager.
Root bool
// Settings are the team's own delegation overrides (teamsettings.go),
// each unset field inheriting from the parent chain and then the
// profile's `teams.` defaults. They are stored flat on the team.
Settings Settings
// Wrap is a wrap-up in progress (wrap.go): when it started and how long
// it was given. Nil is none, which is also what a file from before the
// field was kept reads as.
Wrap *Wrap
// contains filtered or unexported fields
}
Team is one named set of conversations.
func (Team) Hued ¶
Hued reports whether the team has a colour. A team built in code with a non-zero hue or tier counts as coloured.
func (Team) MarshalJSON ¶
MarshalJSON writes the known fields in their order, then any field a later build wrote, sorted, exactly as it was read.
func (*Team) UnmarshalJSON ¶
UnmarshalJSON reads a team, keeping every field it does not know.
type Thread ¶
type Thread struct {
// Root is the message the thread began with.
Root Entry
// Replies is everything that answers it, oldest first.
Replies []Entry
// Latest is the id of the newest entry in the thread, which is what orders
// threads by activity.
Latest string
}
Thread is one message and everything that answered it, directly or through another answer.
func Threads ¶
Threads is entries (oldest first, as ReadTraffic gives them) grouped into threads, the thread with the newest activity FIRST and the entries inside each in the order they were written. An entry answering an id that is not among entries (older than the window, or never written) begins a thread of its own, as does every entry that answers nothing.
type Watch ¶
type Watch struct {
// contains filtered or unexported fields
}
Watch reads Traffic logs for a reader that comes back with the cursor it was last given, and answers "nothing new" from one stat when the log has not moved since. It is the reading a clock makes once a second while a managed team is held, locally and on the engine's side of a wire, and it is safe for several goroutines at once. The zero value is ready.
IT REMEMBERS ONE MARK PER LOG: the stamp the log had just before the last read, and the cursor that read left the reader at. A read asked from that cursor with the stamp unchanged cannot find anything, so it is not made. The stamp is taken BEFORE the read, so a line written while the read was going on moves the stamp past the mark and is found on the next turn.
func (*Watch) Traffic ¶
Traffic is ReadTraffic for team teamID, after the cursor after, at most limit entries, with the log's stamp; a read from the cursor the last one left, of a log that has not moved, answers no entries without reading. A tail (after "") or an unlimited read is always made.
type Wrap ¶
type Wrap struct {
// Started is when the wrap-up began.
Started time.Time `json:"started"`
// Bound is how long it was given. It is stored as a number of nanoseconds,
// which is how a duration is written in JSON, so an old reader that does
// not know the field still leaves it alone.
Bound time.Duration `json:"bound"`
}
Wrap is one team's wrap-up in progress.