Skip to main content

Issue

Struct Issue 

Source
pub struct Issue {
Show 38 fields pub number: u32, pub id: Option<String>, pub title: String, pub status: Status, pub labels: Labels, pub type_: Option<String>, pub priority: Priority, pub assignee: Option<String>, pub estimate: Option<String>, pub commit: Option<String>, pub gitlab_id: Option<String>, pub linear_id: Option<String>, pub mission: Option<String>, pub task: Option<String>, pub relationships: Vec<Relationship>, pub related_issues: Vec<String>, pub parent: Option<IssueRef>, pub series: Option<SeriesRef>, pub health: Option<HealthRef>, pub files: Vec<String>, pub cycles: Option<Vec<String>>, pub milestones: Option<Vec<String>>, pub projects: Option<Vec<String>>, pub epics: Option<Vec<String>>, pub reviewer: Option<String>, pub start_date: Option<String>, pub due_date: Option<String>, pub entered_at: Option<String>, pub source: Option<String>, pub time_entries: Vec<TimeEntry>, pub created: String, pub updated: String, pub filename: String, pub body: String, pub history: History, pub has_retired_links_key: bool, pub extra_frontmatter: BTreeMap<String, String>, pub archived: bool,
}
Expand description

An issue: one markdown file with YAML frontmatter.

Field order and names are frozen by contracts/file-format.md FR-012. number is a stored frontmatter key assigned by the storage layer at create time by scanning the live issue set (issue #52; previously from tix.toml [state].next_number, which is no longer persisted); this module models and validates it but never allocates. filename and body are memory-only (#[serde(skip)]).

Fields§

§number: u32

Human-facing sequential number: a convenience alias, allocated per-checkout as max(number) + 1 and allowed to collide across branches (an agent’s and a human’s tix create may both produce #7). It is stable for every surviving issue in one store (C-007) but is not an identity — use Self::id for that.

§id: Option<String>

Globally unique identity: 8 Crockford base32 characters (see crate::id), assigned at create and stored in the frontmatter and the filename prefix.

Optional so every issue written before the id schema keeps parsing unchanged; such an issue simply has no canonical id yet (and its filename is not id-prefixed). New issues always carry one. Unlike number, this value never collides by accident: it is random across clones, so two branches creating issue #7 each still create two distinct issues.

§title: String

Human title; drives the slug in the filename.

§status: Status

Column (board workflow stage) the issue sits in.

§labels: Labels

Free-form labels.

§type_: Option<String>

Issue type (free-form, e.g. bug, feature).

First-class since the type promotion; it used to be kept as a [custom_attributes] key. tix lint accepts a value only from the configured custom_types (or the built-in defaults). Optional, and skipped when absent so a file without it round-trips byte-identically.

§priority: Priority

Priority; defaults to medium.

§assignee: Option<String>

Assignee. In memory: bare handle (alice) or full Name <email> identity, verbatim. Files serialize per the rule documented on [de_assignee]/[se_assignee] (legacy @alice tolerated on read).

§estimate: Option<String>

Free-form estimate (e.g. 2, S, 3d); default null.

§commit: Option<String>

Git commit recorded when moved to done (FR-026).

§gitlab_id: Option<String>

Remote GitLab issue id after export/import.

§linear_id: Option<String>

Remote Linear issue id after export/import.

§mission: Option<String>

Spec Kitty mission slug (e.g. v3-archive-review) this issue belongs to, if any.

§task: Option<String>

Spec Kitty work package id (e.g. WP01) this issue links to, if any.

§relationships: Vec<Relationship>

Typed relationships to other issues, replacing the retired generic links: Vec<String> bag (C-001).

Each entry is a Relationship: a typed, directed edge with a stable id and a human-readable display name. The vocabulary is the closed four-kind set — blocks, blocked_by, duplicates, related — and the kind that reaches disk is always the canonical one, so blocked_by is normalized into blocks before it is stored and a pair is never stored twice. See crate::relationships for the direction convention, which this field does not restate.

Files without the key parse (default). An empty list is omitted rather than written as []: the typed field is new, so there is no legacy expectation to preserve for it, and omitting it keeps an issue with no relationships free of a meaningless key.

§related_issues: Vec<String>

General, cross-repository references to other issues, stored verbatim.

Unlike Self::relationships (typed, number-keyed, local) this is a forward-only, user-authored list of free-form references. An entry is one of: a local issue number or selector (42, #42, a title/filename fragment); a relative or absolute path to another issue file (possibly in a sibling repository); or a repository URL (optionally with an issue reference). The grammar and resolution live in crate::related.

Entries are stored verbatim (trimmed) rather than as a re-serialized enum, so a path round-trips byte-stable; classification happens at read/render time. The list is never mirrored on write: it is user data, not a symmetric edge like the retired links:.

#[serde(default, skip_serializing_if = "Vec::is_empty")] so every file without the key parses and round-trips byte-identically, and an empty list is omitted rather than written as [].

§parent: Option<IssueRef>

This issue’s decomposition parent, as an issue number.

A parent is a decomposition — “this work is part of that” — and is a distinct concept from a typed dependency edge: blocks is a scheduling dependency, and conflating the two in one field is prohibited (C-007). It is also distinct from the mission and task Spec Kitty pointer fields, which are governance metadata and not a hierarchy (research R2).

At most one parent (C-001). The type is a single optional reference, so two parents are not expressible; the field is not a list and no code path appends to it. Child membership is derived by inverting this pointer and is never stored on the parent (C-003) — see crate::hierarchy, which owns that derivation.

#[serde(default)] so every legacy file without the key parses unchanged, and skipped when absent so a file that never had a parent serialises byte-identically to its pre-mission form.

The value is a canonical issue id. Selectors, filenames, titles, and numbers are input aliases; they never replace the canonical id in storage. Older numeric values parse for the explicit graph migration only.

A reference whose target no longer exists is kept, not cleared: it becomes the diagnosed UnresolvedParentState so it can be repaired.

§series: Option<SeriesRef>

The recurrence metadata this issue carries (series:), when it is part of a series — either the definition (it owns the rule) or one of its occurrences (it carries the occurrence key). Mission recurring-issues-01M3C7G7, data-model §1/§3/§6; see crate::recurrence::series for the typed model and the index.

One table with two roles decided by which fields are set, rather than a role: flag a writer could disagree with. Optional and skipped when absent, so every file that predates the mission parses unchanged.

§health: Option<HealthRef>

The current health value (mission project-issue-health-status-01M3C8HX, WP02 T007).

A projection of the append-only Health events, kept beside them so a surface can read the state without walking the History — and only the projection: the state, how it was obtained, the policy version, and when it was last updated. The rationale lives in the event, never here, because a frontmatter-only note is exactly the untracked value C-004 forbids.

Optional and skipped when absent, so every file that predates the mission parses and re-serializes byte-identically. The state is the file’s own spelling (like SeriesRule’s fields): validating it is the health layer’s job, so a file carrying a value this build does not know still loads and is reported rather than becoming unreadable.

§files: Vec<String>

Repo-relative file paths of the governing specification (e.g. openspec/changes/<name>/specs/<cap>/spec.md). Default [].

§cycles: Option<Vec<String>>

Planning cycle names this issue belongs to (FR-005). Each entry must name a [cycles.<name>] config entry. Option so legacy files lack the key entirely; serialized only when non-empty.

§milestones: Option<Vec<String>>

Milestone names this issue belongs to (FR-005). Each entry must name a milestone file in milestones_dir. Serialized only when non-empty.

§projects: Option<Vec<String>>

Project ids this issue is associated with (mission first-class-projects-01M3BYBW, decision 01M3BYD1F3NWB0ZYXT1AMJYYVY).

Holds project ids, never names: an id is allocated once and never changes, so a project rename cannot break an association (C-002, SC-005). An issue may carry several associations (C-003), so the field is list-valued; a single-valued project field is the rejected alternative (data-model §1, contract §6).

The wire shape is the same as Self::cycles/Self::milestones — an Option<Vec<String>>, #[serde(default)] on read and serialized only when non-empty — so a file that never had an association round-trips byte-identically to its pre-mission form (NFR-002). The key is projects, which is distinct from cycles, milestones, mission, and task: four list/pointer reference fields on one struct is exactly where an accidental alias would hide, so no serde alias is declared for any of them.

(issue, project_id) is unique. The service returns the resulting list and storage writes it verbatim; the shared se_planning_list renderer additionally collapses a duplicate that predates the field, preserving first-seen order.

A reference whose project is no longer in the registry is kept, not dropped: it becomes the diagnosed missing reference so it can be repaired (C-005, contract §5).

§epics: Option<Vec<String>>

Epic ids this issue is referenced by (mission epics-work-items-01M3BYW4, decision 01M3BYX33ZKN9BB275T0BNZJXD).

Holds epic ids, never names: an id is allocated once and never changes, so renaming an epic cannot break a reference (C-003). An issue may be referenced by several epics (C-003), so the field is list-valued; a single-valued epic field is the rejected alternative (contracts/epic-registry.md §3).

The wire shape is the same as Self::projects/Self::cycles/ Self::milestones — an Option<Vec<String>>, #[serde(default)] on read and serialized only when non-empty — so a file that never had a reference round-trips byte-identically to its pre-mission form (NFR-002). The key is epics, which is distinct from projects, cycles, milestones, mission, task, and files: five list-valued reference fields on one struct is exactly where an accidental alias would hide, so no serde alias is declared for any of them.

This is not a task or work-item reference (C-001): there is no task record type, and a task-labelled issue is an ordinary member. The Spec Kitty task pointer field above is unrelated and untouched.

(issue, epic_id) is unique. The service returns the resulting list and storage writes it verbatim; the shared se_planning_list renderer additionally collapses a duplicate that predates the field, preserving first-seen order.

A reference whose epic is no longer in the registry is kept, not dropped: it becomes the diagnosed missing reference so it can be repaired (C-005, contracts/epic-registry.md §5).

§reviewer: Option<String>

Advisory reviewer identity (FR-006), free-form like assignee.

§start_date: Option<String>

Planning start date, YYYY-MM-DD (FR-006).

§due_date: Option<String>

Planning due date, YYYY-MM-DD; must be >= start_date (FR-007).

§entered_at: Option<String>

When the issue most recently entered the triage inbox, RFC 3339 UTC (2026-09-30T09:12:44Z), if it ever did.

Mission triage-inbox-review-flow-01M3C600, data-model §2. Queue age is measured from here and from nothing else: updated moves on every edit and created predates the queue, so either would report a wait the issue did not have (research R4). Read it through crate::triage::entry_time, which is the one documented fallback for an issue that has none.

Optional and skipped when absent, so a repository that predates triage round-trips byte-identically instead of gaining a key on every file (C-001).

Refreshed on re-entry, not merged — a defer restarts the wait (data-model §5) — and deliberately not stamped on every edit, which would turn it into a second updated.

§source: Option<String>

Where the issue’s current entry came from: new, import, defer, or any other origin the caller documents (data-model §2, source).

Optional for the same reason as Self::entered_at. It describes this entry rather than the issue’s permanent provenance, so it is rewritten whenever the issue re-enters.

§time_entries: Vec<TimeEntry>

The issue’s current time entries (mission per-issue-time-tracking-01M3C9A4, WP01 T004), under the frontmatter key time_entries.

Each element is a crate::time_tracking::TimeEntry: actual effort at minute precision, with its own stable id, optional calendar date, author and note. This is the current projection only — corrections and deletions stay in the append-only History, never here (C-004) — and it is not the free-form estimate field above, which keeps its planning meaning (C-005). Nothing about an entry changes status or ownership (C-008).

#[serde(default, skip_serializing_if = "Vec::is_empty")] so a file that never had an entry parses unchanged and round-trips byte-identically instead of gaining a time_entries: [] key, which is the same rule Self::relationships and Self::related_issues follow.

An entry that fails crate::time_tracking::TimeEntry::validate is kept in this list rather than dropped, so a malformed entry stays readable and reportable instead of blocking the issue or vanishing on the next unrelated write (C-005).

§created: String

Creation date, YYYY-MM-DD, the leading component of the filename.

§updated: String

Last-mutation date, YYYY-MM-DD.

§filename: String

Issue file name — path relative to the issues dir. A new-schema name is {date}-{id}-{slug} (e.g. alice/type/status/2026-05-10-A1B2C3D4-fix-login.md), so a directory listing is creation order; a filename written before the id schema is {date}-{slug} (e.g. 2026-05-10-fix-login.md, or grouped per C-010).

§body: String

Raw markdown body; memory-only, preserved verbatim.

§history: History

The append-only ## History section, parsed out of the body on read and re-rendered on write; memory-only like Issue::body.

The section is stripped from body during parsing, so body holds only the issue’s own prose and the two never disagree. Rendering puts the section back immediately after the frontmatter, the position data-model.md §8 pins.

§has_retired_links_key: bool

Whether the source frontmatter still carries a legacy links: key.

The key is never read as typed data (C-001) and never migrated; it is recorded here so the service can report it as a retired_links_key validation result. Memory-only, like the rest of the file-surface state.

§extra_frontmatter: BTreeMap<String, String>

Top-level frontmatter keys this model does not know, as key -> raw block.

#1361. Before this, an unknown key was dropped on every write: Issue deserialises a fixed set of keys and frontmatter re-emits exactly those, so a key tix does not recognise was deleted by the next unrelated tix edit — silently, because tix lint had no unknown-key rule at all.

The value is the raw source block, not a parsed value, and that is the whole design. Re-serialising a parsed value would re-quote and re-indent it, so a key that was written one way would come back another way and every write would produce a spurious diff. Splicing the original bytes back in makes the round trip byte-stable for content this model has no opinion about.

A BTreeMap rather than a HashMap so the render order is deterministic: HashMap iteration order would reshuffle these keys on every write and produce a diff in a file that had not changed.

Eq is preserved (unlike serde_yaml::Value, which is not Eq), and the blocks are plain Strings, so Issue keeps every one of its derives.

The retired links: key is excluded — see Issue::parse. It is recognised, reported by its own finding, and dropped (C-001); round-tripping it would silently undo the typed-relationship migration.

§archived: bool

Whether the issue is archived (issue #1492).

The filename is the marker, never a frontmatter field: an archived issue’s markdown file is dot-prefixed in place (docs/issues/<assignee>/.<file>.md). This field is therefore derived state, not data — it mirrors the leading dot of Issue::filename and is set by crate::storage::Store::list_all, never written to disk. #[serde(skip)] keeps it out of the frontmatter, and it defaults to false for every parse.

Implementations§

Source§

impl Issue

Source

pub fn parse(contents: &str) -> Result<Self>

Parse a full issue file (frontmatter + body) from its text.

The first line must be ---; the closing --- terminates the frontmatter. The body after it is preserved verbatim (the single structural separator blank line is consumed).

Source

pub fn frontmatter(&self) -> String

Serialize just the frontmatter YAML (no delimiters), with None fields omitted except created/updated which are always present.

Source

pub fn to_file_contents(&self) -> String

Render the full issue file: ---, frontmatter, closing ---, blank separator and the raw body.

Source

pub fn issue_view(&self) -> IssueView<'_>

A borrowed IssueView of this issue, for the relationship service.

Keeps the service off the full Issue surface: it needs only the number, title, status, relationships, and whether the legacy links: key is still present.

Source

pub fn filename_stem(&self) -> String

The filename stem: {created}-{id}-{slug} when the issue has an id, or the legacy {created}-{slug} when it does not.

The date leads so a lexicographic directory listing is creation order; the id follows so the name is globally unique across branches (#1587). A legacy issue keeps the date-leading name it was written with, because renaming it would be a migration this feature deliberately does not do.

Source

pub fn bump_updated(&mut self)

Record a mutation by stamping today’s date into updated.

Source

pub fn set_dates( &mut self, start_date: Option<String>, due_date: Option<String>, ) -> Result<()>

Set start_date/due_date together after atomic validation (FR-007): both must parse as calendar dates and due_date >= start_date when both are present (delegates to validate_issue_dates).

Source

pub fn description(&self) -> &str

The description: everything above the first top-level ## Comments heading, trimmed of trailing whitespace. With no section, the whole body is the description.

Source

pub fn comments(&self) -> Vec<Comment>

The comments parsed from the ## Comments section, in file order (NFR-005). With no section — or a section with no ### heads — returns an empty vec.

Source

pub fn add_comment(&mut self, author: &str, text: &str)

Append a comment to the ## Comments section.

Whitespace-only text is discarded and the body is left untouched (FR-004 case 4). Otherwise the section is created if absent and the comment block is appended after the last existing comment. The description and any existing comments are never rewritten.

Trait Implementations§

Source§

impl Clone for Issue

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Issue

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Issue

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl EpicNode for Issue

Source§

fn number(&self) -> u32

The issue’s number, which is its identity within one snapshot.
Source§

fn title(&self) -> &str

The issue’s title, used only for the derived display label.
Source§

fn status(&self) -> &str

The issue’s raw status, compared against the board’s completion column.
Source§

fn epic_ids(&self) -> &[String]

The stored epic ids, in the order the file carries them.
Source§

impl Eq for Issue

Source§

impl HealthNode for Issue

The WP04 adapter for project health.

crate::projects::HealthNode is a WP04-local trait, and src/projects.rs stays free of the issue model (its module contract). WP03 owns src/model.rs, which this WP may not edit, so the one impl for the real record lives here, in the work package’s authoritative surface.

Source§

fn assignee(&self) -> Option<&str>

The issue’s assignee, None when unset. A blank value counts as unset; assigned_keys is where that rule lives.
Source§

impl HierarchyNode for Issue

Source§

fn number(&self) -> u32

The issue’s number, which is its stable identity.
Source§

fn title(&self) -> &str

The issue’s title, used only for the derived display label.
Source§

fn status(&self) -> &str

The issue’s raw status, compared against the caller’s completion column.
Source§

fn canonical_id(&self) -> Option<&str>

The canonical issue id, when the record has one.
Source§

fn filename(&self) -> Option<&str>

The store-relative filename used by the shared alias index.
Source§

fn parent_ref(&self) -> Option<&IssueRef>

The stored canonical parent reference. None means no parent.
Source§

fn parent_number(&self) -> Option<u32>

Legacy numeric parent alias used by in-memory test nodes.
Source§

impl IdentityNode for Issue

Source§

fn canonical_id(&self) -> Option<&str>

The canonical issue id, when this record has one.
Source§

fn issue_number(&self) -> u32

The human-facing number alias.
Source§

fn issue_filename(&self) -> &str

The store-relative filename.
Source§

fn issue_title(&self) -> &str

The current title.
Source§

impl PartialEq for Issue

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl ProjectNode for Issue

Source§

fn number(&self) -> u32

The issue’s number, which is its identity within one snapshot.
Source§

fn title(&self) -> &str

The issue’s title, used only for the derived display label.
Source§

fn status(&self) -> &str

The issue’s raw status.
Source§

fn project_ids(&self) -> &[String]

The stored project ids, in the order the file carries them.
Source§

impl Serialize for Issue

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Issue

Auto Trait Implementations§

§

impl Freeze for Issue

§

impl RefUnwindSafe for Issue

§

impl Send for Issue

§

impl Sync for Issue

§

impl Unpin for Issue

§

impl UnsafeUnpin for Issue

§

impl UnwindSafe for Issue

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.