Help us shape the future of Go! Take the 2026 Go Developer Survey

domain

package
v1.55.0 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: AGPL-3.0 Imports: 2 Imported by: 0

Documentation

Overview

Package domain contains pure domain types for the backlog subsystem. It imports only standard library packages — no ent, no headless, no git deps — so it can be imported by any layer (server, adapters, pkg/events) without creating an import cycle with the parent session package.

Index

Constants

View Source
const (
	ReviewVerdictPass         = ReviewOutcomePass
	ReviewVerdictFail         = ReviewOutcomeFail
	ReviewVerdictPartial      = ReviewOutcomePartial
	ReviewVerdictUnverifiable = ReviewOutcomeUnverifiable
)

Backward-compatible aliases so callers can be migrated incrementally. Prefer ReviewOutcome* constants in new code.

View Source
const DefaultBacklogPriority = 3

DefaultBacklogPriority is the default priority assigned to new backlog items when no priority is specified. Lower values indicate higher priority.

Variables

View Source
var (
	ErrACRequired                   = errors.New("acceptance criteria required before marking ready")
	ErrPlanRequired                 = errors.New("plan must be approved or skip_planning must be true before spawning work session")
	ErrPlanArtifactsRequired        = errors.New("plan artifacts path is required when planning is not skipped")
	ErrVerdictRequired              = errors.New("PASS verdict or manual override required before marking done")
	ErrCodeNotOnMain                = errors.New("code changes must actually be on main (merged locally or via a merged PR) before marking done; provide override_reason to bypass")
	ErrUnresolvedBlockers           = errors.New("item has one or more blockers that have not reached done")
	ErrVerdictClearRequiredForReady = errors.New("item has a recorded PASS verdict; provide override_reason to send it back to ready anyway")
)

Sentinel errors for transition guards.

AllStuckReasons lists every valid StuckReason constant.

Functions

func CanTransitionBacklog

func CanTransitionBacklog(from, to BacklogStatus) bool

CanTransitionBacklog reports whether a transition from one backlog status to another is permitted.

func TransitionGuard

func TransitionGuard(item BacklogItemTransitionInput, to BacklogStatus) error

TransitionGuard validates business rules before a status transition. It returns nil when the transition is allowed, or a sentinel error when a guard condition is violated. It does NOT check CanTransition — callers must invoke CanTransition separately if structural validity is also required.

func ValidTransitions

func ValidTransitions() map[BacklogStatus]map[BacklogStatus]bool

ValidTransitions returns a deep copy of the authoritative transition table. Callers that need a local snapshot (e.g. for concurrent reads without repeated map lookups) should call this once at construction time.

Types

type AcCriteriaJSON

type AcCriteriaJSON string

AcCriteriaJSON is the JSON-serialized form of []AcCriterion stored in the DB. Using a named type prevents silently passing Description or other string fields where serialized AC criteria are expected.

const AcCriteriaJSONEmpty AcCriteriaJSON = ""

AcCriteriaJSONEmpty is the zero value — an empty criteria list.

func SerializeAcCriteria

func SerializeAcCriteria(criteria []AcCriterion) (AcCriteriaJSON, error)

SerializeAcCriteria serializes acceptance criteria to an AcCriteriaJSON value.

func (AcCriteriaJSON) IsEmpty

func (j AcCriteriaJSON) IsEmpty() bool

IsEmpty reports whether j contains no criteria JSON.

func (AcCriteriaJSON) Parse

func (j AcCriteriaJSON) Parse() ([]AcCriterion, error)

Parse deserializes the criteria from JSON.

type AcCriterion

type AcCriterion struct {
	Index  int      `json:"index"`
	Text   string   `json:"text"`
	Status AcStatus `json:"status"` // pending, in_progress, done, fail
	Note   string   `json:"note,omitempty"`
}

AcCriterion is a single acceptance criterion for a backlog item.

func ParseAcCriteria

func ParseAcCriteria(raw AcCriteriaJSON) ([]AcCriterion, error)

ParseAcCriteria deserializes acceptance criteria from a JSON string.

type AcStatus

type AcStatus string

AcStatus represents the status of a single acceptance criterion.

const (
	AcStatusPending    AcStatus = "pending"
	AcStatusInProgress AcStatus = "in_progress"
	AcStatusDone       AcStatus = "done"
	AcStatusFail       AcStatus = "fail"
)

func (AcStatus) IsValid

func (s AcStatus) IsValid() bool

IsValid reports whether s is a known AC status value.

type BacklogCategory added in v1.41.0

type BacklogCategory string

BacklogCategory is a validated string-backed enum classifying a backlog item into a coarse bucket (bugfix/feature/chore/refactor). It is purely a frontend-defaulting hint — BacklogItemForm.tsx applies each category's automation-toggle defaults once, at creation time, into local form state; the server only persists and validates the string itself (see session.IsValidBacklogCategory), it does not resolve or apply any defaults. Unlike StuckReason above, the empty string is a valid value here too, meaning "uncategorized" — today's behavior for every existing item, preserved exactly.

const (
	BacklogCategoryBugfix   BacklogCategory = "bugfix"
	BacklogCategoryFeature  BacklogCategory = "feature"
	BacklogCategoryChore    BacklogCategory = "chore"
	BacklogCategoryRefactor BacklogCategory = "refactor"
)

func (BacklogCategory) IsValid added in v1.41.0

func (c BacklogCategory) IsValid() bool

IsValid reports whether c is a known backlog category, or the empty string (uncategorized).

type BacklogItemTransitionInput

type BacklogItemTransitionInput struct {
	// ItemID is the BacklogItem's own ID (string form, matching
	// session.BacklogItemData.ID). Added for Epic 2.4's
	// ConfiguredWorkflowEngine.PendingGates, which must look up a stateful
	// gate's persisted GateSatisfactionRecord by (item, gate) — TransitionGuard
	// itself never reads this field. Zero value ("") is safe for every
	// existing caller that only exercises TransitionGuard/DefaultWorkflowEngine.
	ItemID            string
	Status            BacklogStatus
	AcCriteria        AcCriteriaJSON // serialized acceptance criteria
	PlanApproved      bool
	SkipPlanning      bool
	PlanArtifactsPath string        // path to plan artifacts written by triage session
	OverallOutcome    ReviewOutcome // from linked ReviewVerdict
	OverrideReason    string
	// HasUnshippedCode is true when a work session committed code
	// (LastCommitSha != "") that has not been verified to actually be on main —
	// locally (merged/committed directly) or remotely (merged PR, pulled or not).
	// A PrURL alone does NOT clear this: an open, unmerged, or later-reverted PR
	// still has PrURL set, so it was never proof the code shipped. The
	// review→done guard uses this to block premature done transitions.
	HasUnshippedCode bool
	// HasUnresolvedBlockers is true when this item has at least one
	// BacklogItemDependency where the blocker has not reached done. The
	// ready/queued->in_progress guard uses this to keep a gated item from
	// being dequeued/started ahead of its blocker. Callers populate this via
	// a batched query (see DequeueNextQueuedItems) rather than a per-item
	// lookup.
	HasUnresolvedBlockers bool
}

BacklogItemTransitionInput carries the fields needed by TransitionGuard.

type BacklogStatus

type BacklogStatus string

BacklogStatus represents the lifecycle state of a backlog item. The type is open, not a closed enum: the 9 constants below are its built-in subset, but an operator-configured custom workflow stage (Epic 2.3's ConfiguredWorkflowEngine) is also a valid BacklogStatus value — no distinct "custom stage slug" type exists, and none is planned. See the "BacklogStatus becomes the open stage-slug type" decision (project_plans/backlog-custom-workflow-stages/implementation/plan.md, Epic 2.1) for the full rationale.

const (
	BacklogStatusIdea       BacklogStatus = "idea"
	BacklogStatusRefining   BacklogStatus = "refining"
	BacklogStatusReady      BacklogStatus = "ready"
	BacklogStatusQueued     BacklogStatus = "queued"
	BacklogStatusInProgress BacklogStatus = "in_progress"
	BacklogStatusReview     BacklogStatus = "review"
	BacklogStatusPRPending  BacklogStatus = "pr_pending"
	BacklogStatusDone       BacklogStatus = "done"
	BacklogStatusArchived   BacklogStatus = "archived"
)

type CriterionVerdict

type CriterionVerdict struct {
	CriterionIndex int           `json:"criterion_index"`
	Outcome        ReviewOutcome `json:"outcome"`
	Evidence       string        `json:"evidence"`
}

CriterionVerdict holds the review outcome for a single acceptance criterion.

type QuestionType added in v1.54.0

type QuestionType string

QuestionType is a validated string-backed enum identifying a GuidanceRequest's answer shape, driving which control the React component renders.

const (
	QuestionTypeYesNo          QuestionType = "yes-no"
	QuestionTypeMultipleChoice QuestionType = "multiple-choice"
	QuestionTypeShortAnswer    QuestionType = "short-answer"
)

func (QuestionType) IsValid added in v1.54.0

func (t QuestionType) IsValid() bool

IsValid reports whether t is a known question type value.

type RequestScope added in v1.54.0

type RequestScope string

RequestScope is a validated string-backed enum identifying what a GuidanceRequest is attached to — matching the house StuckReason/ BacklogStatus style. Determines the ownership check and delivery branch (see GuidanceRequestDeliveryService).

const (
	// RequestScopeBacklogItem: attached to a BacklogItem via item_id.
	RequestScopeBacklogItem RequestScope = "backlog-item"
	// RequestScopeSession: attached to a live/paused session via session_uuid.
	RequestScopeSession RequestScope = "session"
	// RequestScopeStandalone: not attached to anything; human-only surface.
	RequestScopeStandalone RequestScope = "standalone"
)

func (RequestScope) IsValid added in v1.54.0

func (s RequestScope) IsValid() bool

IsValid reports whether s is a known request scope value.

type ReviewOutcome

type ReviewOutcome string

ReviewOutcome is a typed verdict outcome value (PASS, FAIL, PARTIAL, UNVERIFIABLE).

const (
	ReviewOutcomePass         ReviewOutcome = "PASS"
	ReviewOutcomeFail         ReviewOutcome = "FAIL"
	ReviewOutcomePartial      ReviewOutcome = "PARTIAL"
	ReviewOutcomeUnverifiable ReviewOutcome = "UNVERIFIABLE"
)

func AggregateOutcome

func AggregateOutcome(verdicts []CriterionVerdict) ReviewOutcome

AggregateOutcome computes the overall outcome from a slice of CriterionVerdicts. Priority (highest to lowest): FAIL > PARTIAL > UNVERIFIABLE > PASS. Returns FAIL when the slice is empty to prevent auto-approval of empty reviews.

func (ReviewOutcome) IsValid

func (o ReviewOutcome) IsValid() bool

IsValid reports whether o is a recognised review outcome.

type StuckReason added in v1.38.0

type StuckReason string

StuckReason is a validated string-backed enum of the classes a backlog item can be "stuck" for — matching the house BacklogStatus/ReviewOutcome style (validated at the boundary via IsValid, not a truly-unrepresentable sum type). Only these compile-time constants should ever reach MarkStuck; no unvalidated string should reach the DB.

const (
	// StuckReasonPRReadyUnmerged: a pr_pending item's PR is green, mergeable,
	// and unmerged past the threshold (see prReadyToMergeSolo).
	StuckReasonPRReadyUnmerged StuckReason = "pr_ready_unmerged"
	// StuckReasonReworkCap: the auto-rework loop hit maxAutoReworkIterations
	// and parked the item for manual action.
	StuckReasonReworkCap StuckReason = "rework_cap"
	// StuckReasonAbandonedReview: a review-status item has a review verdict on
	// record but nothing active in flight.
	StuckReasonAbandonedReview StuckReason = "abandoned_review"
	// StuckReasonStaleWork: an in_progress item's active work session reported
	// no progress for longer than maxWorkSessionStaleness.
	StuckReasonStaleWork StuckReason = "stale_work"
	// StuckReasonBouncing: an item crossed in_progress <-> review >= bounceThreshold
	// times within bounceLookback with no PASS verdict.
	StuckReasonBouncing StuckReason = "bouncing"
	// StuckReasonPushFailed: pushAndCreatePR failed (push rejected / gh pr
	// create errored) leaving a post-review item with no pr_number.
	StuckReasonPushFailed StuckReason = "push_failed"
	// StuckReasonOrphanedTriage: an idea-status item's triage session ended
	// without ever transitioning the item to ready. Covers two shapes: the
	// session is still open and has gone stale (crashed, was killed, or the
	// server restarted mid-triage), or the session already ended cleanly (the
	// headless call errored, or returned output the triage parser rejected —
	// e.g. a premature "still working" status message instead of the final
	// JSON block, confirmed live 2026-07-29/30, see
	// docs/tasks/backlog-feature-improvement.md's 2026-07-30 entry) but
	// TriggerTriage never reached its idea->ready transition. Previously only
	// the first shape was detected, and only when a human manually
	// re-triggered triage (tombstoneOrphanTriageSessions); this reason lets the
	// periodic stuck sweep catch both shapes without a manual retry — see
	// reconcileOrphanedTriageItems (session/backlog_lifecycle.go).
	StuckReasonOrphanedTriage StuckReason = "orphaned_triage"
	// StuckReasonAutonomousStuck: an autonomous driver run stopped after
	// maxTurns without a DONE signal. Previously only surfaced as a one-off
	// ephemeral notification (onAutonomousDriverComplete), invisible to the
	// Unfinished tab's durable stuck-reason system.
	StuckReasonAutonomousStuck StuckReason = "autonomous_stuck"
	// StuckReasonSpawnFailed: AutoReopenAfterFailedReview transitioned an item
	// to in_progress, then SpawnSessionFromItem failed AND the scoped rollback
	// to "review" also failed (its precondition no longer matched — something
	// else touched the item in the interim). Previously this left the item
	// silently stranded at in_progress with no work session and no visible
	// error (server/services/backlog_service_triage.go's rollback branch only
	// logged it) — invisible to every other stuck detector, since none of them
	// check "in_progress with zero live sessions and no error surfaced."
	StuckReasonSpawnFailed StuckReason = "spawn_failed"
	// StuckReasonPlanNotApproved: DequeueNextQueuedItems' planning gate
	// (SkipPlanning=false, PlanApproved=false) refuses to claim a queued item
	// indefinitely — by design (see that function's doc comment) — with only a
	// per-tick WARNING log and no durable, human-visible signal. Confirmed live
	// 2026-07-22: three items sat queued for days, silently re-blocked on every
	// 60s tick, invisible on the kanban board (BUG-037) and with no "Approve
	// Plan" action anywhere in the UI to unblock them.
	StuckReasonPlanNotApproved StuckReason = "plan_not_approved"
	// StuckReasonPRPendingNoPR: an item is in pr_pending status but has no PR
	// reference (pr_number == 0, pr_url == ""). Every downstream reconciler
	// (ReconcilePRPending's FindPRPendingItems query, EnablePRAutoMerge, etc.)
	// requires a real PrNumber, so an item in this shape is invisible to
	// everything else and sits in pr_pending permanently with nothing left to
	// poll or retry (BUG-040). Detection-only backstop: two write-ordering
	// bugs (pushAndCreatePR's best-effort field persist; ReconcilePRPending's
	// closed-PR branch clearing fields before confirming the reopen actually
	// succeeded) were found and fixed as the direct cause of the live incident
	// this reason was added for, but this detector exists so any *future*
	// mistake with the same shape — "a write silently doesn't happen or
	// happens out of order, and nothing detects the resulting dead end" — is
	// still visible and retryable from /unfinished rather than a silent
	// permanent stall.
	StuckReasonPRPendingNoPR StuckReason = "pr_pending_no_pr"
	// StuckReasonReworkBlockedStale: a review-status item's failed-review
	// rework attempt is blocked because its prior work session is still alive
	// (hasActiveWorkSession's guard, in AutoReopenAfterFailedReview) but has
	// produced no output for longer than maxReworkBlockStaleness (15min — see
	// project_plans/review-gate-stale-session-rework/decisions/ADR-001-
	// staleness-threshold-recalibration.md). Set by
	// notifyIfActiveWorkSessionStale (server/services/backlog_service_triage.go),
	// resolved by ResolveReworkBlockedStaleIfRecovered once the session
	// produces output again, leaves review, or its work session ends.
	// Distinct from StuckReasonStaleWork, which covers the structurally
	// similar but different-status case of an in_progress item's active work
	// session going stale — the two are deliberately kept as separate reasons
	// (different item status, different threshold, different urgency) rather
	// than merged.
	StuckReasonReworkBlockedStale StuckReason = "rework_blocked_stale"
	// StuckReasonPRNeedsFix: a pr_pending item's PR has failing CI, blocking
	// reviews, or a merge conflict (ReconcilePRPending's spawn-fix branches).
	// Gates ReconcilePRPending's AutoReopenForPRFix dispatch through the
	// shared remediation backoff (Storage.RemediationDue) so a PR that keeps
	// failing CI doesn't get a fresh fix session spawned on every ~60s
	// reconciliation tick indefinitely — previously ungated, unlike every
	// sibling remediation call site in session/backlog_lifecycle.go (see
	// docs/tasks/backlog-feature-improvement.md's 2026-07-28 entry). Resolved
	// once the PR becomes healthy again or the item reaches done.
	StuckReasonPRNeedsFix StuckReason = "pr_needs_fix"
	// StuckReasonRespawnBlockedActive: an automated respawn attempt
	// (AutoRespawnAutonomousWork, AutoReopenForPRFix, or AutoRespawnReview —
	// server/services/backlog_service_triage.go) was skipped because the item
	// already has an active work or review session, per
	// findActiveWorkSession/findActiveReviewSession. Before this reason
	// existed, all three call sites only log.InfoLog().Printf'd the skip —
	// zero operator-visible signal and no audit record, strictly worse than
	// spawnSessionAfterGates' own 8b guard (activeWorkSessionBlockedError),
	// which at least returns a progress-enriched error to its synchronous
	// caller (docs/tasks/backlog-feature-improvement.md, 2026-07-31/
	// 2026-08-03 updates). Set by notifyRespawnBlockedByActiveSession, which
	// reuses workSessionStaleness for the same "still active" vs. "likely
	// stalled" distinction. Distinct from StuckReasonReworkBlockedStale
	// (review-status-only, staleness-gated) since this fires regardless of
	// staleness and covers three different item statuses (in_progress,
	// pr_pending, review). Resolved the next time the guarding function runs
	// past its active-session check (the block has cleared).
	StuckReasonRespawnBlockedActive StuckReason = "respawn_blocked_active"
	// StuckReasonLikelyFlaky: session.IsFlakyVerdictFlipFlop or
	// session.IsTestOnlyReworkCycle matched on this item's recent review
	// history — behavioral evidence (not a keyword match on title/description;
	// see project_plans/backlog-bounce-escalation/decisions/ADR-002) that the
	// review outcome may be non-deterministic rather than a real pass/fail
	// signal. Purely informational: set alongside AutoReopenAfterFailedReview's
	// existing reopen/park decision, never gating it — a misfiring heuristic
	// here cannot newly stall an item that would otherwise proceed. Both
	// predicates carry documented false-positive sources (see their doc
	// comments in session/stuck_decisions.go); the UI should present this as a
	// hint to verify, not a confident verdict.
	StuckReasonLikelyFlaky StuckReason = "likely_flaky"
	// StuckReasonBlockedByDependency: DequeueNextQueuedItems' dependency gate
	// (UnresolvedBlockerItemIDs) skipped this item because at least one of its
	// blocker items (session/ent/schema/backlog_item_dependency.go) has not yet
	// reached a resolved status (done or archived — see
	// project_plans/backlog-item-dependencies/decisions/ADR-001-dangling-
	// blocker-resolution.md). Purely detection/visibility: mirrors
	// StuckReasonPlanNotApproved's precedent of surfacing an indefinite,
	// by-design dequeue skip so it's visible on /unfinished and in the item
	// detail view (BlockerChip) instead of only a per-tick log line. Resolved
	// the next time UnresolvedBlockerItemIDs finds no unresolved blocker left
	// for this item.
	StuckReasonBlockedByDependency StuckReason = "blocked_by_dependency"
	// StuckReasonMultipleReasons: a synthetic, aggregate reason marking an
	// item that has multiReasonThreshold or more *other*, non-escalation
	// stuck reasons open simultaneously (session/stuck_decisions.go's
	// isMultiReasonEscalated). Set/resolved by reconcileMultiReasonEscalation
	// (session/backlog_lifecycle_stuck.go) as the count of open non-escalation
	// reasons crosses the threshold in either direction. Deliberately excludes
	// itself and StuckReasonBounceCapExhausted from its own count (ADR-001)
	// to avoid a self-reinforcing escalation loop. Unlike every other
	// StuckReason, this one carries no independent remediation action of its
	// own — it exists purely as an operator-visible severity signal that an
	// item is stuck for multiple simultaneous reasons, not a single narrow
	// one.
	StuckReasonMultipleReasons StuckReason = "multiple_reasons"
	// StuckReasonBounceCapExhausted: a synthetic, aggregate reason marking an
	// item whose bouncing remediation gate hit MaxRemediationAttempts while
	// StuckReasonBouncing itself is still open (autoReopenWithBackoffGate,
	// session/backlog_lifecycle.go). Signals that automated remediation has
	// given up retrying and the item now needs human intervention. Resolved
	// by reconcileBouncingItems once StuckReasonBouncing itself resolves, or
	// by the selfHealStuck backstop once the item's status leaves
	// in_progress/review entirely. Like StuckReasonMultipleReasons, this is a
	// meta/aggregate signal with no independent remediation action of its
	// own.
	StuckReasonBounceCapExhausted StuckReason = "bounce_cap_exhausted"
	// StuckReasonSteerFailed: AutoReopenForPRFix attempted to steer an
	// already-active session with a PR-fix problem description
	// (SessionSteerer.SteerActiveSession) and the delivery itself failed —
	// distinct from StuckReasonRespawnBlockedActive, which covers the
	// degrade paths where a steer was never attempted at all (nil-safe
	// SessionSteerer, session not live, or dedup/debounce suppression). See
	// ADR-002 (project_plans/pr-fix-steering/decisions/): reusing
	// RespawnBlockedActive here would render BlockerChip's fixed "Auto-
	// respawn skipped" label for a strictly worse outcome (attempted and
	// failed). Set/resolved by notifyActiveSessionSteered
	// (server/services/backlog_service_pr_fix_steer.go); the two reasons are
	// mutually exclusive per item — each path resolves the other.
	StuckReasonSteerFailed StuckReason = "steer_failed"
	// StuckReasonGateTimeout: a custom-transition gate check (any GateKind, not
	// only GateKindCustom) exceeded its bound LivenessDefinition's
	// StalenessThreshold — e.g. a GateKindCustom invocation (Epic 2.4.4's
	// InvokeCustomGateCheck) still open past ExpectedDuration+StalenessMargin.
	// Deliberately one generic reason for every custom-transition/custom-gate
	// liveness timeout, not a value per gate kind or per individual gate — see
	// project_plans/backlog-custom-workflow-stages/implementation/plan.md's
	// "Decision: StuckReasonGateTimeout" (Epic 2.4), which keeps StuckReason a
	// closed, exhaustively-switchable enum. Set by reconcileCustomGateChecks
	// (session/backlog_lifecycle_gates.go), mirroring
	// reconcileOrphanedTriageItems' LivenessEngine-consulting sweep pattern.
	StuckReasonGateTimeout StuckReason = "gate_timeout"
)

func (StuckReason) IsValid added in v1.38.0

func (r StuckReason) IsValid() bool

IsValid reports whether r is a known stuck reason value.

Jump to

Keyboard shortcuts

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