core

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package core implements the driving port interface defined in internal/ports/driving. It contains the business logic that orchestrates domain operations through the driven port (Transactor) without knowledge of the specific storage or transport adapters in use.

Driving adapters (CLI commands, HTTP handlers) depend on the driving port interface, not on this package directly. The configurator (internal/wiring) wires core to the driven adapter at startup.

Index

Constants

View Source
const MaxDepth = 3

MaxDepth is the maximum number of levels allowed in the issue hierarchy. A root issue is level 1, its child is level 2, its grandchild is level 3.

Variables

This section is empty.

Functions

func CountSkippedChecks

func CountSkippedChecks(checks []driving.DoctorCheckResult) int

CountSkippedChecks returns the number of checks with status "skipped".

func EpicSecondaryState

func EpicSecondaryState(
	state domain.State,
	hasActiveClaim bool,
	hasChildren bool,
	allChildrenClosed bool,
	blockers []domain.BlockerStatus,
	ancestors []domain.AncestorStatus,
) domain.SecondaryStateResult

EpicSecondaryState computes the secondary state for an epic based on its primary state, claim status, child status, blockers, and ancestor conditions.

Priority rules for open epics (highest to lowest):

  • claimed: open + active claim → claimed (regardless of children or blockers)
  • completed: all children closed → completed
  • blocked: unresolved blocker or blocked/deferred ancestor
  • ready: no children (needs decomposition)
  • active: has children, not all closed

Detail-view states capture the full set of applicable conditions (e.g., [blocked, active] for a blocked epic with in-progress children).

Returns a zero-value SecondaryStateResult (ListState = SecondaryNone) for closed epics, or for deferred epics that are not blocked.

func IsEpicReady

func IsEpicReady(state domain.State, hasActiveClaim bool, hasChildren bool, blockers []domain.BlockerStatus, ancestors []domain.AncestorStatus) bool

IsEpicReady determines whether an epic is ready for decomposition.

An epic is ready when:

  1. Its state is open.
  2. It has no active (non-stale) claim — claimed epics are already being decomposed and are not available for new claimants.
  3. It has no children (needs decomposition).
  4. It has no unresolved blocked_by relationships.
  5. No ancestor is deferred or blocked.

func IsTaskReady

func IsTaskReady(state domain.State, hasActiveClaim bool, blockers []domain.BlockerStatus, ancestors []domain.AncestorStatus) bool

IsTaskReady determines whether a task is ready for work.

A task is ready when:

  1. Its state is open.
  2. It has no active (non-stale) claim — claimed issues are already being worked on and are not available for new claimants.
  3. It has no unresolved blocked_by relationships (closed or deleted targets count as resolved).
  4. No ancestor is deferred or blocked.

func New

func New(tx driven.Transactor, migrator driven.Migrator) driving.Service

New creates a new driving.Service backed by the given Transactor and optional Migrator. When migrator is nil, calls to CheckSchemaVersion and MigrateV1ToV2 return an error indicating that migration is not supported by the backing store (e.g., in-memory stores used in tests). In production the SQLite store satisfies both interfaces and both arguments are provided.

func SeverityBelow

func SeverityBelow(threshold driving.DoctorSeverity) string

SeverityBelow returns the label for the severity level immediately below the given threshold. Used in skip summary messages.

func TaskSecondaryState

func TaskSecondaryState(state domain.State, hasActiveClaim bool, blockers []domain.BlockerStatus, ancestors []domain.AncestorStatus) domain.SecondaryStateResult

TaskSecondaryState computes the secondary state for a task given its primary state, claim status, blockers, and ancestor statuses.

Rules:

  • open + active claim → claimed (takes priority over ready/blocked)
  • open + no active claim + not blocked → ready
  • open + no active claim + blocked → blocked
  • deferred + blocked → blocked
  • deferred + not blocked → none
  • closed → none

func ValidateActiveClaim added in v0.2.0

func ValidateActiveClaim(c domain.Claim, now time.Time) error

ValidateActiveClaim checks that a claim retrieved from storage has not yet gone stale. Operations that mutate an issue (update, close, defer, delete) must call this after loading the claim so that expired claims are rejected with a clear error rather than silently accepted.

Returns nil if the claim is still active, or ErrStaleClaim if it has passed its stale-at timestamp.

func ValidateClaim

func ValidateClaim(status IssueClaimStatus, now time.Time) error

ValidateClaim checks whether an issue can be claimed.

Claims are only valid on open issues. Closed and deferred issues cannot be claimed — reopen operations are claim-free. A stale claim is treated as nonexistent — the caller is responsible for deleting or overwriting the expired row before creating the new claim. An active (non-stale) claim always produces a ClaimConflictError, because steal mechanics have been removed; callers must wait for the existing claim to expire.

Returns nil if the issue is claimable, or an appropriate error.

func ValidateDeletion

func ValidateDeletion(isDeleted bool) error

ValidateDeletion checks whether an issue can be deleted. An issue must not already be deleted.

func ValidateDepth

func ValidateDepth(proposedParent domain.ID, lookup AncestorLookup) error

ValidateDepth checks that assigning a child under proposedParent would not exceed MaxDepth levels. It walks the ancestor chain of proposedParent to determine how deep it sits, then verifies room for one more level.

func ValidateEpicDepth

func ValidateEpicDepth(role domain.Role, proposedParent domain.ID, lookup AncestorLookup) error

ValidateEpicDepth checks that creating an epic under proposedParent would not place it at MaxDepth. Epics organize children, but an issue at MaxDepth cannot have children — so an epic there is structurally invalid. Tasks are permitted at MaxDepth because they are leaf nodes.

This validation is additive to ValidateDepth; callers should invoke both. ValidateDepth rejects any role at depth > MaxDepth, while ValidateEpicDepth further restricts epics to depth < MaxDepth.

func ValidateNoCycle

func ValidateNoCycle(childID, proposedParent domain.ID, lookup AncestorLookup) error

ValidateNoCycle walks the ancestor chain of proposedParent to ensure that assigning it as the parent of childID would not create a cycle. A cycle exists if childID appears as an ancestor of proposedParent.

func ValidateParent

func ValidateParent(childID, parentID domain.ID, parentDeleted bool) error

ValidateParent checks parent assignment constraints:

  • An issue cannot be its own parent.
  • A deleted issue cannot be assigned as a parent.

Any issue role (task or epic) may be a parent of any other issue role. Cycle detection is handled separately by ValidateNoCycle.

Types

type AncestorLookup

type AncestorLookup func(id domain.ID) (parentID domain.ID, err error)

AncestorLookup is a callback that returns the parent ID of a given issue. It returns a zero ID if the issue has no parent. An error indicates a lookup failure.

type DeletionResult

type DeletionResult struct {
	// ToDelete contains the IDs of all issues that should be soft-deleted,
	// including the target issue itself.
	ToDelete []domain.ID

	// Conflicts contains descendants that are currently claimed and prevent
	// the deletion.
	Conflicts []domain.DescendantInfo
}

DeletionResult holds the outcome of a deletion check: either a set of issue IDs to delete or a conflict error identifying claimed descendants.

func PlanEpicDeletion

func PlanEpicDeletion(epicID domain.ID, descendants []domain.DescendantInfo) DeletionResult

PlanEpicDeletion checks whether an epic can be deleted by examining all its descendants. If any descendant is currently claimed, the deletion fails with a conflict listing the claimed issue(s). Otherwise, it returns the set of issue IDs to soft-delete (the epic itself plus all unclaimed descendants).

For tasks, the result contains only the task's own ID (tasks have no descendants).

type EpicProgress

type EpicProgress struct {
	// Total is the number of direct children.
	Total int
	// Closed is the number of children in the closed state.
	Closed int
	// Open is the number of non-blocked children in the open state.
	// Includes children with an active claim (open + claimed secondary state).
	Open int
	// Blocked is the number of children that are blocked (any primary state
	// with an unresolved blocked_by relationship).
	Blocked int
	// Deferred is the number of non-blocked children in the deferred state.
	Deferred int
	// Percent is the completion percentage (0–100).
	Percent int
	// Completed is true when the epic has at least one child and all
	// children are closed.
	Completed bool
}

EpicProgress holds the computed completion metrics for an epic.

func ComputeEpicProgress

func ComputeEpicProgress(children []domain.ChildStatus) EpicProgress

ComputeEpicProgress derives completion metrics from a list of child statuses. An epic is completed when it has at least one child and all children are closed. Returns a zero-value EpicProgress when the child list is empty.

Blocked children are counted separately regardless of their primary state. Claimed is now a secondary state of open, so open children with an active claim count under Open rather than a separate bucket.

type IssueClaimStatus

type IssueClaimStatus struct {
	// State is the issue's current lifecycle state.
	State domain.State
	// IsDeleted is true if the issue has been soft-deleted.
	IsDeleted bool
	// ActiveClaim is the current claim on the issue, if any. A zero-value
	// Claim (empty ID) indicates no active claim.
	ActiveClaim domain.Claim
}

IssueClaimStatus summarizes an issue's state for claim validation.

Jump to

Keyboard shortcuts

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