tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! Explicit, confirmed lifecycle actions (WP05, T024–T029).
//!
//! # Eight actions, one confirmed request, one outcome each
//!
//! [`apply`] carries out exactly one of the eight actions
//! ([`LifecycleAction`]) and returns exactly one [`LifecycleOutcome`] with a stable
//! spelling — `applied`, `no-op` or `rejected`. [`LifecycleRequest::confirmed`] must
//! be set first: naming a cycle is not consent, so an unconfirmed request is refused
//! before anything is read or written (C-004). A reason is *required* for the four
//! actions whose whole point is an auditable decision — `close`, `extend`, `skip` and
//! `adjust-future` — refused by name when it is missing, and every action that writes
//! carries an explicit timestamp and actor (there is no clock here).
//!
//! # Where the evidence of an action lives
//!
//! The mission's evidence for a cycle is its **record**, its **snapshot**, the
//! **cursor**, and — for an applied decision — one line of the record's own
//! `## History` (T027). Cycles are not issues: the events are appended to the
//! **generated cycle record's Markdown body** through the crate's one
//! [`History`] grammar ([`HistoryEventKind::CycleClosed`] and siblings), never to an
//! issue, and there is no second log. No issue is created, moved, relabelled or
//! rewritten, and the normal move workflow is untouched (C-007).
//!
//! * `close` sets the cycle record's `state` to `closed`, appends a `cycle_closed`
//!   event, **and** captures an immutable snapshot under `docs/cycle_snapshots/`
//!   (the historical record, see [`crate::cycle_schedule::snapshot`]);
//! * `reopen` (mission `cycle-capacity-planning-01M3C85C`, WP04) is `close`'s inverse
//!   and deliberately writes **no** snapshot: it restores the record's `state` to
//!   `active` and appends exactly one `cycle_reopened` event naming the snapshot the
//!   close captured. The prior snapshot and every prior event are retained verbatim
//!   (C-008/NFR-002) — a reopen is the one way a closed cycle may be configured
//!   again, not a second capture;
//! * `skip` records the sequence as a cycle record with `state = "skipped"` plus a
//!   `cycle_skipped` event, creating the record from the schedule's own anchor when
//!   it does not exist yet, so a later generation sees the key and reports it a
//!   no-op (C-006);
//! * `pause`/`resume` write the schedule's own `state`, every affected instance's
//!   state (the named one, or all non-terminal ones), and one event per touched
//!   record;
//! * `extend` rewrites the instance's `due_date` to one more cadence period, and
//!   `adjust-future` rewrites every *future* instance's dates and re-derives the
//!   cursor — both through the shared [`RecurrenceRule`], never by hand;
//! * `ignore` writes nothing at all: it is the "no action" outcome and says so.
//!
//! # Every write carries the record's description and body forward
//!
//! A cycle record is a [`CycleRecord`] frontmatter plus a Markdown body. The identity
//! adapter ([`GeneratedCycle::to_record`]) has no notion of either, so a lifecycle
//! write never rebuilds a record from it: it reads the **stored** record and body
//! first (`crate::records::RecordStore::get`) and merges the change onto that. A
//! `tix cycle edit --description …`, a hand-written body, or a previously appended
//! `## History` entry therefore survives every action — and every rollback restores
//! exactly what was there, description and body included.
//!
//! # Inertness is the contract, not a nicety
//!
//! An action that cannot be coherent — closing a skipped cycle, resuming an ended
//! schedule, extending a terminal one, a cycle id that belongs to another schedule,
//! an unknown cycle, a missing reason/actor/timestamp, a malformed timestamp, a skip
//! that would fork the cursor past a hole — is `rejected` with a field-aware
//! diagnostic and **zero writes**. A **repeated** action, whose postcondition already
//! holds, is a `no-op`: it must never append a second snapshot, a second event, or
//! move a date twice. A *returned* error from the storage seam is compensated as a
//! unit — every record this call wrote is restored (or removed) and every directory it
//! created is removed when empty — so even a failure leaves the tree byte-identical
//! (or says exactly what could not be undone).
//!
//! # Idempotency by construction
//!
//! `extend` and `adjust-future` are **derived**, not accumulated: `extend` computes
//! the instance's target due date from its own (invariant) `start_date`, and
//! `adjust-future` recomputes every instance's dates from the schedule's anchor and
//! its own `sequence`. Applying either twice is therefore the *same computation*, so
//! the second call finds the postcondition already true and is a no-op.
//!
//! # No clock, no daemon
//!
//! Nothing here reads `Instant`/`SystemTime`: a close's `closed_at` and every event
//! timestamp is the explicit string the request carries, validated by
//! [`validate_timestamp`](crate::cycle_schedule::snapshot::validate_timestamp). The
//! whole module is a confirmed function call, not a service.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use time::{Date, Month};

use crate::capacity::calculate::CapacityCalculation;
use crate::capacity::snapshot::CapacitySnapshot;
use crate::config::{Config, PlanningCycle};
use crate::cycle_schedule::config::{CycleScheduleConfiguration, CycleScheduleId, ScheduleState};
use crate::cycle_schedule::identity::{
    CycleReference, GeneratedCycle, GeneratedCycleId, GeneratedCycleState, ScheduleIndex,
    cadence_rule,
};
use crate::cycle_schedule::snapshot::{
    CycleSnapshot, CycleSnapshotMetadata, CycleSnapshotStore, SnapshotOccupant, validate_timestamp,
};
use crate::cycle_store::{CycleRecord, CycleStore};
use crate::error::{Result, TixError, render_diagnostics};
use crate::model::{History, HistoryEvent, HistoryEventKind};
use crate::records::RecordStore;
use crate::recurrence::rule::{RecurrenceRule, RuleContext, RuleKind};

mod actions;
mod diagnostics;
mod model;
mod store;
pub use actions::*;
#[allow(clippy::wildcard_imports)]
use diagnostics::*;
pub use model::*;
use store::{
    StoredCycle, append_event, base_record, history_text_hazard, missing_ancestors, nominal_due,
    nth_occurrence, read_stored, reference_name, remove_empty_dirs, roll_back, roll_back_tail,
    step_from, stored_body, write_cycle,
};

// ---------------------------------------------------------------------------
// Record and History helpers
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Shared construction and arithmetic helpers
// ---------------------------------------------------------------------------