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
- func CountSkippedChecks(checks []driving.DoctorCheckResult) int
- func EpicSecondaryState(state domain.State, hasActiveClaim bool, hasChildren bool, ...) domain.SecondaryStateResult
- func IsEpicReady(state domain.State, hasActiveClaim bool, hasChildren bool, ...) bool
- func IsTaskReady(state domain.State, hasActiveClaim bool, blockers []domain.BlockerStatus, ...) bool
- func New(tx driven.Transactor, migrator driven.Migrator) driving.Service
- func SeverityBelow(threshold driving.DoctorSeverity) string
- func TaskSecondaryState(state domain.State, hasActiveClaim bool, blockers []domain.BlockerStatus, ...) domain.SecondaryStateResult
- func ValidateActiveClaim(c domain.Claim, now time.Time) error
- func ValidateClaim(status IssueClaimStatus, now time.Time) error
- func ValidateDeletion(isDeleted bool) error
- func ValidateDepth(proposedParent domain.ID, lookup AncestorLookup) error
- func ValidateEpicDepth(role domain.Role, proposedParent domain.ID, lookup AncestorLookup) error
- func ValidateNoCycle(childID, proposedParent domain.ID, lookup AncestorLookup) error
- func ValidateParent(childID, parentID domain.ID, parentDeleted bool) error
- type AncestorLookup
- type DeletionResult
- type EpicProgress
- type IssueClaimStatus
Constants ¶
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:
- Its state is open.
- It has no active (non-stale) claim — claimed epics are already being decomposed and are not available for new claimants.
- It has no children (needs decomposition).
- It has no unresolved blocked_by relationships.
- 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:
- Its state is open.
- It has no active (non-stale) claim — claimed issues are already being worked on and are not available for new claimants.
- It has no unresolved blocked_by relationships (closed or deleted targets count as resolved).
- No ancestor is deferred or blocked.
func New ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
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.