crew

package
v0.0.0-...-09d6bc6 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package crew is the work: sprints, the tasks in them, the deliverables analysts write, and the operator's stamp that makes one count.

The rule the whole plane is built around: an analyst proposes and only a person disposes. A deliverable exists as a draft the moment it is written, and changes nothing until somebody stamps it. Returning one requires a reason, for the same argument the anomaly plane makes: a rejection with no reason cannot be told apart from nobody having read it.

Index

Constants

View Source
const ExplainerSchema = `` /* 263-byte string literal not displayed */
View Source
const RosterSchema = `` /* 335-byte string literal not displayed */
View Source
const Schema = `` /* 1019-byte string literal not displayed */

tasks.owner is who answered for the agent when the charge was made.

Stamped once, at the charge, and moved only while the work is still open. Without it, spend has to be read from the agent's CURRENT owner, and an agent changing hands rewrites history for both people: the new owner's total jumps by an amount they never authorised and the previous owner's drops by the same.

No SQL comments in this string. The driver executes it as one multi-statement exec and a "--" comment inside it silently breaks the statement it sits in: the column vanished from the CREATE TABLE and the failure surfaced two statements later as "no such column: owner" on the index.

Variables

View Source
var (
	ErrNotFound   = fmt.Errorf("no such task")
	ErrNeedReason = fmt.Errorf("this decision needs a reason")
	ErrSettled    = fmt.Errorf("this is already posted")
)
View Source
var (
	Cadences = []string{"daily", "weekly", "fortnightly", "monthly", "on-request"}
	States   = []string{"active", "suspended", "restricted", "probation", "onboarding"}
	Rights   = []string{
		"figures-read", "sql-readonly", "budgets-read",
		"propose-only", "close-covered", "channel-post", "publish-explainer",
		"export-data", "kpi-registry",
	}
	SkillPool = []string{
		"variance-commentary", "anomaly-triage", "driver-classification",
		"rightsizing-analysis", "commitment-modelling", "forecasting-commentary",
		"forecast-accuracy", "unit-economics", "exec-reporting",
		"showback-narration", "capacity-estimation", "ai-spend-analysis",
		"licence-reconciliation", "allocation-rules", "period-close",
	}
)
View Source
var AttestationNeeds = map[string]string{
	"none":        "",
	"oidc":        "the issuer URL that mints the token, e.g. https://login.example.com",
	"spiffe-svid": "the SPIFFE ID, e.g. spiffe://example.com/ns/finops/sa/triage",
	"enclave-key": "the measurement or key id the enclave publishes",
	"mtls-cert":   "the certificate's SHA-256 fingerprint, or its subject DN",
}

AttestationNeeds says what evidence a method has to carry, in the words a person filling in the form needs rather than the spec's.

View Source
var Attestations = []string{"none", "oidc", "spiffe-svid", "enclave-key", "mtls-cert"}

Attestations is what the Agent Passport spec allows.

View Source
var ErrHasWork = errors.New("this analyst still has open work")

ErrHasWork is returned when an analyst still has work on the board.

View Source
var ErrSuspended = errors.New("this analyst is suspended")

ErrSuspended is returned when work is handed to an analyst whose mandate has been withdrawn.

View Source
var Owners = []string{
	"y.mercer",
	"t.langley",
	"a.whitfield",
	"j.ashby",
	"j.calder",
}

Owners is who answers for the agents in this demo estate.

An initial, a dot and a surname, all lower case, because first names alone stop working the moment there are two Johns and an estate that grows owners is exactly where this page earns its place. Two of these ARE two Johns, so the shape is doing its job in the fixture rather than only in principle.

What is written here is what is shown: there is no separate display form, so there is nothing that can drift into disagreeing about who somebody is, and nothing to decide about capitals on one page and not another.

The surnames are invented and deliberately not the real ones belonging to the people whose first names these are. Fixture data ends up in screenshots, and a screenshot is a poor place to publish somebody's family name.

Functions

func ActiveNames

func ActiveNames(db *sql.DB) ([]string, error)

ActiveNames is the rota: who can actually be given work.

func Approve

func Approve(db *sql.DB, p Plan) (int, error)

Approve materialises a plan onto the board. This is the only thing that creates the tasks, and it is a person's act.

func Assign

func Assign(db *sql.DB, id int, analyst string) error

Assign hands a task to an analyst and starts it.

func AssignableTo

func AssignableTo(db *sql.DB, analyst string) error

AssignableTo refuses to hand new work to a suspended analyst.

The task page builds its dropdown from ActiveNames, so nobody clicking through the console can pick a suspended name. That is the UI, and the UI is not the rule: this function is, and without it a form post carrying a name the dropdown never offered put the task on the board anyway.

It refuses suspension and nothing else, deliberately. Suspension is the one state where RightsFor hands back nothing at all and the live runner refuses to price the task, so those three agree on one meaning: the mandate is withdrawn. Probation, restricted and onboarding are narrower authority, not withdrawn authority, and an analyst on probation that could be given no work could never come off it.

func BackfillMandate

func BackfillMandate(db *sql.DB, owner string) (int, error)

BackfillMandate fills in what an older seeding left blank.

It only ever writes into an EMPTY column, so an analyst somebody hired or re-briefed through the console keeps every word of what was decided about it. An installation that has been running since before this existed should gain the mandate without having its roster replaced.

func Block

func Block(db *sql.DB, id int, reason string) error

Block stops a task, and insists on why. A blocked task with no reason is indistinguishable from one nobody picked up.

func CheckGuards

func CheckGuards(db *sql.DB, month string, rec Recorder) (past int, by money.Cents, err error)

CheckGuards says, out loud, which analysts went past the guard they were given this month.

The console has always been able to SHOW this: the crew page counts it and the card draws a bar. Nothing said it to anybody else, so an operator who was not looking at the page never learned, and the estate's own alerting could not tell them because the event did not exist.

It reports rather than enforces, and the event it emits says so. The stack's budget_exhausted carries a denied verdict, and this console denies nothing; budget_threshold is a warning with no verdict, which is what is true. Making the guard actually bite is a proxy's job, not a console's, and pretending otherwise in a record somebody audits would be worse than not recording it.

Idempotent by the caller: it takes the month, so running it twice for the same month emits twice. The caller decides when a month is worth saying again, because that is a question about notification and not about money.

func ClearFabricated

func ClearFabricated(db *sql.DB) (int, error)

ClearFabricated removes attestations this console invented.

A migration and an apology. Twelve agents on an installation seeded before today carry `oidc` or `spiffe-svid` chosen from their permission list, with no issuer, no SPIFFE ID and nothing that attested anything. Leaving them would leave an identity graph believing them.

It only clears a method with NO detail, so an attestation somebody actually recorded, with its evidence, survives untouched. That is the whole test for whether a claim was made by a person or by the code that used to guess.

func CloseSprint

func CloseSprint(db *sql.DB, id int) (int, error)

CloseSprint stops a sprint accepting new work. Open tasks are NOT closed with it: work that did not finish did not finish, and a sprint that tidies itself up on the way out hides exactly the thing a retrospective needs.

func Comment_

func Comment_(db *sql.DB, task int, author, body string) error

Comment adds a note to the thread. Notes are not decisions and need no reason, which is exactly why they are separate from the states above.

func Commission

func Commission(db *sql.DB, team, topic, audience, author string, amount money.Cents) (int, error)

Commission asks an analyst for a story and drafts it immediately.

The draft is written here rather than by a model because the console must work with no engine configured at all: an empty page that says "run an agent" teaches nobody what an explainer is for. With an engine attached this is what the agent replaces.

func CountsForAttestation

func CountsForAttestation(a Analyst) bool

CountsForAttestation excludes the agents this question does not apply to.

A suspended agent is not running, so what its identity is bound to is not a live exposure. It is left out of the denominator as well as the numerator, because counting it in only one is how a percentage starts lying.

func DeskOwners

func DeskOwners() map[string]string

ownerOfDesk maps a desk to the person who answers for its agents.

By desk, because that is the unit the estate is already organised in: an owner who holds half of one desk and a third of another answers for nothing anybody can hold a conversation about. The AI desk stays with the installation's owner, since it is the crew running the console itself. DeskOwners is ownerOfDesk, for a test that needs to know which desks this list claims to cover. Returned as a copy: a caller that could edit the map could silently re-home half the estate.

func Desks

func Desks(db *sql.DB) ([]string, error)

Desks lists the desks that actually have work, in a stable order.

func DropRetiredRights

func DropRetiredRights(db *sql.DB) (int, error)

DropRetiredRights removes a right this console no longer has anything behind.

`requests-read` named an intake queue of questions people had asked. No such table was ever built, and the decision has been made not to build one, so nine agents held a permission to read a thing that does not exist. It read as a capability on their card, it travelled into the identity graph as a tool, and it could never be exercised or refused.

The rights list is a claim about what an agent can reach. A claim about something absent is the same fault as an attestation nothing attested, in a smaller coat.

func EnsureArtifactProvenance

func EnsureArtifactProvenance(db *sql.DB) error

A deliverable says whether a person's money bought it.

The estate ships 279 generated drafts so that a new installation has something to review. A live run adds real ones, written by a real model on a real key, and the seeded and the real land in the same table, with the same author, the same state and the same shape.

Two kinds of number under one heading is the exact fault this console spends its time catching in other people's data, and it does not get an exemption for its own. `docs/live-agents.md` named this before any of it was built:

a live run does not write into the seeded estate. It writes its own rows,
marked, and every page that sums them says which kind it is summing.

The runner was built without the marker anyway, and 63 real drafts sat indistinguishable among 342 for one run. This is that marker.

'fixture' is the default for the same reason the column is NOT NULL: every row that predates this column was generated, and a row whose provenance is unknown must not read as evidence of a real call.

func EnsureLiveSpendLedger

func EnsureLiveSpendLedger(db *sql.DB) error

The ledger must not overstate what was spent.

tasks.spent_cents is the ledger's unit and stays so. The trouble is that one model call costs a FRACTION of a cent, and rounding each one up on its own turns 44 calls of about half a cent into 44 whole cents.

@measured, a full run on 2026-08-24: the router billed 0.2337 and the console recorded 0.56. Overstated by 140%, on the page whose heading is what the crew cost. A console that exists to catch exactly this in somebody else's data does not get to do it in its own.

So the true amount accumulates here in micro-dollars and the cents are worked out ONCE, over the whole run, in SettleLiveSpend.

Rounding per TASK was the first attempt and it fixed nothing, because the runner makes one call per task: 44 tasks each rounded their own half-cent up and the total was 0.44 again. The test that passed used 44 calls on a single task, which is not how anything works. A test can only prove what it describes.

func EnsureOwnershipHistory

func EnsureOwnershipHistory(db *sql.DB) error

EnsureOwnershipHistory records, on the charge itself, who answered for the agent that ran it.

Before this, tasks carried only the assignee. Everything about ownership was then read from the agent's CURRENT owner, which made an agent that changed hands rewrite history: the new owner's lifetime figure jumped by an amount they had never authorised, and the previous owner's dropped by the same, so "what has this person spent" had no stable answer. The console could not say who owned an agent in July, and no wording on the page could fix it, because the fact was never written down.

The column is stamped when the charge is made and is not rewritten afterwards, which is what makes it history rather than a second copy of the same mutable field. A transfer moves it on OPEN work only, matching the desk and matching what a transfer means: the new owner takes on what is running, not what is finished.

func FromAnomaly

func FromAnomaly(db *sql.DB, anomalyID, title, goal, assignee, desk string, budget money.Cents) (int, error)

FromAnomaly opens a task to investigate one anomaly, and records where it came from so the two are joined in both directions.

func Hire

func Hire(db *sql.DB, a Analyst) error

Hire creates an analyst and its governance identity in ONE act.

The name is constrained because it becomes an agent:// URI, and a URI is a contract other services parse. Letting a form produce "Reporter (AWS desk)" as an identifier is how a shared event stream fills with ids nobody can match.

func IsUnattested

func IsUnattested(a Analyst) bool

IsUnattested is the single predicate behind every count of this on every page.

It is exported and shared rather than reimplemented per page because the two implementations that existed disagreed: one asked whether the attestation was well FORMED, which "none" is, and reported zero unbound agents on an estate where none of them was bound to anything. A page that answers a security question with a reassuring number nobody can reconcile is worse than a page that does not answer it.

func LiveSpend

func LiveSpend(db *sql.DB) (micros int64, tasks int, err error)

LiveSpend is what the crew's own model calls have actually cost.

The estate's crew figures are generated: 3871.35 across 310 tasks, none of it real money. A live run adds real charges to the same column, and a reader with one figure in front of them cannot tell which part somebody paid for.

Returned in micro-dollars, because that is what it is stored in and because rounding it here would repeat the mistake this file exists to record. The caller rounds once, for display.

func LiveSpendBy

func LiveSpendBy(db *sql.DB, analyst string) (micros int64, tasks int, err error)

LiveSpendBy is what one analyst's own model calls have cost.

Per-analyst rather than a share of the total, because the card is about one agent and a figure divided evenly is a figure nobody measured.

func OpenWork

func OpenWork(db *sql.DB, name string) (int, error)

OpenWork counts what an analyst still owes, so a caller can say what has to happen before it can be removed rather than just refusing.

func OwnerOf

func OwnerOf(db *sql.DB, agent string) string

OwnerOf is the owner to stamp on a charge for this agent right now.

Read at the moment of the charge, not passed in by the caller, because a caller holding a stale Analyst would stamp a stale owner and the history would record a transfer that had already happened as though it had not.

func Post

func Post(db *sql.DB, artifactID int, stamper string) error

Post is the stamp. It is the only thing that makes a deliverable count, and it is a person's act rather than an analyst's.

func Publish

func Publish(db *sql.DB, id int, by string) error

Publish is the stamp: an explainer only reaches a team once a person has read it. Recorded as the PERSON's act, not the analyst's.

func RealMoney

func RealMoney(micros int64, tasks int) string

RealMoney is the sentence three pages carry, written once.

The crew page, the KPI library and the agent card all show a cost that mixes generated spend with real spend. Three copies of a sentence about the same fact is how two pages end up disagreeing about it: this console has found four such disagreements and every one looked plausible from one side.

Empty when nothing real has been spent, so a console where no agent has run does not carry a sentence about money nobody spent. The KPI library is the reason that matters: its own headline is that a library where everything reports a number is one where several are invented.

func Rebrief

func Rebrief(db *sql.DB, a Analyst) error

Rebrief changes what an analyst is for. Guards and identity are re-checked, because an edit that could not have been hired is one that should not stand.

func Remove

func Remove(db *sql.DB, name, by string) error

Remove takes an analyst off the roster.

Three things it refuses, and each is a way a console loses track of money:

  • an analyst with open work. The work does not disappear with it, it becomes work assigned to a name nobody can open, and the crew page then reports spend against an agent that is not on the roster. Reassign or transfer first, and the error says which.
  • an analyst that other analysts act under. Removing it orphans them, and a delegation chain with a missing link is a chain that proves nothing.
  • the supervisor, which every default parent points at.

What it does NOT do is delete the analyst's history. Its finished tasks, its artifacts and its journal entries stay exactly where they are: an agent being taken off the rota does not unspend what it spent, and a console that tidied that away would be one whose totals changed when somebody resigned.

func Return

func Return(db *sql.DB, artifactID int, reason string) error

Return sends a deliverable back, and the reason is the whole point: it is what the analyst is meant to act on.

func ReturnExplainer

func ReturnExplainer(db *sql.DB, id int, reason string) error

ReturnExplainer sends it back with what has to change.

func RightsFor

func RightsFor(skills []string, state string) []string

RightsFor is the union of what an analyst's skills need, sorted so the same crew always produces the same document.

A SUSPENDED agent gets nothing. Leaving its rights in place would put a card on the screen that says it may read the figures while the console refuses every request it makes, and the card is the thing people trust.

func Scoreboards

func Scoreboards(db *sql.DB) (map[string]Scoreboard, error)

func Seed

func Seed(db *sql.DB, anomalies []AnomalySeed) (sprints, tasks, artifacts int, err error)

Seed fills the board with the work a crew this size would have done.

A console that opens on an empty board teaches nobody anything: the states that matter are the unhappy ones, and they only exist in a history. So the fixture carries blocked work, returned work, work over its guard, and an analyst on probation whose first-pass rate is genuinely poor rather than asserted.

Deterministic by construction: every choice comes from a hash of the thing being decided, never from a sequential generator, so the board is identical on every machine and does not depend on iteration order.

func SeedOwners

func SeedOwners(db *sql.DB, mk AccountMaker, seededBy string) (accounts, moved int, err error)

SeedOwners gives the estate more than one person to answer for it.

The accounts are created with a password nobody holds: 32 random bytes, discarded immediately. An owner has to be an account that exists, because an agent owned by a name nobody can sign in as is an agent nobody answers for, but that is not a reason to put five signable accounts with known passwords on somebody's machine. An admin gives one a real password with -set-password when a person actually needs to sign in.

Both halves are idempotent and neither overrides a decision already made: an existing account is left alone, and an agent is only moved if it is still owned by whoever seeded it.

func SeedRoster

func SeedRoster(db *sql.DB, owner string) (int, error)

SeedRoster copies the fixture's crew into the store, once.

func SeededOwner

func SeededOwner(configured string) string

SeededOwner is the name SeedRoster stamps on a fresh roster.

Exported so the two cannot drift: SeedRoster substitutes "unclaimed" for an empty owner, and SeedOwners then looked for the empty string, matched nothing, and left every agent unplaced on any installation started without -stack-owner. Which is every installation somebody tries for the first time.

func SetState

func SetState(db *sql.DB, name, state, reason string) error

SetState takes an analyst off the rota, or puts it back.

Anything other than active REQUIRES a reason, and the reason is shown on the card. Suspension does not touch a single thing the analyst already did: it is a pause, never an undo.

func SettleLiveSpend

func SettleLiveSpend(db *sql.DB) (booked money.Cents, err error)

SettleLiveSpend turns the run's true cost into whole cents, once.

Cents cannot hold a fifth of a cent, and one model call costs about that. Rounding each call up recorded 0.56 for a run that billed 0.2337; rounding each TASK up recorded the same, because there is one call per task. The only unit where the arithmetic can be right is the whole run.

So: the run's exact total is rounded up ONCE, and those cents are handed out by largest remainder. A task that cost 0.4 of a cent may end up showing nothing, and the task beside it shows a whole cent; what is guaranteed is that the column adds up to what was actually billed, which is the figure a person reads.

Idempotent, and correct across several runs: live_cents records what has already been booked, so this only ever writes the difference.

func SpendByOwner

func SpendByOwner(db *sql.DB, period string) (map[string]money.Cents, error)

SpendByOwner is what each person answers for, by the owner recorded on the charge rather than by who owns the agent today.

period is a month like "2026-07", or "" for everything since the board opened.

func SpendInMonth

func SpendInMonth(db *sql.DB, period string) (map[string]money.Cents, error)

SpendInMonth is what each analyst's work cost inside one month.

The guard on an analyst is MONTHLY, and its Scoreboard.Spent is everything it has ever been charged with. Setting one against the other says most of the crew is over budget when the truth is that the board covers five months and the guard covers one. It is the same mistake as comparing a part-month bill with a whole-month budget, from the other end.

The month comes from the sprint the work sat in, because that is when the work was done. A task with no sprint has no month and is left out rather than being charged to whichever month somebody is looking at.

func TaskOfArtifact

func TaskOfArtifact(db *sql.DB, artifactID int) (int, error)

TaskOfArtifact answers which task a deliverable belongs to, so an action on one can send the reader back where they came from.

func Transfer

func Transfer(db *sql.DB, name, toDesk, toOwner, toParent, by string) (moved int, err error)

Transfer moves an analyst to another desk, another owner, or both.

What moves with it is its OPEN work. Its finished work does not.

That split is the whole design decision here, and it is not a shortcut. A closed month has been charged: the chargeback page exists to stop an allocation moving after somebody was told what they owed, and re-attributing a finished task would move money out of a period that has been frozen and invoiced. So the past stays where it was charged, the open work follows the agent, and the agent's card shows both figures rather than one that quietly spans two owners.

The transfer itself is recorded, so the card can say when the split happened and a reader is never left to guess which desk a number belongs to.

func Unattested

func Unattested(roster []Analyst) (n int, of int)

Unattested counts the agents whose identity is bound to nothing.

The console reports this itself rather than waiting for the identity graph to say it, because an operator reading this page should not learn it from somewhere else.

func UnusablePassword

func UnusablePassword() (string, error)

UnusablePassword is a password nobody holds: 32 random bytes, returned once and never recorded anywhere.

A named function rather than four lines inline, because the property that matters here is only testable at the source. Comparing stored hashes proves nothing: they carry a per-hash salt, so a HARD-CODED password still produces a different hash on every installation and every check downstream passes while one string opens every CostCrew anywhere.

func ValidAttestation

func ValidAttestation(method, detail string) error

ValidAttestation checks a method and its evidence together.

Together, because neither half means anything alone: a method with no detail is a word, and a detail with no method says nothing about how it was obtained.

Types

type AccountMaker

type AccountMaker interface {
	// Exists rather than Get, because this only needs to know whether to
	// create one. Taking the whole *auth.User would put the password hash in
	// reach of a package that has no business holding it.
	Exists(username string) (bool, error)
	Create(username, password, role string) (bool, error)
}

AccountMaker is the part of the auth package this needs, named here so the crew package does not depend on the whole of it.

type Analyst

type Analyst struct {
	Name     string
	Role     string
	Mission  string
	Desk     string
	Engine   string
	State    string
	Reason   string
	Skills   []string
	Rights   []string
	PerTask  money.Cents
	Monthly  money.Cents
	Cadence  string // daily, weekly, fortnightly, monthly, on-request
	Audience string

	// Governance, decided at hire time and never separately.
	Owner       string // the account that hired it
	Parent      string // who it acts on behalf of
	Attestation string // none, oidc, spiffe-svid, enclave-key, mtls-cert
	// The evidence that makes the method checkable: an issuer, a SPIFFE ID, a
	// fingerprint. Empty is only valid alongside "none"; see attestation.go
	// for why a method without one is worse than no method at all.
	AttestationDetail string
	Hired             string
}

Analyst is one member of the crew as the console holds it, which is a different thing from the fixture's static list: hiring, suspending and re-briefing all have to persist, and a Go slice cannot be edited by a form.

The governance fields are the point of this type existing. In the original, hiring an analyst and registering it with the governance stack were two unrelated acts, and the second one mostly did not happen. Here they are the same act, because at hire time the operator already has every answer in their head: they have just decided this analyst's desk, its rights and its budget. Asking again three screens later is how the metadata ends up empty.

func GetAnalyst

func GetAnalyst(db *sql.DB, name string) (Analyst, error)

func Roster

func Roster(db *sql.DB) ([]Analyst, error)

type AnomalySeed

type AnomalySeed struct {
	ID        string
	Source    string
	Service   string
	Day       string
	Direction string
	Excess    money.Cents
}

AnomalySeed is the little this package needs to know about an anomaly, so it does not depend on the package that owns them.

type Artifact

type Artifact struct {
	ID      int
	Task    int
	Author  string
	Title   string
	Body    string
	State   ArtifactState
	Reason  string // why it came back, when it did
	Created string
	Stamped string
	Stamper string

	// Source is "fixture" for a generated draft and "live" for one a model
	// actually wrote against somebody's key. See EnsureArtifactProvenance.
	Source string
}

func Artifacts

func Artifacts(db *sql.DB, task int) ([]Artifact, error)

type ArtifactState

type ArtifactState string

ArtifactState is what has happened to one deliverable.

const (
	Draft         ArtifactState = "draft"
	ReturnedDraft ArtifactState = "returned"
	PostedDraft   ArtifactState = "posted"
)

type Comment

type Comment struct {
	ID      int
	Task    int
	Author  string
	Body    string
	Created string
}

func Comments

func Comments(db *sql.DB, task int) ([]Comment, error)

type Explainer

type Explainer struct {
	ID        int
	Team      string
	Topic     string
	Audience  string
	Author    string
	Body      string
	State     string // draft, returned, published
	Reason    string
	Amount    money.Cents // the money the story is about
	Created   string
	Published string
	Publisher string
}

An explainer is a cost story written for the team that has to act on it, not for the FinOps team that found it.

This is the piece most consoles skip, and it is the reason findings do not turn into changes. A variance report says "EC2 in ml-platform rose 34% month on month"; the team reads that, cannot tell whether it is their fault or what to do, and does nothing. An explainer says what happened, what it cost, and what one named person could do about it this week.

It is commissioned, drafted and PUBLISHED BY STAMP, like every other deliverable: something written in a team's name and sent without review is how a FinOps practice loses a team it needs.

func Explainers

func Explainers(db *sql.DB) ([]Explainer, error)

func GetExplainer

func GetExplainer(db *sql.DB, id int) (Explainer, error)

type Plan

type Plan struct {
	Label    string
	Start    string
	End      string
	Goal     string
	Items    []PlanItem
	Budget   money.Cents
	Existing bool // this sprint is already on the board
}

Plan is a proposed sprint: what the crew would do next, routed to analysts by desk, with the guards it would run under.

It is a PROPOSAL. Nothing is created until an operator approves it, and the approval is what materialises the tasks. A planner that writes straight to the board is one that spends the budget before anybody agreed to the work.

func Propose

func Propose(db *sql.DB, label, start, end string) (Plan, error)

Propose builds the next sprint from what the estate actually needs, which is the difference between a plan and a template: every item names the thing it came from.

type PlanItem

type PlanItem struct {
	Title    string
	Goal     string
	Assignee string
	Desk     string
	Budget   money.Cents
	Why      string
}

type Recorder

type Recorder interface {
	Emit(kind, actor, severity string, data map[string]any, onBehalfOf []string) error
}

Recorder is the same interface anomaly.Recorder is, restated so this package does not depend on the one that depends on it.

type Scoreboard

type Scoreboard struct {
	Analyst   string
	Tasks     int
	Open      int
	Posted    int
	Returned  int
	Blocked   int
	Spent     money.Cents
	FirstPass float64 // posted over (posted + returned)
	HasRate   bool
	Anomalies int // handled
}

Scoreboard is one analyst's record, which is what a staff card is for.

type Sprint

type Sprint struct {
	ID     int
	Label  string // 2026-W31
	Start  string
	End    string
	State  string // planned, active, closed
	Goal   string
	Tasks  int
	Open   int
	Posted int
	Spent  money.Cents
	Budget money.Cents
}

func Sprints

func Sprints(db *sql.DB) ([]Sprint, error)

type Task

type Task struct {
	ID       int
	Sprint   int
	Title    string
	Goal     string
	Assignee string
	Desk     string
	State    TaskState
	Reason   string      // required for blocked and returned
	Budget   money.Cents // the per-task guard this work runs under
	Spent    money.Cents
	Anomaly  string // the anomaly this came from, when it came from one
	Created  string
	Updated  string
}

func AwaitingStamp

func AwaitingStamp(db *sql.DB) ([]Task, error)

AwaitingStamp is the work that is written and not yet judged.

It is derived from ARTIFACTS, not from a task state. The two drift: a task can sit in "active" while its deliverable is already written and waiting, which is exactly the case a reviewer needs to see. Counting task states instead measured a proxy and reported zero while six drafts sat unread.

func GetTask

func GetTask(db *sql.DB, id int) (Task, error)

func Tasks

func Tasks(db *sql.DB, f TaskFilter) ([]Task, error)

func (Task) Open

func (t Task) Open() bool

func (Task) OverGuard

func (t Task) OverGuard() bool

OverGuard is the question a per-task budget exists to answer, and it is asked of the task rather than of the analyst, because a guard nobody checks is a number in a form.

type TaskFilter

type TaskFilter struct {
	Sprint   int
	Assignee string
	Desk     string
	State    TaskState
	Anomaly  string
	OpenOnly bool
}

type TaskState

type TaskState string

TaskState is where a piece of work has got to.

const (
	Queued   TaskState = "queued"   // planned, not started
	Active   TaskState = "active"   // an analyst is on it
	Blocked  TaskState = "blocked"  // stopped, with a reason
	Returned TaskState = "returned" // came back from review, with a reason
	Done     TaskState = "done"     // written, awaiting a stamp
	Posted   TaskState = "posted"   // stamped: it counts
)

Jump to

Keyboard shortcuts

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