driving

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package driving defines the driving port — the use-case boundary that CLI and other adapters invoke. The Service interface declares every operation the application supports; the DTO types carry data across that boundary.

Driving adapters (CLI commands, future HTTP handlers, etc.) depend on this package for the interface and its input/output types. The core implementation lives in a separate package and is wired to the interface by the configurator.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AddCommentInput

type AddCommentInput struct {
	IssueID string
	Author  string
	Body    string
}

AddCommentInput holds the parameters for adding a comment.

type AddCommentOutput

type AddCommentOutput struct {
	Comment CommentDTO
}

AddCommentOutput holds the result of adding a comment.

type AgentNameInput added in v0.3.0

type AgentNameInput struct {
	// Seed is an optional string whose content is used to derive a
	// deterministic PCG seed via SHA-256. When Seed is empty, a random name
	// is generated instead.
	Seed string
}

AgentNameInput carries the inputs for the AgentName use case. Seed is optional; when empty, the service generates a fresh random name via the unseeded domain generator. When non-empty, the service delegates to the seeded domain generator so the same seed always produces the same name.

type BackupInput

type BackupInput struct {
	// Writer is the driven-port writer that receives the serialised
	// backup data. The caller is responsible for constructing and
	// closing the writer.
	Writer driven.BackupWriter
}

BackupInput holds the parameters for creating a database backup.

type BackupOutput

type BackupOutput struct {
	// IssueCount is the number of issue records written.
	IssueCount int
}

BackupOutput holds the result of a backup operation.

type BlockedByAncestorRow added in v0.4.0

type BlockedByAncestorRow struct {
	// Issue is the blocked issue's ID.
	Issue string `json:"issue"`
	// BlockingAncestor is the ancestor issue that is blocking it.
	BlockingAncestor string `json:"blocking_ancestor"`
}

BlockedByAncestorRow is emitted by the blocked-by-ancestor check. One row per (issue, ancestor) pair.

type BlockedByClosableIssueRow added in v0.4.0

type BlockedByClosableIssueRow struct {
	// Issue is the blocked issue's ID.
	Issue string `json:"issue"`
	// ClosableBlocker is the blocker that is eligible to be closed.
	ClosableBlocker string `json:"closable_blocker"`
}

BlockedByClosableIssueRow is emitted by the blocked-by-closable-issue check. One row per (issue, closable blocker) pair.

type BlockedByDeferredIssueRow added in v0.4.0

type BlockedByDeferredIssueRow struct {
	// Issue is the blocked issue's ID.
	Issue string `json:"issue"`
	// Blocker is the deferred blocking issue's ID.
	Blocker string `json:"blocker"`
}

BlockedByDeferredIssueRow is emitted by the blocked-by-deferred-issue check. One row per (issue, deferred blocker) pair.

type BlockerCycleRow added in v0.4.0

type BlockerCycleRow struct {
	// Cycle lists the issue IDs in canonical order.
	Cycle []string `json:"cycle"`
}

BlockerCycleRow is emitted by the blocker-cycles check. One row per cycle. The cycle array begins with the lowest-ID issue and follows blocked_by edges around the loop; the last element is blocked by the first.

type BlockerDetail

type BlockerDetail struct {
	ID          string
	Title       string
	State       domain.State
	ClaimAuthor string
}

BlockerDetail holds enriched information about an issue that blocks this one via a blocked_by relationship. The command layer uses this to render blockers ordered by state and annotated with claim/deferral metadata.

type ClaimInput

type ClaimInput struct {
	IssueID        string
	Author         string
	StaleThreshold time.Duration
	// StaleAt is an optional absolute timestamp at which the claim becomes
	// stale. When non-zero, it takes precedence over StaleThreshold. The
	// caller is responsible for validating that StaleAt is in the future
	// and within 24 hours; the service passes it through to the domain.
	StaleAt time.Time
	// LabelFilters specifies label guard-rail assertions when claiming by ID.
	// If non-empty, the issue must match all filters or the claim fails with a
	// descriptive error naming the issue and unmet condition.
	LabelFilters []LabelFilterInput
	// Role specifies a role guard-rail assertion when claiming by ID. If
	// non-zero, the issue must have this role or the claim fails with a
	// descriptive error naming the issue and unmet condition.
	Role domain.Role
}

ClaimInput holds the parameters for claiming an issue.

type ClaimNextReadyInput

type ClaimNextReadyInput struct {
	Author         string
	Role           domain.Role
	LabelFilters   []LabelFilterInput
	StaleThreshold time.Duration
	// StaleAt is an optional absolute timestamp at which the claim becomes
	// stale. When non-zero, it takes precedence over StaleThreshold.
	StaleAt time.Time
}

ClaimNextReadyInput holds the parameters for claiming the next ready issue. The service selects the highest-priority open issue that has no active (non-stale) claim held by another author, no unresolved blockers, and matches all provided label and role filters.

type ClaimOutput

type ClaimOutput struct {
	ClaimID   string
	IssueID   string
	Author    string
	CreatedAt time.Time
	StaleAt   time.Time
}

ClaimOutput holds the result of claiming an issue.

type ClosableParentIssueRow added in v0.4.0

type ClosableParentIssueRow struct {
	// Issue is the closable parent's ID.
	Issue string `json:"issue"`
}

ClosableParentIssueRow is emitted by the closable-parent-issues check. One row per closable parent.

type CloseCompletedEpicResult

type CloseCompletedEpicResult struct {
	ID      string
	Title   string
	Closed  bool
	Message string
}

CloseCompletedEpicResult records the outcome of attempting to close a single epic.

type CloseCompletedEpicsInput

type CloseCompletedEpicsInput struct {
	// Author is the agent closing the epics. Used for claiming and commenting.
	Author string
	// DryRun, when true, returns the list of completed epics without closing
	// them.
	DryRun bool
	// IncludeTasks, when true, extends the close-completed logic to tasks that
	// have children, all of which are closed. Tasks without children are not
	// affected by this flag.
	IncludeTasks bool
}

CloseCompletedEpicsInput holds the parameters for batch-closing completed epics.

type CloseCompletedEpicsOutput

type CloseCompletedEpicsOutput struct {
	Results     []CloseCompletedEpicResult
	ClosedCount int
}

CloseCompletedEpicsOutput holds the result of batch-closing completed epics.

type CloseWithReasonInput

type CloseWithReasonInput struct {
	IssueID string
	ClaimID string
	Reason  string
}

CloseWithReasonInput holds the parameters for the atomic close-with-reason workflow. The service derives the author from the claim record, adds the reason as a comment, and closes the issue — all within a single transaction. If any step fails, neither the comment nor the state change persists.

type ClosedParentWithOpenChildRow added in v0.4.0

type ClosedParentWithOpenChildRow struct {
	// Issue is the closed parent's ID.
	Issue string `json:"issue"`
	// NonClosedChildren lists the parent's non-closed children, sorted ascending.
	NonClosedChildren []string `json:"non_closed_children"`
}

ClosedParentWithOpenChildRow is emitted by the closed-parent-with-open-child check. One row per closed parent that still has non-closed children.

type CommentDTO

type CommentDTO struct {
	// CommentID is the sequential integer ID (displayed as "comment-N").
	CommentID int64
	// DisplayID is the human-readable ID (e.g., "comment-368").
	DisplayID string
	// IssueID is the string representation of the parent issue ID.
	IssueID string
	// Author is the comment author's name.
	Author string
	// Body is the comment text.
	Body string
	// CreatedAt is the creation timestamp.
	CreatedAt time.Time
}

CommentDTO is a flat projection of a domain comment with primitive-typed fields. CLI adapters do not need to import domain/comment to read this.

type CommentFilterInput

type CommentFilterInput struct {
	// Authors filters comments by any of the listed authors (OR'd).
	Authors []string
	// CreatedAfter filters to comments created after this timestamp.
	CreatedAfter time.Time
	// AfterCommentID filters to comments with ID greater than this.
	AfterCommentID int64
	// IssueIDs scopes the search to specific issues (OR'd).
	IssueIDs []string
	// ParentIDs scopes to comments on issues that are direct children of
	// the specified parents.
	ParentIDs []string
	// TreeIDs scopes to comments on all issues in the tree rooted at the
	// specified IDs.
	TreeIDs []string
	// LabelFilters scopes to comments on issues matching these labels.
	LabelFilters []LabelFilterInput
	// FollowRefs expands the scope to include all issues referenced by any
	// issue already in scope.
	FollowRefs bool
}

CommentFilterInput defines filtering criteria for comment list and search operations. CLI commands construct this type; the service translates it to the repository's filter type internally.

type CreateIssueInput

type CreateIssueInput struct {
	Role               domain.Role
	Title              string
	Description        string
	AcceptanceCriteria string
	Priority           domain.Priority
	ParentID           string
	Labels             []LabelInput
	Relationships      []RelationshipInput
	Author             string
	Claim              bool
	IdempotencyLabel   domain.Label
}

CreateIssueInput holds the parameters for creating an issue.

IdempotencyLabel, when non-zero (Key() != ""), enables deduplication: if any non-deleted issue already carries that exact key:value label, the service returns the existing issue without mutation. When zero, creation always proceeds without a deduplication check.

type CreateIssueOutput

type CreateIssueOutput struct {
	Issue   domain.Issue
	ClaimID string // Non-empty if the issue was created as claimed.
	Skipped bool   // True if the issue was deduplicated against an existing one.
}

CreateIssueOutput holds the result of creating an issue.

When Skipped is true, a non-deleted issue carrying the same idempotency label already existed; Issue contains that existing issue and no new issue was created. ClaimID is always empty in the skipped case.

type DeferIssueInput

type DeferIssueInput struct {
	IssueID string
	ClaimID string
}

DeferIssueInput holds the parameters for deferring a claimed issue. The operation transitions the issue to the deferred state and releases the claim — both within a single transaction.

type DeleteInput

type DeleteInput struct {
	IssueID string
	ClaimID string
}

DeleteInput holds the parameters for soft-deleting an domain.

type DoctorFinding

type DoctorFinding struct {
	// Check is the check's slug (e.g., "invalid-parent-reference").
	Check string `json:"check"`
	// Description is a one-sentence description of what the check detects.
	Description string `json:"description"`
	// WhyItMatters explains the impact; present only in verbose output.
	WhyItMatters string `json:"why_it_matters,omitzero"`
	// Summary describes what was found on this run.
	Summary string `json:"summary"`
	// Affected holds per-check typed rows. Nil for checks with no row schema.
	Affected []any `json:"affected,omitzero"`
	// Fix describes how to remediate the finding.
	Fix DoctorFix `json:"fix"`
}

DoctorFinding represents a single diagnostic check finding. WhyItMatters is populated only when verbose output is requested. Affected is nil for checks that have no per-issue row schema (system and environment checks).

type DoctorFix added in v0.4.0

type DoctorFix struct {
	// Command is the np CLI command to run, when the fix is automated.
	Command string `json:"command,omitzero"`
	// Instructions provides free-form remediation guidance for manual fixes.
	Instructions string `json:"instructions,omitzero"`
}

DoctorFix describes the remediation for a DoctorFinding. Exactly one of Command or Instructions is set: Command for an executable np CLI command, Instructions for free-form manual guidance.

type DoctorInput

type DoctorInput struct {
	// MinSeverity controls display filtering; findings below this level are
	// omitted from output. Filtering does not affect the exit code.
	MinSeverity DoctorSeverity
	// LongDeferralThreshold is the duration after which a deferred issue is
	// considered stale. Zero means use the default (7 days).
	LongDeferralThreshold time.Duration
	// WorkDir is the starting directory for the dot-np-directory check's
	// upward filesystem walk. Defaults to os.Getwd() when empty.
	WorkDir string
	// DBPath is the absolute path to the database file, supplied by the
	// CLI so the database-exists check can inspect the file before (or
	// without) an open SQLite connection. Empty when no .np/ was found.
	DBPath string
	// DBOpenError carries the error from opening the database, set by the
	// CLI when the store could not be opened. The database-exists check
	// uses this to report "cannot be opened by SQLite" when the file exists
	// and is non-empty but SQLite rejected it.
	DBOpenError error
}

DoctorInput holds the parameters for running diagnostics.

type DoctorOutput

type DoctorOutput struct {
	// Errors contains all error-severity findings.
	Errors []DoctorFinding `json:"errors"`
	// Warnings contains all warning-severity findings.
	Warnings []DoctorFinding `json:"warnings"`
	// Passed lists checks that ran and found no issues; present in verbose output.
	Passed []DoctorPassedCheck `json:"passed,omitzero"`
	// Skipped lists checks skipped because a prerequisite failed; present in
	// verbose output.
	Skipped []DoctorSkippedCheck `json:"skipped,omitzero"`
}

DoctorOutput holds the results of the doctor diagnostic.

type DoctorPassedCheck added in v0.4.0

type DoctorPassedCheck struct {
	// Check is the check's slug.
	Check string `json:"check"`
	// Description is a one-sentence description of what the check verifies.
	Description string `json:"description"`
}

DoctorPassedCheck carries identifying information for a check that passed; present in verbose output.

type DoctorSeverity

type DoctorSeverity string

DoctorSeverity represents the severity of a doctor finding. The two-level model (error, warning) maps directly to the JSON contract; string values are used because the external representation matters.

const (
	// SeverityError represents a data integrity or correctness violation.
	SeverityError DoctorSeverity = "error"
	// SeverityWarning represents a workflow anomaly or misconfiguration.
	SeverityWarning DoctorSeverity = "warning"
)

func ParseDoctorSeverity

func ParseDoctorSeverity(s string) (DoctorSeverity, error)

ParseDoctorSeverity converts a string to a DoctorSeverity. Returns an error for values outside the two-level model (error, warning).

func (DoctorSeverity) String

func (s DoctorSeverity) String() string

String returns the severity as its string value.

type DoctorSkippedCheck added in v0.4.0

type DoctorSkippedCheck struct {
	// Check is the check's slug.
	Check string `json:"check"`
	// Description is a one-sentence description of what the check verifies.
	Description string `json:"description"`
	// Prerequisite is the slug of the check whose failure caused this skip.
	Prerequisite string `json:"prerequisite"`
}

DoctorSkippedCheck carries identifying information for a check that was skipped because a prerequisite failed; present in verbose output.

type EpicProgressInput

type EpicProgressInput struct {
	// Filter restricts which epics are included. When empty, all open epics
	// are returned. Callers may set Roles, ExcludeClosed, or other filter
	// fields; the service always forces the role to epic.
	Filter IssueFilterInput
	// EpicID, when non-empty, restricts the result to a single epic.
	EpicID string
}

EpicProgressInput holds the parameters for retrieving epic completion data.

type EpicProgressItem

type EpicProgressItem struct {
	ID             string
	Title          string
	State          domain.State
	Priority       domain.Priority
	SecondaryState domain.SecondaryState
	// 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.
	// This includes children with active claims (open/claimed secondary state).
	Open int
	// Blocked is the number of children with unresolved blocked_by
	// relationships (regardless of primary state).
	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 all children are closed.
	Completed bool
}

EpicProgressItem holds completion data for a single epic.

type EpicProgressOutput

type EpicProgressOutput struct {
	Items []EpicProgressItem
}

EpicProgressOutput holds the result of the epic progress query.

type FieldChangeDTO

type FieldChangeDTO struct {
	// Field is the name of the changed field.
	Field string
	// Before is the previous value (empty for additions).
	Before string
	// After is the new value (empty for removals).
	After string
}

FieldChangeDTO describes a single field change within a history entry.

type GCInput

type GCInput struct {
	IncludeClosed bool
}

GCInput holds the parameters for garbage collection.

type GCOutput

type GCOutput struct {
	DeletedIssuesRemoved int
	ClosedIssuesRemoved  int
	// ExpiredClaimsDeleted is the number of stale claim rows that were
	// removed. This count is always populated regardless of IncludeClosed.
	ExpiredClaimsDeleted int
}

GCOutput holds the result of garbage collection.

type GraphDataOutput

type GraphDataOutput struct {
	// Nodes contains all non-deleted issues as lightweight projections.
	Nodes []IssueListItemDTO
	// Relationships contains all relationships for the included issues,
	// projected as flat DTOs with string fields.
	Relationships []RelationshipDTO
}

GraphDataOutput holds the data needed to render an issue graph.

type HistoryEntryDTO

type HistoryEntryDTO struct {
	// IssueID is the string representation of the issue this entry belongs to.
	IssueID string
	// Revision is the sequential revision number within the domain.
	Revision int
	// Author is the name of the agent that performed the action.
	Author string
	// Timestamp is when the action occurred.
	Timestamp time.Time
	// EventType is the canonical string name of the event (e.g., "created",
	// "claimed", "closed").
	EventType string
	// Changes lists the field-level changes recorded in this entry.
	Changes []FieldChangeDTO
}

HistoryEntryDTO is a flat projection of a domain history entry with primitive-typed fields. CLI adapters do not need to import domain/history.

type HistoryFilterInput

type HistoryFilterInput struct {
	// Author filters entries by author name.
	Author string
	// After filters entries created after this timestamp.
	After time.Time
	// Before filters entries created before this timestamp.
	Before time.Time
}

HistoryFilterInput defines filtering criteria for history queries. CLI commands construct this type; the service translates it to the repository's filter type internally.

type ImportInput

type ImportInput struct {
	Records       []domain.ValidatedRecord
	DefaultAuthor string
	ForceAuthor   bool
}

ImportInput holds the parameters for importing validated JSONL records.

type ImportLineResult

type ImportLineResult struct {
	IdempotencyLabel domain.Label
	IssueID          domain.ID
	Skipped          bool // True if the idempotency label matched an existing issue.
	Err              error
}

ImportLineResult describes the outcome of importing a single record.

IdempotencyLabel holds the label that was used for deduplication. Its canonical string form (Key():Value()) is suitable for human-readable output. When the source record carried no idempotency label, this field is the zero Label.

type ImportOutput

type ImportOutput struct {
	Created int
	Skipped int
	Failed  int
	Results []ImportLineResult
}

ImportOutput holds the aggregate result of an import operation.

type InheritedBlocking

type InheritedBlocking struct {
	AncestorID string
	BlockerIDs []string
}

InheritedBlocking describes why an issue is not ready due to a blocked ancestor. The AncestorID identifies the first blocked ancestor, and BlockerIDs lists the unresolved blockers on that ancestor.

type InvalidParentReferenceRow added in v0.4.0

type InvalidParentReferenceRow struct {
	// Issue is the child issue's ID.
	Issue string `json:"issue"`
	// MissingParentID is the referenced parent that does not resolve to a live
	// issue.
	MissingParentID string `json:"missing_parent_id"`
}

InvalidParentReferenceRow is emitted by the invalid-parent-reference check. One row per child issue with a dangling parent reference.

type IssueFilterInput

type IssueFilterInput struct {
	// Roles filters by one or more issue roles (empty means no filter).
	Roles []domain.Role
	// States filters by one or more states (empty means no filter).
	States []domain.State
	// Ready filters to only ready issues when true.
	Ready bool
	// ParentIDs filters to children of one or more parent epics.
	// When multiple IDs are provided, issues matching any parent are included.
	ParentIDs []string
	// DescendantsOf recursively filters to all descendants of an domain.
	DescendantsOf string
	// AncestorsOf filters to the parent chain of an issue (up to the root).
	AncestorsOf string
	// LabelFilters specifies label-based filters.
	LabelFilters []LabelFilterInput
	// Orphan filters to issues that have no parent epic.
	Orphan bool
	// Blocked filters to issues that have at least one unresolved blocked_by
	// relationship (target is neither closed nor deleted).
	Blocked bool
	// ExcludeClosed hides closed issues from results when true. Ignored when
	// States explicitly includes StateClosed — an explicit state filter
	// represents intentional user selection and takes precedence.
	ExcludeClosed bool
	// IncludeDeleted includes soft-deleted issues when true.
	IncludeDeleted bool
}

IssueFilterInput defines filtering criteria for issue list and search operations. CLI commands construct this type; the service translates it to the repository's filter type internally.

type IssueListItemDTO

type IssueListItemDTO struct {
	// ID is the string representation of the issue ID (e.g., "NP-a3bxr").
	ID string
	// Role is the issue role (RoleTask or RoleEpic).
	Role domain.Role
	// State is the primary state (StateOpen, StateClosed, StateDeferred).
	// Claimed is a secondary state of open, not a primary state.
	State domain.State
	// Priority is the priority level (P0–P3).
	Priority domain.Priority
	// Title is the issue title.
	Title string
	// ParentID is the string representation of the parent issue ID, or empty
	// if the issue has no parent.
	ParentID string
	// ParentCreatedAt is the creation timestamp of the parent issue. Zero
	// when the issue has no parent.
	ParentCreatedAt time.Time
	// CreatedAt is the creation timestamp.
	CreatedAt time.Time
	// IsDeleted is true when the issue has been soft-deleted.
	IsDeleted bool
	// IsBlocked is true when the issue has at least one unresolved blocked_by
	// relationship or a blocked/deferred ancestor.
	IsBlocked bool
	// BlockerIDs contains the string representations of non-closed issue IDs
	// that directly block this issue via blocked_by relationships. Empty when
	// IsBlocked is false or blocking is inherited only.
	BlockerIDs []string
	// SecondaryState is the computed list-view secondary state (e.g.,
	// SecondaryReady, SecondaryBlocked, SecondaryActive). SecondaryNone
	// indicates no secondary state.
	SecondaryState domain.SecondaryState
	// DisplayStatus is the human-readable status (e.g., "open (ready)",
	// "open (blocked)"). Precomputed from State and SecondaryState.
	DisplayStatus string
}

IssueListItemDTO is a flat projection of a driven.IssueListItem with all domain types converted to strings. Driving adapters consume this type instead of the driven-port IssueListItem so they do not depend on domain packages. The service layer performs the conversion after retrieving data from the repository.

type IssueSummaryOutput

type IssueSummaryOutput struct {
	Open     int
	Deferred int
	Closed   int
	Ready    int
	Blocked  int
	Total    int
}

IssueSummaryOutput holds aggregate issue counts for the status dashboard. Mirrors driven.IssueSummary with an additional Total convenience field. The three primary states are open, closed, and deferred; claimed is a transient secondary state of open and is not counted separately.

type LabelFilterInput

type LabelFilterInput struct {
	// Key is the label key to match.
	Key string
	// Value is the label value to match. Empty for wildcard ("key:*").
	Value string
	// Negate inverts the filter — exclude issues matching this label.
	Negate bool
}

LabelFilterInput specifies a single label-based filter criterion.

type LabelInput

type LabelInput struct {
	// Key is the label key (e.g., "kind", "area").
	Key string
	// Value is the label value (e.g., "bug", "auth").
	Value string
}

LabelInput is a service-layer DTO for specifying a label key-value pair. CLI adapters construct this type from raw strings; the service implementation validates and converts to the domain domain.Label type internally.

type LabelKeyOutput added in v0.3.0

type LabelKeyOutput struct {
	// Key is the label key (e.g., "kind", "area").
	Key string
	// PopularValues holds the 1–3 most frequently used values for this key,
	// ordered by descending usage count with an alphabetical tiebreaker.
	// It is never nil and always contains at least one entry.
	PopularValues []string
}

LabelKeyOutput is a service-layer DTO for returning a label key with its most popular values aggregated across all non-deleted issues. It is the element type returned by ListLabelPopularity.

type LabelOutput

type LabelOutput struct {
	// Key is the label key.
	Key string
	// Value is the label value.
	Value string
}

LabelOutput is a service-layer DTO for returning a label key-value pair. The service converts domain domain.Label values into this type so driving adapters do not depend on domain types.

type ListCommentsInput

type ListCommentsInput struct {
	IssueID string
	Filter  CommentFilterInput
	Limit   int
}

ListCommentsInput holds the parameters for listing comments.

type ListCommentsOutput

type ListCommentsOutput struct {
	Comments []CommentDTO
	HasMore  bool
}

ListCommentsOutput holds the result of listing comments.

type ListHistoryInput

type ListHistoryInput struct {
	IssueID string
	Filter  HistoryFilterInput
	Limit   int
}

ListHistoryInput holds the parameters for listing history.

type ListHistoryOutput

type ListHistoryOutput struct {
	Entries []HistoryEntryDTO
	HasMore bool
}

ListHistoryOutput holds the result of listing history.

type ListIssuesInput

type ListIssuesInput struct {
	Filter    IssueFilterInput
	OrderBy   OrderBy
	Direction SortDirection
	Limit     int
}

ListIssuesInput holds the parameters for listing issues.

type ListIssuesOutput

type ListIssuesOutput struct {
	Items   []IssueListItemDTO
	HasMore bool
}

ListIssuesOutput holds the result of listing issues. Items are flat service-layer DTOs with string fields — driving adapters do not need to import domain or driven-port packages to read this struct.

type LongDeferralRow added in v0.4.0

type LongDeferralRow struct {
	// Issue is the deferred issue's ID.
	Issue string `json:"issue"`
	// DeferredAt is when the issue entered the deferred state (RFC3339 UTC).
	DeferredAt time.Time `json:"deferred_at"`
	// LastActivityAt is the most recent activity timestamp (RFC3339 UTC).
	LastActivityAt time.Time `json:"last_activity_at"`
}

LongDeferralRow is emitted by the long-deferrals check. One row per stale deferred issue.

type MigrationResult added in v0.2.0

type MigrationResult struct {
	// ClaimedIssuesConverted is the number of issues whose primary state was
	// changed from "claimed" to "open" during the v1→v2 migration.
	ClaimedIssuesConverted int

	// HistoryRowsRemoved is the number of history rows deleted because their
	// event_type was "claimed" or "released", which are no longer valid in v2.
	HistoryRowsRemoved int

	// LegacyRelationshipsTranslated is the number of relationship rows that
	// were translated or dropped during v1→v2 migration: "cites" rows are
	// renamed to "refs" and "cited_by" rows are removed. Both counts are summed.
	LegacyRelationshipsTranslated int

	// IdempotencyKeysMigrated is the number of non-NULL idempotency_key column
	// values successfully written as idempotency:<value> label rows during the
	// v2→v3 migration.
	IdempotencyKeysMigrated int

	// IdempotencyKeysSkipped is the number of idempotency_key column values not
	// written because the issue already carried an idempotency label
	// (skip-on-conflict policy, v2→v3 migration).
	IdempotencyKeysSkipped int

	// InvalidLabelValuesSkipped is the number of idempotency_key column values
	// not written because domain.NewLabel rejected the stored value as invalid
	// (v2→v3 migration).
	InvalidLabelValuesSkipped int
}

MigrationResult describes the outcome of a schema migration. It is returned by Service.MigrateV1ToV2 and Service.MigrateV2ToV3 and consumed by the upgrade command to produce human-readable or JSON output. Fields that are not relevant to a particular migration step are zero.

type OneShotUpdateInput

type OneShotUpdateInput struct {
	IssueID            string
	Author             string
	Title              *string
	Description        *string
	AcceptanceCriteria *string
	Priority           *domain.Priority
	ParentID           *string
	LabelSet           []LabelInput
	LabelRemove        []string
}

OneShotUpdateInput holds the parameters for an atomic claim→update→release.

type OrderBy

type OrderBy int

OrderBy specifies the sort order for issue listings. CLI commands use these constants; the service translates them to the repository's ordering type.

const (
	// OrderByPriority sorts by priority (highest urgency first), then by
	// family-anchored creation time, then by issue ID as a tiebreaker.
	OrderByPriority OrderBy = iota

	// OrderByCreatedAt sorts by family-anchored creation time (oldest
	// family first), then by issue created_at within a family.
	OrderByCreatedAt

	// OrderByUpdatedAt sorts by family-anchored creation time, then by issue
	// created_at within a family, then by issue ID as a deterministic
	// tiebreaker. SortAscending yields oldest-first; SortDescending
	// yields newest-first — consistent with all other OrderBy values.
	OrderByUpdatedAt

	// OrderByPriorityCreated sorts by priority (highest urgency first),
	// then by the issue's own created_at (ascending), then by issue ID
	// as a deterministic tiebreaker. Unlike OrderByPriority, this variant
	// does not use family-anchored sorting. Designed for flat listing
	// commands (ready, blocked) where parent grouping is not meaningful.
	OrderByPriorityCreated

	// OrderByID sorts by issue ID ascending (lexicographic). Because IDs
	// contain a random component, this produces a stable but effectively
	// arbitrary order — useful as a neutral default when no semantic
	// ordering is explicitly requested.
	OrderByID

	// OrderByRole sorts by role name ascending (alphabetic), then by
	// issue ID as a deterministic tiebreaker.
	OrderByRole

	// OrderByState sorts by state name ascending (alphabetic), then by
	// issue ID as a deterministic tiebreaker.
	OrderByState

	// OrderByTitle sorts by title ascending (case-insensitive alphabetic),
	// then by issue ID as a deterministic tiebreaker.
	OrderByTitle

	// OrderByParentID sorts by parent issue ID (lexicographic), then by issue
	// ID as a deterministic tiebreaker. Parentless issues use an empty-string
	// sentinel, so they cluster at the start under SortAscending and at the
	// end under SortDescending — descending is the exact reverse of ascending.
	OrderByParentID

	// OrderByParentCreated sorts by parent creation time, then by issue ID as
	// a deterministic tiebreaker. Parentless issues use an empty-string
	// sentinel, so they cluster at the start under SortAscending and at the
	// end under SortDescending — descending is the exact reverse of ascending.
	OrderByParentCreated
)

type PriorityInversionRow added in v0.4.0

type PriorityInversionRow struct {
	// Issue is the child issue's ID.
	Issue string `json:"issue"`
	// Parent is the parent issue's ID.
	Parent string `json:"parent"`
	// ChildPriority is the child's priority label (e.g., "P0").
	ChildPriority string `json:"child_priority"`
	// ParentPriority is the parent's priority label (e.g., "P3").
	ParentPriority string `json:"parent_priority"`
}

PriorityInversionRow is emitted by the priority-inversions check. One row per (child, parent) pair where the child has strictly higher priority.

type PropagateLabelInput

type PropagateLabelInput struct {
	// IssueID is the parent issue whose label value is propagated.
	IssueID string
	// Key is the label key to propagate.
	Key string
	// Author is the agent performing the propagation, used for claiming
	// each descendant during one-shot updates.
	Author string
}

PropagateLabelInput holds the parameters for propagating a label from a parent issue to all its descendants.

type PropagateLabelOutput

type PropagateLabelOutput struct {
	// Value is the label value that was propagated from the parent.
	Value string
	// Propagated is the number of descendants that were updated.
	Propagated int
	// Total is the total number of descendants examined.
	Total int
}

PropagateLabelOutput holds the result of a label propagation operation.

type RelationshipDTO

type RelationshipDTO struct {
	// SourceID is the string representation of the relationship's source issue ID.
	SourceID string
	// TargetID is the string representation of the relationship's target issue ID.
	TargetID string
	// Type is the canonical string name of the relationship type (e.g.,
	// "blocked_by", "blocks", "refs", "parent_of", "child_of").
	Type string
}

RelationshipDTO is a flat projection of a domain relationship, using string fields so that driving adapters do not depend on domain types. The service layer converts domain.Relationship values into this type.

type RelationshipInput

type RelationshipInput struct {
	Type     domain.RelationType
	TargetID string
}

RelationshipInput describes a relationship to add during issue creation or update.

type ReopenInput

type ReopenInput struct {
	IssueID string
	Author  string
}

ReopenInput holds the parameters for reopening a closed or deferred domain. The service validates that the issue is in a reopenable state (closed or deferred), claims it internally, transitions it back to open, and records the appropriate history event (EventReopened or EventUndeferred).

type RepairInvalidParentsInput added in v0.4.0

type RepairInvalidParentsInput struct {
	// Author is the name to record on audit comments for each repaired issue.
	Author string
	// DryRun, when true, identifies affected issues and returns them without
	// performing any writes.
	DryRun bool
}

RepairInvalidParentsInput carries the parameters for repairing dangling parent references via the admin fix subcommand.

type RepairInvalidParentsOutput added in v0.4.0

type RepairInvalidParentsOutput struct {
	// Repaired lists each issue whose dangling parent reference was cleared.
	// In dry-run mode this slice represents what would have been fixed.
	Repaired []RepairedParentRecord
}

RepairInvalidParentsOutput holds the result of a RepairInvalidParentReferences operation.

type RepairedParentRecord added in v0.4.0

type RepairedParentRecord struct {
	// IssueID is the string representation of the issue that was repaired.
	IssueID string
	// RemovedParentID is the string representation of the dangling parent
	// that was removed from the issue.
	RemovedParentID string
}

RepairedParentRecord describes a single issue whose dangling parent reference was cleared (or would be cleared in dry-run mode).

type RestoreInput

type RestoreInput struct {
	// Reader is the driven-port reader that provides the serialised
	// backup data. The caller is responsible for constructing and
	// closing the reader.
	Reader driven.BackupReader
}

RestoreInput holds the parameters for restoring a database from backup.

type SearchCommentsInput

type SearchCommentsInput struct {
	Query   string
	IssueID string // Empty for global search.
	Filter  CommentFilterInput
	Limit   int
}

SearchCommentsInput holds the parameters for searching comments.

type SearchIssuesInput

type SearchIssuesInput struct {
	Query        string
	Filter       IssueFilterInput
	OrderBy      OrderBy
	Direction    SortDirection
	Limit        int
	IncludeNotes bool
}

SearchIssuesInput holds the parameters for searching issues.

type Service

type Service interface {

	// Init creates a new database with the given prefix.
	Init(ctx context.Context, prefix string) error

	// AgentName generates an agent name. When input.Seed is empty, the
	// returned name is randomly generated on each call. When input.Seed is
	// non-empty, the same seed value always produces the same name, allowing
	// callers to derive a stable identity from a process-level value such as
	// $PPID.
	AgentName(ctx context.Context, input AgentNameInput) (string, error)

	// GetPrefix returns the database's configured issue ID prefix.
	GetPrefix(ctx context.Context) (string, error)

	// CreateIssue creates a new issue.
	CreateIssue(ctx context.Context, input CreateIssueInput) (CreateIssueOutput, error)

	// ClaimByID claims a specific issue.
	ClaimByID(ctx context.Context, input ClaimInput) (ClaimOutput, error)

	// ClaimNextReady claims the highest-priority ready issue.
	ClaimNextReady(ctx context.Context, input ClaimNextReadyInput) (ClaimOutput, error)

	// LookupClaimIssueID returns the string representation of the issue ID
	// associated with the given claim ID. Returns domain.ErrNotFound if the
	// claim does not exist.
	LookupClaimIssueID(ctx context.Context, claimID string) (string, error)

	// LookupClaimAuthor returns the author who holds the given claim.
	// Returns domain.ErrNotFound if the claim does not exist.
	LookupClaimAuthor(ctx context.Context, claimID string) (string, error)

	// OneShotUpdate performs an atomic claim→update→release.
	OneShotUpdate(ctx context.Context, input OneShotUpdateInput) error

	// UpdateIssue updates a claimed issue's fields.
	UpdateIssue(ctx context.Context, input UpdateIssueInput) error

	// ExtendStaleThreshold extends the stale threshold on an active claim.
	ExtendStaleThreshold(ctx context.Context, issueID string, claimID string, threshold time.Duration) error

	// TransitionState changes the state of a claimed issue.
	TransitionState(ctx context.Context, input TransitionInput) error

	// CloseWithReason atomically adds a closing reason as a comment and
	// transitions the issue to closed. The author is derived from the claim
	// record. If any step fails, neither the comment nor the state change
	// persists. Returns an error if the reason is empty, the claim is invalid,
	// or the state transition is not allowed.
	CloseWithReason(ctx context.Context, input CloseWithReasonInput) error

	// DeferIssue transitions a claimed issue to the deferred state and
	// releases the claim — all within a single transaction.
	DeferIssue(ctx context.Context, input DeferIssueInput) error

	// ReopenIssue transitions a closed or deferred issue back to the open
	// state. The operation is atomic: the issue is claimed internally,
	// transitioned to open, and released — all within a single transaction.
	// Records EventReopened for closed issues or EventUndeferred for
	// deferred issues.
	ReopenIssue(ctx context.Context, input ReopenInput) error

	// DeleteIssue soft-deletes a claimed issue.
	DeleteIssue(ctx context.Context, input DeleteInput) error

	// ShowIssue returns the full detail view of an issue.
	ShowIssue(ctx context.Context, id string) (ShowIssueOutput, error)

	// ListIssues returns a filtered, ordered, paginated list of issues.
	ListIssues(ctx context.Context, input ListIssuesInput) (ListIssuesOutput, error)

	// SearchIssues performs full-text search on issues.
	SearchIssues(ctx context.Context, input SearchIssuesInput) (ListIssuesOutput, error)

	// GetIssueSummary returns aggregate issue counts by primary state and
	// computed readiness/blocked status. More efficient than calling
	// ListIssues multiple times — uses a single query internally.
	GetIssueSummary(ctx context.Context) (IssueSummaryOutput, error)

	// EpicProgress returns completion data for open epics. It uses
	// GetChildStatuses to compute progress in a single query per epic,
	// avoiding the N+1 pattern of listing children via ListIssues.
	EpicProgress(ctx context.Context, input EpicProgressInput) (EpicProgressOutput, error)

	// CloseCompletedEpics finds all open epics in the completed secondary
	// state and batch-closes them. Each epic is claimed, a closing comment
	// is added, and the epic is transitioned to closed. Per-epic failures
	// are captured in the result without aborting the batch.
	CloseCompletedEpics(ctx context.Context, input CloseCompletedEpicsInput) (CloseCompletedEpicsOutput, error)

	// ListLabelPopularity returns label keys together with their three most
	// frequently used values across all non-deleted issues (open, closed, and
	// deferred). Results are grouped by key; within each entry the
	// PopularValues slice is ordered by descending usage count with an
	// alphabetical tiebreaker, and contains between one and three entries.
	// The count in any envelope built from this output should reflect the
	// number of distinct keys, not the number of distinct key-value pairs.
	ListLabelPopularity(ctx context.Context) ([]LabelKeyOutput, error)

	// AddRelationship adds a relationship between two issues. The sourceID
	// is the string representation of the source issue ID.
	AddRelationship(ctx context.Context, sourceID string, rel RelationshipInput, author string) error

	// RemoveRelationship removes a relationship between two issues. The
	// sourceID is the string representation of the source issue ID.
	RemoveRelationship(ctx context.Context, sourceID string, rel RelationshipInput, author string) error

	// RemoveBidirectionalBlock removes any blocked_by relationship between
	// issueA and issueB regardless of which direction was stored. It tries
	// both "A blocked_by B" and "B blocked_by A". The operation is
	// idempotent — if neither relationship exists, no error is returned.
	// Both issue IDs are string representations.
	RemoveBidirectionalBlock(ctx context.Context, issueA, issueB string, author string) error

	// PropagateLabel copies a label from a parent issue to all its descendants
	// that lack it or have a different value. Each descendant is updated via
	// OneShotUpdate (atomic claim→update→release). Per-descendant failures
	// are skipped without aborting the batch.
	PropagateLabel(ctx context.Context, input PropagateLabelInput) (PropagateLabelOutput, error)

	// AddComment adds a comment to an issue.
	AddComment(ctx context.Context, input AddCommentInput) (AddCommentOutput, error)

	// ShowComment retrieves a single comment by ID.
	ShowComment(ctx context.Context, commentID int64) (CommentDTO, error)

	// ListComments lists comments for an issue.
	ListComments(ctx context.Context, input ListCommentsInput) (ListCommentsOutput, error)

	// SearchComments searches comments by text.
	SearchComments(ctx context.Context, input SearchCommentsInput) (ListCommentsOutput, error)

	// ShowHistory lists history entries for an issue.
	ShowHistory(ctx context.Context, input ListHistoryInput) (ListHistoryOutput, error)

	// GetGraphData returns all non-deleted issues and their relationships
	// in a single read-only transaction, for rendering as a graph.
	GetGraphData(ctx context.Context) (GraphDataOutput, error)

	// Doctor runs diagnostics and returns classified findings. The input
	// controls the minimum severity threshold and allows the caller to inject
	// additional findings from checks that run outside the service layer
	// (e.g., filesystem checks). The output includes per-check pass/fail
	// status and a healthy flag.
	Doctor(ctx context.Context, input DoctorInput) (DoctorOutput, error)

	// RepairInvalidParentReferences scans every non-deleted issue for a
	// parent_id that refers to an absent or soft-deleted parent, clears those
	// references, and records an audit comment on each affected issue. When
	// input.DryRun is true, the scan runs but no writes are performed and the
	// output describes what would have been fixed.
	RepairInvalidParentReferences(ctx context.Context, input RepairInvalidParentsInput) (RepairInvalidParentsOutput, error)

	// GC performs garbage collection.
	GC(ctx context.Context, input GCInput) (GCOutput, error)

	// Backup writes a complete snapshot of all non-deleted issues — with
	// their comments, labels, relationships, claims, and history — to the
	// provided BackupWriter. The operation runs in a single read
	// transaction for snapshot consistency.
	Backup(ctx context.Context, input BackupInput) (BackupOutput, error)

	// Restore replaces the entire database contents with data read from
	// the provided BackupReader. This is a destructive operation: all
	// existing data is removed before the backup is applied. The restore
	// is atomic — either the full backup is applied or the database is
	// left unchanged.
	Restore(ctx context.Context, input RestoreInput) error

	// ImportIssues creates issues from validated JSONL import records.
	ImportIssues(ctx context.Context, input ImportInput) (ImportOutput, error)

	// CountAllIssues returns the total number of issues in the database,
	// including closed and deferred issues but excluding soft-deleted ones.
	CountAllIssues(ctx context.Context) (int, error)

	// ResetDatabase removes all data from every table and reclaims disk
	// space. This is a destructive operation: all issues, comments, claims,
	// relationships, history, and labels are permanently deleted. The
	// caller is responsible for creating a backup beforehand.
	ResetDatabase(ctx context.Context) error

	// CheckSchemaVersion returns nil when the database is at the current
	// schema version (v3). It returns a wrapped domain.ErrSchemaMigrationRequired
	// when the schema is at an older version. Commands that gate on schema
	// version call this method rather than accessing the storage adapter directly.
	CheckSchemaVersion(ctx context.Context) error

	// MigrateV1ToV2 upgrades a v1 database to v2 schema in a single atomic
	// transaction. The migration converts claimed-state issues to open, removes
	// obsolete history event types, and records schema_version=2. Callers should
	// call CheckSchemaVersion first to distinguish the "already current" case
	// from a migration that made changes. Returns a MigrationResult with row
	// counts for each migration step.
	MigrateV1ToV2(ctx context.Context) (MigrationResult, error)

	// MigrateV2ToV3 upgrades a v2 database to v3 schema in a single atomic
	// transaction. The migration carries each non-NULL idempotency_key column
	// value forward as an idempotency:<value> label row (skip-on-conflict policy),
	// drops the idx_issues_idempotency unique index and the idempotency_key column,
	// and records schema_version=3. Callers should call CheckSchemaVersion first to
	// distinguish the "already current" case. Returns a MigrationResult with counts
	// for migrated, skipped, and invalid rows.
	MigrateV2ToV3(ctx context.Context) (MigrationResult, error)
}

Service defines the driving port — the use-case boundary that CLI and other adapters invoke. Each method corresponds to a command from §8 of the specification.

type ShowIssueOutput

type ShowIssueOutput struct {
	// Flat primitive-typed fields populated from the domain Issue by the
	// service implementation.
	ID                 string
	Role               domain.Role
	Title              string
	Description        string
	AcceptanceCriteria string
	Priority           domain.Priority
	State              domain.State
	ParentID           string
	Labels             map[string]string
	CreatedAt          time.Time

	Revision      int
	Author        string
	Relationships []RelationshipDTO
	IsReady       bool
	// SecondaryState is the single list-view secondary state (e.g.,
	// SecondaryReady, SecondaryBlocked). SecondaryNone indicates no secondary
	// state.
	SecondaryState domain.SecondaryState
	// DetailStates is the ordered set of secondary conditions for detail views
	// (e.g., [SecondaryBlocked, SecondaryActive]). May contain multiple
	// entries for epics. Empty or nil when there is no secondary state.
	DetailStates      []domain.SecondaryState
	InheritedBlocking *InheritedBlocking
	CommentCount      int
	ChildCount        int
	Children          []IssueListItemDTO
	Comments          []CommentDTO
	ParentTitle       string
	BlockerDetails    []BlockerDetail
	ClaimID           string
	ClaimAuthor       string
	ClaimedAt         time.Time
	ClaimStaleAt      time.Time
}

ShowIssueOutput holds the full detail view of an domain. All fields are primitive types or service-layer DTOs — CLI adapters do not need to import the domain package to read this struct.

type SortDirection added in v0.2.0

type SortDirection int

SortDirection indicates ascending or descending sort order. The zero value (SortAscending) is the standard ascending direction for every OrderBy value: lowest numeric value first, oldest timestamp first, alphabetic A–Z, etc.

const (
	// SortAscending is the default direction — lowest first, oldest first,
	// alphabetic A–Z. For timestamp-based OrderBy values (OrderByCreatedAt,
	// OrderByUpdatedAt) this means oldest issues appear first.
	SortAscending SortDirection = iota

	// SortDescending reverses the primary sort axis while keeping tiebreaker
	// columns ascending for deterministic output. For timestamp-based OrderBy
	// values (OrderByCreatedAt, OrderByUpdatedAt) this means newest issues
	// appear first.
	SortDescending
)

type TransitionAction

type TransitionAction int

TransitionAction identifies the kind of state transition.

const (
	// ActionRelease returns the issue to its default unclaimed state.
	ActionRelease TransitionAction = iota + 1

	// ActionClose marks a task as complete. Terminal.
	ActionClose

	// ActionDefer shelves the domain.
	ActionDefer
)

type TransitionInput

type TransitionInput struct {
	IssueID string
	ClaimID string
	Action  TransitionAction
}

TransitionInput holds the parameters for a state transition.

type UpdateIssueInput

type UpdateIssueInput struct {
	IssueID            string
	ClaimID            string
	Title              *string
	Description        *string
	AcceptanceCriteria *string
	Priority           *domain.Priority
	ParentID           *string
	LabelSet           []LabelInput
	LabelRemove        []string
	RelationshipAdd    []RelationshipInput
	RelationshipRemove []RelationshipInput
	CommentBody        string
}

UpdateIssueInput holds the parameters for updating a claimed domain.

Jump to

Keyboard shortcuts

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