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
- Variables
- func ActiveNames(db *sql.DB) ([]string, error)
- func Approve(db *sql.DB, p Plan) (int, error)
- func Assign(db *sql.DB, id int, analyst string) error
- func AssignableTo(db *sql.DB, analyst string) error
- func BackfillMandate(db *sql.DB, owner string) (int, error)
- func Block(db *sql.DB, id int, reason string) error
- func CheckGuards(db *sql.DB, month string, rec Recorder) (past int, by money.Cents, err error)
- func ClearFabricated(db *sql.DB) (int, error)
- func CloseSprint(db *sql.DB, id int) (int, error)
- func Comment_(db *sql.DB, task int, author, body string) error
- func Commission(db *sql.DB, team, topic, audience, author string, amount money.Cents) (int, error)
- func CountsForAttestation(a Analyst) bool
- func DeskOwners() map[string]string
- func Desks(db *sql.DB) ([]string, error)
- func DropRetiredRights(db *sql.DB) (int, error)
- func EnsureArtifactProvenance(db *sql.DB) error
- func EnsureLiveSpendLedger(db *sql.DB) error
- func EnsureOwnershipHistory(db *sql.DB) error
- func FromAnomaly(db *sql.DB, anomalyID, title, goal, assignee, desk string, budget money.Cents) (int, error)
- func Hire(db *sql.DB, a Analyst) error
- func IsUnattested(a Analyst) bool
- func LiveSpend(db *sql.DB) (micros int64, tasks int, err error)
- func LiveSpendBy(db *sql.DB, analyst string) (micros int64, tasks int, err error)
- func OpenWork(db *sql.DB, name string) (int, error)
- func OwnerOf(db *sql.DB, agent string) string
- func Post(db *sql.DB, artifactID int, stamper string) error
- func Publish(db *sql.DB, id int, by string) error
- func RealMoney(micros int64, tasks int) string
- func Rebrief(db *sql.DB, a Analyst) error
- func Remove(db *sql.DB, name, by string) error
- func Return(db *sql.DB, artifactID int, reason string) error
- func ReturnExplainer(db *sql.DB, id int, reason string) error
- func RightsFor(skills []string, state string) []string
- func Scoreboards(db *sql.DB) (map[string]Scoreboard, error)
- func Seed(db *sql.DB, anomalies []AnomalySeed) (sprints, tasks, artifacts int, err error)
- func SeedOwners(db *sql.DB, mk AccountMaker, seededBy string) (accounts, moved int, err error)
- func SeedRoster(db *sql.DB, owner string) (int, error)
- func SeededOwner(configured string) string
- func SetState(db *sql.DB, name, state, reason string) error
- func SettleLiveSpend(db *sql.DB) (booked money.Cents, err error)
- func SpendByOwner(db *sql.DB, period string) (map[string]money.Cents, error)
- func SpendInMonth(db *sql.DB, period string) (map[string]money.Cents, error)
- func TaskOfArtifact(db *sql.DB, artifactID int) (int, error)
- func Transfer(db *sql.DB, name, toDesk, toOwner, toParent, by string) (moved int, err error)
- func Unattested(roster []Analyst) (n int, of int)
- func UnusablePassword() (string, error)
- func ValidAttestation(method, detail string) error
- type AccountMaker
- type Analyst
- type AnomalySeed
- type Artifact
- type ArtifactState
- type Comment
- type Explainer
- type Plan
- type PlanItem
- type Recorder
- type Scoreboard
- type Sprint
- type Task
- type TaskFilter
- type TaskState
Constants ¶
const ExplainerSchema = `` /* 263-byte string literal not displayed */
const RosterSchema = `` /* 335-byte string literal not displayed */
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 ¶
var ( ErrNotFound = fmt.Errorf("no such task") ErrNeedReason = fmt.Errorf("this decision needs a reason") ErrSettled = fmt.Errorf("this is already posted") )
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", } )
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.
var Attestations = []string{"none", "oidc", "spiffe-svid", "enclave-key", "mtls-cert"}
Attestations is what the Agent Passport spec allows.
var ErrHasWork = errors.New("this analyst still has open work")
ErrHasWork is returned when an analyst still has work on the board.
var ErrSuspended = errors.New("this analyst is suspended")
ErrSuspended is returned when work is handed to an analyst whose mandate has been withdrawn.
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 ¶
ActiveNames is the rota: who can actually be given work.
func Approve ¶
Approve materialises a plan onto the board. This is the only thing that creates the tasks, and it is a person's act.
func AssignableTo ¶
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 ¶
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 ¶
Block stops a task, and insists on why. A blocked task with no reason is indistinguishable from one nobody picked up.
func CheckGuards ¶
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 ¶
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 ¶
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_ ¶
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 ¶
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 ¶
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 ¶
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 DropRetiredRights ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Return sends a deliverable back, and the reason is the whole point: it is what the analyst is meant to act on.
func ReturnExplainer ¶
ReturnExplainer sends it back with what has to change.
func RightsFor ¶
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 ¶
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 ¶
SeedRoster copies the fixture's crew into the store, once.
func SeededOwner ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
TaskOfArtifact answers which task a deliverable belongs to, so an action on one can send the reader back where they came from.
func Transfer ¶
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 ¶
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 ¶
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 ¶
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.
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
}
type ArtifactState ¶
type ArtifactState string
ArtifactState is what has happened to one deliverable.
const ( Draft ArtifactState = "draft" ReturnedDraft ArtifactState = "returned" PostedDraft ArtifactState = "posted" )
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.
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.
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 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 ¶
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.
type TaskFilter ¶
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 )