domain

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: 17 Imported by: 0

Documentation

Overview

Package domain defines the core business types, error categories, and identity types for the nitpicking issue tracker. It includes the Issue entity and its associated value types (ID, State, Role, Priority, Label, Relationship), state machine transitions, readiness and completion derivation, parent hierarchy validation, soft-deletion rules, Author validation, agent name generation, agent instructions, backup serialisation structures (BackupHeader, BackupIssueRecord, etc.), and typed domain errors so that adapters (CLI, HTTP, etc.) can map them to appropriate exit codes or status codes without inspecting error messages.

Index

Constants

View Source
const BackupAlgorithmVersion = 3

BackupAlgorithmVersion identifies the backup format version. Restore implementations dispatch on this value to select the correct deserialisation path.

Version history:

1 — initial format; included claim rows in the backup.
2 — claims are transient and excluded from backup.
3 — IdempotencyKey removed; the field is no longer written. Existing v2
    backups that contain a non-empty idempotency_key value are accepted by
    the restore path, which carries the value forward as an ordinary label
    under the key "idempotency" (the migration-key scheme documented in
    docs/developer/decisions/idempotency-key-migration.md).
View Source
const DefaultPriority = P2

DefaultPriority is the priority assigned to issues that do not specify one.

View Source
const DefaultStaleThreshold = 2 * time.Hour

DefaultStaleThreshold is the default duration after which a claim becomes stale and eligible for stealing.

View Source
const MaxStaleThreshold = 24 * time.Hour

MaxStaleThreshold is the maximum allowed stale threshold.

Variables

View Source
var (
	// ErrNotFound indicates a requested entity (issue, comment, etc.) does not
	// exist or has been soft-deleted.
	ErrNotFound = errors.New("not found")

	// ErrIllegalTransition indicates a state machine transition that violates
	// the defined transition rules.
	ErrIllegalTransition = errors.New("illegal state transition")

	// ErrCycleDetected indicates a parent assignment or relationship would
	// create a cycle in the hierarchy.
	ErrCycleDetected = errors.New("cycle detected")

	// ErrDeletedIssue indicates an operation was attempted on a soft-deleted
	// issue, which is immutable.
	ErrDeletedIssue = errors.New("issue is deleted")

	// ErrTerminalState indicates an operation was attempted on an issue in a
	// terminal state (closed or deleted) that forbids further mutations.
	ErrTerminalState = errors.New("issue is in a terminal state")

	// ErrDepthExceeded indicates a parent assignment would exceed the maximum
	// hierarchy depth (3 levels).
	ErrDepthExceeded = errors.New("hierarchy depth exceeded")

	// ErrStaleClaim indicates an operation was attempted with a claim that has
	// passed its stale-at timestamp. The caller must re-claim the issue before
	// retrying the operation.
	ErrStaleClaim = errors.New("claim is stale")

	// ErrSchemaMigrationRequired indicates the database is at an older schema
	// version (v1) and must be upgraded before most commands can operate. The
	// caller should instruct the user to run 'np admin upgrade'.
	ErrSchemaMigrationRequired = errors.New("database schema migration required")

	// ErrCorruptDatabase indicates the database file exists but does not contain
	// the expected schema. This occurs when the file is empty, truncated, or
	// otherwise corrupt. The caller should direct the user to remove or replace
	// the file and re-run 'np init', or run 'np admin doctor' for diagnostics.
	ErrCorruptDatabase = errors.New("database is corrupt or uninitialized")

	// ErrDatabaseNotInitialized indicates that the .np/ directory exists but
	// the database file has not been created yet. The caller should direct the
	// user to run 'np init' to initialize the workspace.
	ErrDatabaseNotInitialized = errors.New("workspace not initialized")
)

Sentinel errors for domain failure categories. Adapters use errors.Is to classify these into exit codes or HTTP status codes.

View Source
var ErrEmptyAgentNameSeed = errors.New("agent name seed must not be empty")

ErrEmptyAgentNameSeed is returned by GenerateAgentNameFromSeed when it is called with an empty seed. An empty seed almost always indicates an unexpanded shell variable (e.g. `--seed=$PPID` in an environment where PPID is unset) rather than a deliberate choice; returning an error surfaces the misconfiguration at the point it originates instead of silently producing a stable but useless name shared by every affected caller.

Functions

func GenerateAgentName

func GenerateAgentName() string

GenerateAgentName produces a random agent name in the format "agent-adjective-noun-modifier" (e.g., "agent-dashing-storage-glitter"). Each invocation returns a fresh random name using the package-level PCG generator. The result is not reproducible across calls; use GenerateAgentNameFromSeed when determinism is required.

func GenerateAgentNameFromSeed added in v0.3.0

func GenerateAgentNameFromSeed(seed string) (string, error)

GenerateAgentNameFromSeed produces a deterministic agent name in the format "agent-adjective-noun-modifier" derived from seed. The same seed value always yields the same name; distinct seeds overwhelmingly produce distinct names.

An empty seed returns ErrEmptyAgentNameSeed. Empty seeds almost always indicate an unexpanded shell variable rather than a deliberate choice, and silently producing a name would make that misconfiguration very hard to diagnose.

The seed bytes are passed through SHA-256 to derive two uint64 values that seed a PCG generator. Using SHA-256 as a KDF (rather than parsing the seed string directly as an integer) ensures that the full entropy of any-length seed is mixed uniformly into the PCG state and that the derivation is well-defined across all non-empty seed inputs.

func HashClaimID

func HashClaimID(token string) string

HashClaimID computes the SHA-512 hash of a claim token after Crockford Base32 normalization. The token is normalized (lowercased, with I/L→1 and O→0 substitutions applied) before hashing, so that confusable variants of the same token produce the same hash. Returns the hex-encoded hash string.

func NormalizeCrockford

func NormalizeCrockford(s string) string

NormalizeCrockford applies Crockford Base32 decoding normalization to s. Per the spec (https://www.crockford.com/base32.html), decoding is case-insensitive and maps commonly confused characters to their intended values:

  • I/i → 1
  • L/l → 1
  • O/o → 0

All other alphabetic characters are lowercased. Digits pass through unchanged. The result is suitable for validation against the canonical Crockford alphabet.

func ResetKeyGenerate

func ResetKeyGenerate() string

ResetKeyGenerate produces a cryptographically random reset key encoded as lowercase Crockford Base32. Uses math/rand/v2, which is backed by crypto/rand by default in Go 1.22+. The key encodes 128 bits of entropy — identical to the claim ID generation algorithm.

func ResetKeyHash

func ResetKeyHash(key string) (string, error)

ResetKeyHash computes the SHA-512 hash of the binary value encoded by a Crockford Base32 reset key. The key is first normalized (lowercased, with I/L→1 and O→0 substitutions) and then decoded back to 128-bit binary before hashing. Returns the hex-encoded hash string. Returns an error if the key is not a valid 26-character Crockford Base32 string.

func Transition

func Transition(current, next State) error

Transition validates a state transition for any issue role. Returns ErrIllegalTransition if the transition is not allowed, or ErrTerminalState if the current state is terminal.

func ValidatePrefix

func ValidatePrefix(prefix string) error

ValidatePrefix checks that a prefix is 1–10 uppercase ASCII letters.

Types

type AncestorStatus

type AncestorStatus struct {
	// ID identifies the ancestor issue.
	ID ID
	// State is the ancestor's current state.
	State State
	// IsBlocked is true when the ancestor has at least one unresolved
	// blocked_by relationship. A blocked ancestor gates readiness for
	// all descendants, mirroring the behavior of deferred ancestors.
	IsBlocked bool
}

AncestorStatus summarizes an ancestor's state for readiness propagation.

type Author

type Author struct {
	// contains filtered or unexported fields
}

Author represents a validated, NFC-normalized author identifier. Authors are case-sensitive — "alice" and "Alice" are distinct. An Author is immutable after construction.

func NewAuthor

func NewAuthor(s string) (Author, error)

NewAuthor validates and NFC-normalizes the given string into an Author.

Validation rules (per §4.8):

  • At least one alphanumeric character.
  • Maximum 64 Unicode runes (measured after normalization).
  • No whitespace — no Unicode whitespace characters.

func (Author) Equal

func (a Author) Equal(other Author) bool

Equal reports whether two authors are identical. Comparison is case-sensitive on the NFC-normalized form.

func (Author) IsZero

func (a Author) IsZero() bool

IsZero reports whether the Author is the zero value (uninitialized).

func (Author) String

func (a Author) String() string

String returns the NFC-normalized author string.

type BackupClaimRecord

type BackupClaimRecord struct {
	// ClaimSHA512 is the hex-encoded SHA-512 hash of the original claim
	// token. The plaintext token is not recoverable from the backup.
	ClaimSHA512 string `json:"claim_sha512"`

	// Author is the claim holder's name.
	Author string `json:"author"`

	// StaleThreshold is the claim duration in nanoseconds, derived from
	// StaleAt minus ClaimedAt. The JSON field name is retained for backup
	// format compatibility.
	StaleThreshold int64 `json:"stale_threshold"`

	// LastActivity is the timestamp when the claim was created (claimedAt).
	// The JSON field name is retained for backup format compatibility.
	LastActivity time.Time `json:"last_activity"`
}

BackupClaimRecord is a snapshot of an active claim on an issue.

type BackupCommentRecord

type BackupCommentRecord struct {
	// CommentID is the auto-increment integer ID.
	CommentID int64 `json:"comment_id"`

	// Author is the comment author's name.
	Author string `json:"author"`

	// CreatedAt is the comment creation timestamp.
	CreatedAt time.Time `json:"created_at"`

	// Body is the comment text.
	Body string `json:"body"`
}

BackupCommentRecord is a snapshot of a single comment.

type BackupFieldChangeRecord

type BackupFieldChangeRecord struct {
	// Field is the name of the changed field.
	Field string `json:"field"`

	// Before is the value before the change.
	Before string `json:"before"`

	// After is the value after the change.
	After string `json:"after"`
}

BackupFieldChangeRecord is a single field-level before/after pair within a history entry.

type BackupHeader

type BackupHeader struct {
	// Prefix is the issue-ID prefix of the source database (e.g. "NP").
	Prefix string `json:"prefix"`

	// Timestamp is the UTC moment the backup was initiated.
	Timestamp time.Time `json:"timestamp"`

	// Version is the backup algorithm version. Restore implementations
	// use this to select the correct deserialisation logic.
	Version int `json:"version"`
}

BackupHeader is the first record in a backup file. It contains metadata about the backup itself — when it was taken, from which database prefix, and which algorithm version was used to produce it.

type BackupHistoryRecord

type BackupHistoryRecord struct {
	// EntryID is the auto-increment integer ID.
	EntryID int64 `json:"entry_id"`

	// Revision is the zero-based revision index within the issue's
	// history.
	Revision int `json:"revision"`

	// Author is the actor who performed the mutation.
	Author string `json:"author"`

	// Timestamp is when the mutation occurred.
	Timestamp time.Time `json:"timestamp"`

	// EventType is the event type string (e.g. "created", "updated").
	EventType string `json:"event_type"`

	// Changes is the list of field-level changes recorded by this entry.
	Changes []BackupFieldChangeRecord `json:"changes"`
}

BackupHistoryRecord is a snapshot of a single history entry.

type BackupIssueRecord

type BackupIssueRecord struct {

	// IssueID is the unique identifier (e.g. "NP-a3bxr").
	IssueID string `json:"issue_id"`

	// Role is "task" or "epic".
	Role string `json:"role"`

	// Title is the issue title.
	Title string `json:"title"`

	// Description is the issue body text.
	Description string `json:"description"`

	// AcceptanceCriteria is the issue's acceptance criteria text.
	AcceptanceCriteria string `json:"acceptance_criteria"`

	// Priority is the priority string (e.g. "P1", "P2").
	Priority string `json:"priority"`

	// State is the lifecycle state (e.g. "open", "closed", "deferred").
	State string `json:"state"`

	// ParentID is the parent epic's ID, or empty when unparented.
	ParentID string `json:"parent_id"`

	// CreatedAt is the issue creation timestamp in RFC 3339 with
	// nanosecond precision.
	CreatedAt time.Time `json:"created_at"`

	// Labels is the set of key–value label pairs attached to the issue.
	Labels []BackupLabelRecord `json:"labels"`

	// Comments is the ordered list of comments on the issue.
	Comments []BackupCommentRecord `json:"comments"`

	// Relationships is the list of relationships where this issue is
	// the source.
	Relationships []BackupRelationshipRecord `json:"relationships"`

	// Claims is the list of active claims on the issue. Typically zero
	// or one, but modelled as a slice for forward-compatibility.
	Claims []BackupClaimRecord `json:"claims"`

	// History is the ordered list of history entries for the issue.
	History []BackupHistoryRecord `json:"history"`
}

BackupIssueRecord is a self-contained snapshot of a single issue and all of its associated data: labels, comments, relationships, claims, and history. Each non-deleted issue in the database produces exactly one BackupIssueRecord in the backup.

type BackupLabelRecord

type BackupLabelRecord struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

BackupLabelRecord is a single key–value label on an issue.

type BackupRelationshipRecord

type BackupRelationshipRecord struct {
	// TargetID is the ID of the related issue.
	TargetID string `json:"target_id"`

	// RelType is the relationship type string (e.g. "blocked_by").
	RelType string `json:"rel_type"`
}

BackupRelationshipRecord is a snapshot of a single directed relationship.

type BlockerStatus

type BlockerStatus struct {
	// IsClosed is true if the blocker is closed (resolved).
	IsClosed bool
	// IsDeleted is true if the blocker has been soft-deleted.
	IsDeleted bool
}

BlockerStatus summarizes a blocked_by target's state for readiness checks.

type ChildStatus

type ChildStatus struct {
	// State is the child's current state.
	State State
	// IsBlocked is true when the child has at least one unresolved
	// blocked_by relationship. Used by epic progress bars to distinguish
	// open-and-blocked from open-and-ready.
	IsBlocked bool
}

ChildStatus summarizes a child issue's state for parent-close validation and epic progress computation.

type Claim

type Claim struct {
	// contains filtered or unexported fields
}

Claim represents active ownership of an issue. Claims are immutable value objects — "extending" or "updating stale deadline" produces a new Claim.

A claim carries two identifiers: the token (Crockford Base32 string returned to the caller) and the hash ID (SHA-512 hash of the token's raw bytes, hex-encoded, used for database storage and lookups). For freshly created claims, both are populated. For claims reconstructed from the database, only the hash ID is available — the plaintext token is not recoverable.

func NewClaim

func NewClaim(p NewClaimParams) (Claim, error)

NewClaim creates a new claim with a randomly generated claim ID.

func ReconstructClaim

func ReconstructClaim(id string, issueID ID, author Author, claimedAt time.Time, staleAt time.Time) Claim

ReconstructClaim rebuilds a Claim from persisted data without generating a new ID. Used by the storage layer when loading claims from the database.

func (Claim) Author

func (c Claim) Author() Author

Author returns the author who holds the claim.

func (Claim) ClaimedAt

func (c Claim) ClaimedAt() time.Time

ClaimedAt returns the timestamp when this claim was created.

func (Claim) ID

func (c Claim) ID() string

ID returns the SHA-512 hash of the claim token, hex-encoded. This is the value stored in the database and used for all persistence operations.

func (Claim) IsStale

func (c Claim) IsStale(now time.Time) bool

IsStale reports whether the claim is stale at the given time.

func (Claim) IssueID

func (c Claim) IssueID() ID

IssueID returns the ID of the claimed issue.

func (Claim) StaleAt

func (c Claim) StaleAt() time.Time

StaleAt returns the timestamp at which this claim becomes stale.

func (Claim) Token

func (c Claim) Token() string

Token returns the Crockford Base32 claim token. This is the bearer credential returned to the caller who created the claim. For claims reconstructed from the database, Token returns an empty string because the plaintext is not stored.

func (Claim) WithStaleAt

func (c Claim) WithStaleAt(t time.Time) Claim

WithStaleAt returns a new Claim with the staleAt timestamp updated. Used by adapters to extend claim lifetimes (e.g., when updating an issue pushes the stale deadline forward).

type ClaimConflictError

type ClaimConflictError struct {
	// IssueID is the ID of the issue that could not be claimed.
	IssueID string

	// CurrentHolder is the author who holds the active claim.
	CurrentHolder string

	// StaleAt is the timestamp at which the current claim becomes stale
	// and eligible for stealing.
	StaleAt time.Time
}

ClaimConflictError indicates an issue is already claimed and the claim is not stale. It carries structured context so that callers (especially AI agents) can decide whether to wait or steal.

func (*ClaimConflictError) Error

func (e *ClaimConflictError) Error() string

Error returns a human-readable description of the claim conflict.

func (*ClaimConflictError) Is

func (e *ClaimConflictError) Is(target error) bool

Is reports whether target is a *ClaimConflictError, enabling errors.Is checks against a nil *ClaimConflictError sentinel.

type Comment

type Comment struct {
	// contains filtered or unexported fields
}

Comment represents a comment attached to an issue. Comments are immutable after creation. IDs are auto-assigned sequential integers, displayed as "comment-<integer>" (e.g., "comment-368"). IDs are global across the database, not scoped per issue.

func NewComment

func NewComment(p NewCommentParams) (Comment, error)

NewComment creates a validated Comment. The body must be non-empty.

func (Comment) Author

func (c Comment) Author() Author

Author returns the comment's author.

func (Comment) Body

func (c Comment) Body() string

Body returns the comment's text content.

func (Comment) CreatedAt

func (c Comment) CreatedAt() time.Time

CreatedAt returns the comment's creation timestamp.

func (Comment) DisplayID

func (c Comment) DisplayID() string

DisplayID returns the human-readable comment ID (e.g., "comment-368").

func (Comment) ID

func (c Comment) ID() int64

ID returns the comment's sequential integer ID.

func (Comment) IssueID

func (c Comment) IssueID() ID

IssueID returns the ID of the issue this comment belongs to.

type DatabaseError

type DatabaseError struct {
	// Op describes the operation that failed (e.g., "create issue", "begin transaction").
	Op string

	// Err is the underlying storage error.
	Err error
}

DatabaseError wraps a storage-layer error with additional context. The adapter layer maps this to exit code 5.

func (*DatabaseError) Error

func (e *DatabaseError) Error() string

Error returns the operation and underlying error message.

func (*DatabaseError) Is

func (e *DatabaseError) Is(target error) bool

Is reports whether target is a *DatabaseError, enabling errors.Is checks against a nil *DatabaseError sentinel.

func (*DatabaseError) Unwrap

func (e *DatabaseError) Unwrap() error

Unwrap returns the underlying error for use with errors.Is and errors.As.

type DescendantInfo

type DescendantInfo struct {
	// ID is the descendant's issue ID.
	ID ID
	// IsClaimed is true if the descendant is currently claimed.
	IsClaimed bool
	// ClaimedBy is the author of the active claim, if any.
	ClaimedBy string
}

DescendantInfo describes a descendant issue for recursive deletion checks.

type ID

type ID struct {
	// contains filtered or unexported fields
}

ID represents an issue identifier in the form PREFIX-random (e.g., "NP-a3bxr"). The prefix is uppercase ASCII letters; the random portion is lowercase Crockford Base32 characters. IDs are immutable after construction.

func GenerateID

func GenerateID(prefix string, collisionCheck func(ID) (bool, error)) (ID, error)

GenerateID creates a new random issue ID with the given prefix. The collisionCheck callback returns true if the generated ID already exists; on collision, the function regenerates and retries up to maxRetries times.

func ParseID

func ParseID(s string) (ID, error)

ParseID parses a string of the form "PREFIX-random" into an ID. It validates that the prefix is 1–10 uppercase ASCII letters and the random portion is exactly 5 lowercase Crockford Base32 characters.

func ResolveID

func ResolveID(raw, prefix string) (ID, error)

ResolveID parses an issue ID string that may be either a full ID (PREFIX-random) or a bare random part (just the 5-char Crockford string). If the input contains a separator, it is parsed as a full ID. Otherwise, the given prefix is prepended and the result is parsed as a full ID.

func (ID) IsZero

func (id ID) IsZero() bool

IsZero reports whether the ID is the zero value (uninitialized).

func (ID) Prefix

func (id ID) Prefix() string

Prefix returns the uppercase prefix portion of the ID.

func (ID) Random

func (id ID) Random() string

Random returns the lowercase random portion of the ID.

func (ID) String

func (id ID) String() string

String returns the canonical string representation: PREFIX-random.

type Issue

type Issue struct {
	// contains filtered or unexported fields
}

Issue represents the core domain entity — either an Epic or a Task. Issues are immutable after construction; all mutation methods return a new Issue value. Revision and author are derived from history at read time, not stored on the struct.

func NewEpic

func NewEpic(p NewEpicParams) (Issue, error)

NewEpic creates a new epic issue. The title must contain at least one alphanumeric character. The ID must be valid. Priority defaults to P2 if zero.

func NewTask

func NewTask(p NewTaskParams) (Issue, error)

NewTask creates a new task issue. The title must contain at least one alphanumeric character. The ID must be valid. Priority defaults to P2 if zero.

func (Issue) AcceptanceCriteria

func (t Issue) AcceptanceCriteria() string

AcceptanceCriteria returns the issue's acceptance criteria.

func (Issue) CreatedAt

func (t Issue) CreatedAt() time.Time

CreatedAt returns the issue's creation timestamp.

func (Issue) Description

func (t Issue) Description() string

Description returns the issue's description.

func (Issue) ID

func (t Issue) ID() ID

ID returns the issue's identifier.

func (Issue) IdempotencyKey

func (t Issue) IdempotencyKey() string

IdempotencyKey returns the optional idempotency key used at creation.

func (Issue) IsDeleted

func (t Issue) IsDeleted() bool

IsDeleted reports whether the issue has been soft-deleted.

func (Issue) IsEpic

func (t Issue) IsEpic() bool

IsEpic reports whether the issue is an epic.

func (Issue) IsTask

func (t Issue) IsTask() bool

IsTask reports whether the issue is a task.

func (Issue) Labels

func (t Issue) Labels() LabelSet

Labels returns the issue's label set.

func (Issue) ParentID

func (t Issue) ParentID() ID

ParentID returns the ID of the parent epic, or zero ID if unparented.

func (Issue) Priority

func (t Issue) Priority() Priority

Priority returns the issue's priority.

func (Issue) Role

func (t Issue) Role() Role

Role returns the issue's role (task or epic).

func (Issue) State

func (t Issue) State() State

State returns the issue's current lifecycle state.

func (Issue) Title

func (t Issue) Title() string

Title returns the issue's title.

func (Issue) WithAcceptanceCriteria

func (t Issue) WithAcceptanceCriteria(ac string) Issue

WithAcceptanceCriteria returns a new issue with the updated acceptance criteria.

func (Issue) WithDeleted

func (t Issue) WithDeleted() Issue

WithDeleted returns a new issue marked as soft-deleted.

func (Issue) WithDescription

func (t Issue) WithDescription(desc string) Issue

WithDescription returns a new issue with the updated description.

func (Issue) WithLabels

func (t Issue) WithLabels(fs LabelSet) Issue

WithLabels returns a new issue with the updated label set.

func (Issue) WithParentID

func (t Issue) WithParentID(parentID ID) Issue

WithParentID returns a new issue with the updated parent epic ID. Pass a zero ID to remove the parent.

func (Issue) WithPriority

func (t Issue) WithPriority(p Priority) Issue

WithPriority returns a new issue with the updated priority.

func (Issue) WithState

func (t Issue) WithState(s State) Issue

WithState returns a new issue with the updated state. This does not validate the transition — callers must use Transition before calling this.

func (Issue) WithTitle

func (t Issue) WithTitle(title string) (Issue, error)

WithTitle returns a new issue with the updated title.

type Label

type Label struct {
	// contains filtered or unexported fields
}

Label represents a validated key–value pair attached to an issue for filtering and agent coordination.

func NewLabel

func NewLabel(key, value string) (Label, error)

NewLabel creates a Label after validating both key and value.

Key rules: 1–64 bytes, ASCII printable (0x21–0x7E), no whitespace; the first character must be an ASCII letter (A-Z or a-z) or underscore (_).

Value rules: 1–256 bytes, free-form UTF-8, no whitespace, at least one alphanumeric character.

func (Label) Key

func (f Label) Key() string

Key returns the label key.

func (Label) String added in v0.3.0

func (f Label) String() string

String returns the canonical "key:value" representation of the label.

func (Label) Value

func (f Label) Value() string

Value returns the label value.

type LabelCount added in v0.3.0

type LabelCount struct {
	// contains filtered or unexported fields
}

LabelCount pairs a label key-value combination with the number of non-deleted issues that carry it. It is used by the repository layer to communicate label usage frequency to the service layer so the service can compute per-key popularity rankings without embedding that logic in the storage layer.

func NewLabelCount added in v0.3.0

func NewLabelCount(key, value string, count int) (LabelCount, error)

NewLabelCount creates a LabelCount. The count must be positive; values of zero or below indicate a data inconsistency in the storage layer.

func (LabelCount) Count added in v0.3.0

func (lc LabelCount) Count() int

Count returns the number of non-deleted issues carrying this key-value pair.

func (LabelCount) Key added in v0.3.0

func (lc LabelCount) Key() string

Key returns the label key.

func (LabelCount) Value added in v0.3.0

func (lc LabelCount) Value() string

Value returns the label value.

type LabelSet

type LabelSet struct {
	// contains filtered or unexported fields
}

LabelSet is an ordered collection of labels with unique keys. Setting an existing key overwrites the previous value. LabelSet is immutable — all mutation methods return a new LabelSet.

func LabelSetFrom

func LabelSetFrom(labels []Label) LabelSet

LabelSetFrom creates a LabelSet from a slice of Labels. If duplicate keys appear, the last value wins.

func NewLabelSet

func NewLabelSet() LabelSet

NewLabelSet creates an empty LabelSet.

func (LabelSet) All

func (fs LabelSet) All() iter.Seq2[string, string]

All returns an iterator over all labels in the set. Iteration order is not guaranteed.

func (LabelSet) Get

func (fs LabelSet) Get(key string) (string, bool)

Get returns the value for the given key and whether it exists.

func (LabelSet) Len

func (fs LabelSet) Len() int

Len returns the number of labels in the set.

func (LabelSet) Remove

func (fs LabelSet) Remove(key string) LabelSet

Remove returns a new LabelSet with the given key removed. If the key does not exist, the returned set is identical.

func (LabelSet) Set

func (fs LabelSet) Set(f Label) LabelSet

Set returns a new LabelSet with the given label added or overwritten.

type LineError

type LineError struct {
	Line    int
	Message string
}

LineError represents a validation error on a specific line of the import file. Line is the zero-based index into the input slice.

func (LineError) Error

func (e LineError) Error() string

Error implements the error interface for display purposes.

type NewClaimParams

type NewClaimParams struct {
	IssueID       ID
	Author        Author
	StaleDuration time.Duration
	// StaleAt is an optional absolute timestamp at which the claim becomes
	// stale. When non-zero, it takes precedence over StaleDuration. The
	// caller is responsible for ensuring StaleAt is in the future and within
	// MaxStaleThreshold of Now.
	StaleAt time.Time
	Now     time.Time
}

NewClaimParams holds the parameters for creating a new claim.

type NewCommentParams

type NewCommentParams struct {
	ID        int64
	IssueID   ID
	Author    Author
	CreatedAt time.Time
	Body      string
}

NewCommentParams holds the parameters for creating a new comment.

type NewEpicParams

type NewEpicParams struct {
	ID                 ID
	Title              string
	Description        string
	AcceptanceCriteria string
	Priority           Priority
	ParentID           ID
	Labels             LabelSet
	CreatedAt          time.Time
	IdempotencyKey     string
}

NewEpicParams holds the required and optional parameters for creating a new epic.

type NewTaskParams

type NewTaskParams struct {
	ID                 ID
	Title              string
	Description        string
	AcceptanceCriteria string
	Priority           Priority
	ParentID           ID
	Labels             LabelSet
	CreatedAt          time.Time
	IdempotencyKey     string
}

NewTaskParams holds the required and optional parameters for creating a new task.

type ParseError

type ParseError struct {
	Line int
	Err  error
}

ParseError represents a JSON parsing error on a specific line of the import file. Line is the 1-based line number.

func (ParseError) Error

func (e ParseError) Error() string

Error implements the error interface.

func (ParseError) Unwrap

func (e ParseError) Unwrap() error

Unwrap returns the underlying error for use with errors.Is/As.

type Priority

type Priority int

Priority represents the urgency of an issue. Lower numbers indicate higher urgency. The default priority is P2.

const (
	// P0 is the highest urgency — critical, drop-everything priority.
	// The enum starts at iota+1 so that the zero value of Priority is not a
	// valid priority, allowing constructors to distinguish "not set" from P0.
	P0 Priority = iota + 1

	// P1 is high urgency — should be addressed soon.
	P1

	// P2 is normal urgency — the default for new issues.
	P2

	// P3 is low urgency — address when convenient.
	P3

	// P4 is the lowest urgency — nice-to-have.
	P4
)

func ParsePriority

func ParsePriority(s string) (Priority, error)

ParsePriority parses a priority string into a Priority. Accepts canonical form ("P0"–"P4"), lowercase ("p0"–"p4"), and bare numeric ("0"–"4").

func (Priority) IsHigherThan

func (p Priority) IsHigherThan(other Priority) bool

IsHigherThan reports whether p has higher urgency than other. Lower numeric value means higher urgency.

func (Priority) String

func (p Priority) String() string

String returns the canonical string representation (e.g., "P2").

type RawLine

type RawLine struct {
	// IdempotencyLabel is the full "key:value" label string used for deduplication.
	// It is required on every import line.
	IdempotencyLabel   string            `json:"idempotency_label"`
	Role               string            `json:"role"`
	Title              string            `json:"title"`
	Description        string            `json:"description"`
	AcceptanceCriteria string            `json:"acceptance_criteria"`
	Priority           string            `json:"priority"`
	State              string            `json:"state"`
	Author             string            `json:"author"`
	Comment            string            `json:"comment"`
	Claim              bool              `json:"claim"`
	Labels             map[string]string `json:"labels"`
	Parent             string            `json:"parent"`
	BlockedBy          []string          `json:"blocked_by"`
	Blocks             []string          `json:"blocks"`
	Refs               []string          `json:"refs"`
}

RawLine represents a single parsed but unvalidated line from a JSONL import file. Field names match the JSON schema defined in the JSONL import format specification. All fields are strings or string collections to defer validation to the Validate function.

func Parse

func Parse(r io.Reader) ([]RawLine, error)

Parse reads JSONL from r and returns the parsed lines. Each non-empty line is decoded as a JSON object into a RawLine. Blank lines are skipped. Parsing stops at the first JSON decode error and returns a ParseError identifying the line.

type RelationType

type RelationType int

RelationType represents the kind of relationship between two issues.

const (
	// RelBlockedBy indicates the source issue cannot make progress until
	// the target issue is closed.
	RelBlockedBy RelationType = iota + 1

	// RelBlocks is the inverse of RelBlockedBy — the source blocks the target.
	RelBlocks

	// RelRefs indicates the two issues are contextually related. Refs is
	// symmetric — there is no directional distinction. If A refs B, then
	// B refs A implicitly.
	RelRefs

	// RelParentOf indicates the source issue is the parent epic of the target.
	// This type is synthetic — it is not stored in the relationships table but
	// derived from the issues.parent_id column for display purposes.
	RelParentOf

	// RelChildOf indicates the source issue is a child of the target epic.
	// This type is synthetic — derived from issues.parent_id for display.
	RelChildOf
)

func ParseRelationType

func ParseRelationType(s string) (RelationType, error)

ParseRelationType parses a relationship type string into a RelationType.

func (RelationType) Inverse

func (rt RelationType) Inverse() RelationType

Inverse returns the inverse relationship type.

func (RelationType) IsSymmetric

func (rt RelationType) IsSymmetric() bool

IsSymmetric reports whether the relationship type is symmetric — i.e., if A relates to B, then B implicitly relates to A with the same type.

func (RelationType) String

func (rt RelationType) String() string

String returns the canonical string representation.

type Relationship

type Relationship struct {
	// contains filtered or unexported fields
}

Relationship represents a directional link between two issues.

func NewRelationship

func NewRelationship(sourceID, targetID ID, relType RelationType) (Relationship, error)

NewRelationship creates a validated relationship. It rejects self-relationships.

func SyntheticRelationship

func SyntheticRelationship(sourceID, targetID ID, relType RelationType) Relationship

SyntheticRelationship creates a relationship for display purposes that is not stored in the relationships table. Used for parent-child links derived from issues.parent_id.

func (Relationship) SourceID

func (r Relationship) SourceID() ID

SourceID returns the ID of the issue initiating the relationship.

func (Relationship) TargetID

func (r Relationship) TargetID() ID

TargetID returns the ID of the issue referenced by the relationship.

func (Relationship) Type

func (r Relationship) Type() RelationType

Type returns the relationship type.

type Role

type Role int

Role identifies whether an issue is an Epic (organizer) or a Task (leaf work unit). Role is immutable after creation.

const (
	// RoleTask represents an actionable work unit — the leaf node in the
	// issue hierarchy.
	RoleTask Role = iota + 1

	// RoleEpic represents an organizing issue whose completion is determined
	// by the completed secondary state.
	RoleEpic
)

func ParseRole

func ParseRole(s string) (Role, error)

ParseRole parses a role string ("task" or "epic") into a Role. Parsing is case-sensitive.

func (Role) String

func (r Role) String() string

String returns the canonical lowercase string representation.

type SecondaryState

type SecondaryState int

SecondaryState qualifies the primary state with additional context about an issue's readiness, progress, or blocking status. The zero value (SecondaryNone) indicates no secondary qualifier — used for closed issues or when the primary state does not warrant a qualifier.

const (
	// SecondaryNone indicates no secondary state. Used for closed issues or
	// when the primary state does not warrant a qualifier.
	SecondaryNone SecondaryState = iota

	// SecondaryClaimed indicates an open issue has an active (non-stale) claim.
	// Claimed takes precedence over ready and blocked in display priority.
	SecondaryClaimed

	// SecondaryReady indicates an issue is available for work (tasks) or
	// decomposition (epics with no children).
	SecondaryReady

	// SecondaryBlocked indicates the issue has unresolved blockers or a
	// blocked/deferred ancestor.
	SecondaryBlocked

	// SecondaryUnplanned indicates an epic has no children and needs
	// decomposition. Used in detail views alongside SecondaryBlocked when
	// an unplanned epic is also blocked.
	SecondaryUnplanned

	// SecondaryActive indicates an epic has children but not all are closed.
	SecondaryActive

	// SecondaryCompleted indicates an epic has children and all are closed.
	SecondaryCompleted
)

func ParseSecondaryState

func ParseSecondaryState(s string) (SecondaryState, error)

ParseSecondaryState parses a secondary state string. Parsing is case-sensitive. The empty string is not accepted — callers should check for SecondaryNone explicitly rather than parsing it.

func (SecondaryState) String

func (s SecondaryState) String() string

String returns the canonical string representation. Returns an empty string for SecondaryNone.

type SecondaryStateResult

type SecondaryStateResult struct {
	// ListState is the single secondary state for list views, chosen by
	// priority: completed > claimed > blocked > ready > active.
	ListState SecondaryState

	// DetailStates is the ordered set of secondary conditions applicable in
	// detail views. May contain multiple entries (e.g., [blocked, active]
	// for a blocked epic with children).
	DetailStates []SecondaryState
}

SecondaryStateResult carries the computed secondary state for both list and detail views. ListState is a single secondary state chosen by priority rules for compact list displays. DetailStates is the full set of applicable secondary conditions for rich detail views.

func (SecondaryStateResult) HasSecondary

func (r SecondaryStateResult) HasSecondary() bool

HasSecondary reports whether this result carries any secondary state.

type State

type State int

State represents the lifecycle state of an issue. All issue roles (task and epic) share the same state machine.

const (
	// StateOpen is the default state for new issues. Available for work.
	StateOpen State = iota + 1

	// StateClosed indicates the issue is fully resolved. Closed issues can
	// be reopened if needed.
	StateClosed

	// StateDeferred indicates the issue should not be worked on now.
	StateDeferred
)

func DefaultState

func DefaultState() State

DefaultState returns the initial state for any newly created issue.

func ParseState

func ParseState(s string) (State, error)

ParseState parses a state string into a State. Parsing is case-sensitive.

func (State) IsTerminal

func (s State) IsTerminal() bool

IsTerminal reports whether the state is terminal — no further transitions are allowed. No states are currently terminal; closed issues can be reopened. "Deleted" is a separate concept checked independently.

func (State) String

func (s State) String() string

String returns the canonical lowercase string representation.

type ValidatedRecord

type ValidatedRecord struct {
	// IdempotencyLabel is the parsed label used for deduplication. Its string
	// form (Key():Value()) is also the intra-file reference key used to resolve
	// parent, blocked_by, blocks, and refs fields within the same import file.
	IdempotencyLabel   Label
	Role               Role
	Title              string
	Description        string
	AcceptanceCriteria string
	Priority           Priority
	State              State
	Author             string
	Comment            string
	// Claim indicates the imported issue should be immediately claimed after
	// creation. Only valid when State is open; the import service returns an
	// error if Claim is true for a non-open record.
	Claim     bool
	Labels    []Label
	Parent    string
	BlockedBy []string
	Blocks    []string
	Refs      []string
}

ValidatedRecord is a successfully validated import line with parsed domain types. It retains all data needed by the import pass to create issues and relationships without re-parsing.

type ValidationError

type ValidationError struct {
	// Fields maps field names to human-readable descriptions of why
	// validation failed. Multiple fields may fail in a single operation.
	Fields map[string]string
}

ValidationError carries structured detail about which fields failed validation and why, enabling self-describing error responses per §9.

func NewMultiValidationError

func NewMultiValidationError(fields map[string]string) *ValidationError

NewMultiValidationError creates a ValidationError for multiple fields.

func NewValidationError

func NewValidationError(field, reason string) *ValidationError

NewValidationError creates a ValidationError for a single field.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error returns a summary of all validation failures.

func (*ValidationError) Is

func (e *ValidationError) Is(target error) bool

Is reports whether target is a *ValidationError, enabling errors.Is checks against a nil *ValidationError sentinel.

type ValidationResult

type ValidationResult struct {
	Errors  []LineError
	Records []ValidatedRecord
}

ValidationResult holds the outcome of validating an entire import file. Errors contains per-line validation failures. Records contains the successfully validated lines — only populated for lines with no errors.

func Validate

func Validate(lines []RawLine, prefix string) ValidationResult

Validate performs a two-pass validation of the given import lines.

Pass 1 collects all idempotency labels and their line indices, detecting duplicates. Pass 2 validates each line's fields and resolves references.

The prefix parameter is the database's issue ID prefix (e.g., "NP"), used to distinguish issue ID references from idempotency label references.

func (ValidationResult) HasErrors

func (r ValidationResult) HasErrors() bool

HasErrors reports whether the validation produced any errors.

Directories

Path Synopsis
Package history defines the HistoryEntry entity — an immutable, append-only record of every mutation to an issue's state.
Package history defines the HistoryEntry entity — an immutable, append-only record of every mutation to an issue's state.

Jump to

Keyboard shortcuts

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