teams

package
v0.5.1-rc.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
const (
	StateRunning  = "running"
	StateIdle     = "idle"
	StateAsking   = "asking"
	StateFinished = "finished"
	StateFailed   = "failed"
)

Member states a caller reports in MemberState.

View Source
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).

View Source
const (
	HandleMin = 2
	HandleMax = 12
)

Handle lengths, in characters.

View Source
const (
	TeamOpen   = "open"
	TeamClosed = "closed"
)

Team states.

View Source
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.

View Source
const (
	FileName       = "teams.json"
	LegacyFileName = "spaces.json"
)

The file's name inside the profile directory, and the first build's.

View Source
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.

View Source
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.

View Source
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.

View Source
const HueBand = 25

HueBand is how many degrees either side of a reserved hue no team may take.

View Source
const HueTierFree = 6

HueTierFree is how many teams are all drawn at tier 0 before the tiers start to alternate.

View Source
const MissingStamp = "-"

MissingStamp is the stamp of a file that does not exist.

View Source
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.

View Source
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").

View Source
const RootName = "All teams"

RootName is the root team's name.

View Source
const ScopeAll = ""

ScopeAll is every packet still waiting, whoever decides it.

View Source
const StateWrapUp = "wrap-up"

StateWrapUp marks a KindDirective as the person's `Wrap up first`.

View Source
const Version = 2

Version is the file format this build writes.

View Source
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

View Source
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.

View Source
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.

View Source
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.

View Source
var ErrNotClosing = errors.New("teams: that packet is not a closing report")

ErrNotClosing is AcceptClosing handed a packet that is not a closing report.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

func AcceptClosing(profileDir string, p Packet) (bool, error)

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

func AppendTraffic(profileDir, teamID string, e Entry) error

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

func AppendTrafficID(profileDir, teamID string, e Entry) (string, error)

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

func DecisionsPath(profileDir, teamID string) string

DecisionsPath is team teamID's packet file.

func Delete

func Delete(profileDir, id string) ([]string, error)

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

func DeriveHandle(title string) string

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

func Digest(team Team, states map[string]MemberState, recent []Entry, budget int) string

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

func FallbackHandle(t Team, key string) string

FallbackHandle returns a deterministic, valid handle for a member of t (such as an untitled manager) that is unique within t.

func HueAllowed

func HueAllowed(h float64, reserved []float64) bool

HueAllowed reports whether a hue is clear of every reserved band.

func HueGap

func HueGap(a, b float64) float64

HueGap is the distance between two hues around the circle, 0..180.

func Index

func Index(teams []Team, id string) int

Index is where the team with id sits in teams, -1 when it is not there.

func IsRuling

func IsRuling(e Entry) bool

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 IsWrapUp

func IsWrapUp(e Entry) bool

IsWrapUp reports whether e is the person's wrap-up request.

func Money

func Money(usd float64) string

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

func PacketsStamp(profileDir string) string

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

func ParentLoops(teams []Team, id, parent string) bool

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

func Path(profileDir string) string

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

func Quiet(profileDir string, f *File, now time.Time, idle time.Duration) ([]string, error)

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

func RaiseTo(ceiling float64) float64

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

func RoundMoney(usd float64) float64

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

func Save(profileDir string, teams []Team) error

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

func SetAside(profileDir string) (string, error)

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

func SpendStamp(profileDir, ledger string) string

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 Stamp

func Stamp(profileDir string) string

Stamp is the teams file's stamp in profileDir.

func TeamDir

func TeamDir(profileDir, teamID string) string

TeamDir is team id's own directory, <profile>/teams/<id>, which holds its Traffic and its decision packets.

func TeamSpendStamp

func TeamSpendStamp(profileDir, teamID, day string) string

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

func ThreadID(s string) (string, bool)

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

func ThreadNumber(id string) string

ThreadNumber is an entry id as a person and a model write it: "#42".

func TierFor

func TierFor(n int) int

TierFor is the tier the n-th team (from zero) is drawn at.

func Today

func Today() string

Today is today's local day in the ledger's spelling.

func Told

func Told(profileDir, id string) error

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

func TrafficPath(profileDir, teamID string) string

TrafficPath is the log of team id: <profile>/teams/<id>/traffic.jsonl.

func TrafficStamp

func TrafficStamp(profileDir, teamID string) string

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

func Update(profileDir string, fn func(*File) error) error

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

func ValidHandle(h string) error

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 is a fraction, the row's whole percentage over 100.
	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

func DefaultsAt(profileDir string) Defaults

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"`
	SubShare     float64 `json:"sub_share"`
	SubShareFrom Origin  `json:"sub_share_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

func ReadTraffic(profileDir, teamID string, after string, limit int) ([]Entry, error)

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

func WrapUpRequest(text string) Entry

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

func (e Entry) Addressed(handle string) bool

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

func (e Entry) Recipients() []string

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.

func (Entry) Wake

func (e Entry) Wake() bool

Wake reports whether e says a turn was started by team traffic: `woke @web`, `woke ◆`. It is the cause of a state and never news of its own, so a reader draws the state (a member working) rather than the line.

type File

type File struct {
	Version int
	Teams   []Team
}

File is the whole of teams.json.

func Change

func Change(profileDir string, fn func(*File) error) (*File, string, error)

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

func ChangeIf(profileDir, base string, fn func(*File) error) (*File, string, error)

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

func Load(profileDir string) (*File, error)

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

func LoadHued(profileDir string, reserved []float64) (*File, error)

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

func (f *File) AddMember(id string, m Member) error

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

func (f *File) Ancestors(id string) []Team

Ancestors is id's parent, its parent's parent, and so on to the top, nearest first.

func (*File) CanNest

func (f *File) CanNest(parent string, d Defaults) bool

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

func (f *File) Children(id string) []Team

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

func (f *File) ClearManager(id string) error

ClearManager leaves team id without a manager. The conversation stays a member.

func (*File) ClearWrap

func (f *File) ClearWrap(id string) error

ClearWrap forgets team id's wrap-up. A team with none is left as it was.

func (*File) Close

func (f *File) Close(id string, at time.Time, report string) error

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

func (f *File) ClosedTeams() []Team

ClosedTeams is the teams that are closed, most recently closed first, for the folded `Closed · N` section.

func (*File) Colour

func (f *File) Colour(reserved []float64) bool

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

func (f *File) Depth(id string) int

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

func (f *File) Descendants(id string) []Team

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

func (f *File) Effective(id string, d Defaults) 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

func (f *File) Home(key string) (Report, bool)

Home is the manager the conversation with key reports to, false when it has none (it is an ordinary chat).

func (*File) LCA

func (f *File) LCA(keys ...string) (Team, bool)

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 (f *File) Links(key string) []Report

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

func (f *File) MakeRoot(at time.Time) string

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

func (f *File) Move(ids []string, parent string) error

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

func (f *File) MoveCheck(ids []string, parent string, d Defaults) (MoveBlock, bool)

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

func (f *File) MoveEffects(ids []string, parent string, d Defaults) (MoveEffect, error)

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

func (f *File) MoveRoots(ids []string) []string

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

func (f *File) MoveTarget(parent string) string

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) Open

func (f *File) Open() []Team

Open is the teams that are open, in stored order.

func (*File) RemoveMember

func (f *File) RemoveMember(id, key string) error

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

func (f *File) Reopen(id string) error

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) Root

func (f *File) Root() (Team, bool)

Root is the root team, false when there is none.

func (*File) SetHandle

func (f *File) SetHandle(id, key, h string) error

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

func (f *File) SetHome(key, id string) error

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

func (f *File) SetManager(id, key string) error

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

func (f *File) SetParent(id, parent string) error

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

func (f *File) SetSettings(id string, change func(*Settings)) error

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

func (f *File) SetWrap(id string, started time.Time, bound time.Duration) error

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

func (f *File) SubTeamCap(parent string, d Defaults) float64

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) Team

func (f *File) Team(id string) (Team, bool)

Team is the team with id.

func (*File) TopManagers

func (f *File) TopManagers() []Member

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

type HueSpec struct {
	Hue  float64
	Tier int
}

HueSpec is a team's colour as stored: a hue angle in degrees and a lightness tier, 0 or 1.

func HueChoices

func HueChoices(used []HueSpec, reserved []float64, k int) []HueSpec

HueChoices is k colours a new team could take, best first: each the farthest from the used hues and from the choices before it.

func NextHue

func NextHue(used []HueSpec, reserved []float64) HueSpec

NextHue is the colour for a new team beside the used ones: the allowed whole degree farthest from every used hue, the lowest such degree on a tie.

type JudgeMove

type JudgeMove struct {
	Team          string
	Before, After string
}

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

func (m Member) HandleDerived() bool

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

type MoveNotice struct {
	Team  string
	Entry Entry
}

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.

func (Origin) Inherited

func (o Origin) Inherited() bool

Inherited reports whether the value is not the team's own.

func (Origin) Words

func (o Origin) Words() string

Words is the dim words an inherited value is drawn with, "" for the team's own: `from Settings`, `from harbor`, `closed`.

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

func Decide(profileDir, id, by, decision, reason string) (Packet, error)

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

func Escalate(profileDir, id, by, to, reason string) (Packet, error)

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

func OpenPackets(profileDir, scope string) ([]Packet, string, error)

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

func PacketByID(profileDir, id string) (Packet, error)

PacketByID is packet id as it stands.

func Packets

func Packets(profileDir, teamID string) ([]Packet, error)

Packets is every packet raised from team teamID, decided or not, oldest first: a team's history, and its closing reports.

func Raise

func Raise(profileDir string, p Packet) (Packet, error)

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.

func (Packet) Option

func (p Packet) Option(id string) (Option, bool)

Option is the option with id.

func (Packet) Waiting

func (p Packet) Waiting() bool

Waiting reports whether the packet still needs a decision.

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

type PoolMove struct {
	Team                string
	Before, After       string
	BeforeCap, AfterCap float64
}

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

type Recommendation struct {
	Option string `json:"option"`
	Reason string `json:"reason"`
}

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

type ReportMove struct {
	Key           string
	Before, After Report
	Had, Has      bool
}

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"`
	SubShare    *float64 `json:"sub_share,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.

func (Settings) Empty

func (s Settings) Empty() bool

Empty reports whether the team overrides nothing.

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.

func TeamSpend

func TeamSpend(profileDir, teamID, day string) (Spend, error)

TeamSpend is team teamID's spend on day ("2006-01-02", local) from this machine's usage ledger.

func TeamSpendIn

func TeamSpendIn(profileDir, ledger, teamID, day string) (Spend, error)

TeamSpendIn is TeamSpend against the ledger at ledger.

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) ByHandle

func (t Team) ByHandle(handle string) (Member, bool)

ByHandle is the member with handle.

func (Team) Clone

func (t Team) Clone() Team

Clone is a copy of t that shares nothing with it.

func (Team) Closed

func (t Team) Closed() bool

Closed reports whether the team is closed.

func (Team) Holds

func (t Team) Holds(key string) bool

Holds reports whether key is one of the team's members.

func (Team) HueSpec

func (t Team) HueSpec() HueSpec

HueSpec is the team's colour.

func (Team) Hued

func (t Team) Hued() bool

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

func (t Team) MarshalJSON() ([]byte, error)

MarshalJSON writes the known fields in their order, then any field a later build wrote, sorted, exactly as it was read.

func (Team) Member

func (t Team) Member(key string) (Member, bool)

Member is the member with key.

func (*Team) SetHue

func (t *Team) SetHue(h HueSpec)

SetHue gives the team a colour.

func (*Team) UnmarshalJSON

func (t *Team) UnmarshalJSON(raw []byte) error

UnmarshalJSON reads a team, keeping every field it does not know.

func (Team) Wakes

func (t Team) Wakes() bool

Wakes reports whether the team's OWN setting leaves waking on: true unless the team itself says "wake": false. It does not walk the chain; whether team traffic really wakes a conversation is Effective.Wake, which inherits.

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

func Threads(entries []Entry) []Thread

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

func (w *Watch) Traffic(profileDir, teamID, after string, limit int) ([]Entry, string, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL