tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! `tix lint`: validate frontmatter option values in every issue file
//! (`contracts/cli.md` §lint, FR-015..FR-018).
//!
//! Checks per issue (FR-016): `status` ∈ `tix.toml [columns]`, `priority` ∈
//! {low, medium, high}, `created`/`updated` parse as `YYYY-MM-DD`, `estimate`
//! absent or a readable duration (`3`, `3d`, `1 day`, `0.5-1 day`; non-estimate
//! comma-separated annotations are ignored), `number` integer. Unknown frontmatter
//! keys are never flagged.
//!
//! Lint walks the store itself (mirroring [`crate::storage::Store::list`]
//! layout: one segment level + flat root, skipping `.tix-tmp-*` dirs and
//! non-`.md` files) instead of calling [`Store::list`], because `list` abort
//! on the first malformed file. Lint must keep scanning and report each
//! broken file as a `missing`/`parse` diagnostic — never a panic (FR-018).
//!
//! Diagnostics render through `annotate-snippets` (C-004): one rustc-style
//! block per finding, and every block names the file, line, offending value
//! and valid set in a single reading (NFR-006). Diagnostics go to **stderr**;
//! stdout stays parseable and only carries the `Lint OK: N issues checked.`
//! summary on a healthy store. Exit semantics live at the CLI boundary
//! (`cli::run_lint`): `0` healthy, `1` findings, `2` usage/internal.
//!
//! **Only errors set the exit code** (#1361). A finding carries a
//! [`Severity`]: an unrecognised top-level frontmatter key is a *warning*,
//! because the key is preserved on write and the file is therefore valid. It is
//! rendered with `Level::WARNING` and a `W-` id so the output agrees with the
//! exit code, and `[custom_attributes]` in `tix.toml` silences it for keys the
//! project uses on purpose.

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};

use crate::commands::create::planner_for;
use crate::config::Config;
use crate::diagnostics::Diagnostic;
use crate::error::{Result, TixError};
use crate::model::{Issue, Priority};
use crate::planning::Planner;
use crate::related::{self, RelatedRef};
use crate::relationships::{IssueView, RelationshipGraph};
use crate::storage::Store;
use crate::workflow::{DiagnosticKind, validate_config};

mod collect;
mod validate;
pub use collect::*;
#[allow(clippy::wildcard_imports)]
use validate::*;

/// How much a finding should worry the reader.
///
/// Added for #1361. Everything that existed before is an [`Severity::Error`], and
/// the distinction is exactly one of consequence: only errors set the exit code.
/// A file carrying an unrecognised key is still a **valid** file — it parses, and
/// the key is now preserved on write — so failing a build over it would be
/// reporting a non-problem as a broken one.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    /// Sets the exit code. A real problem with the file.
    Error,
    /// Printed, counted, and **not** fatal. Worth knowing; not worth failing over.
    Warning,
}

/// One lint finding, carrying enough location to render a rustc-style
/// diagnostic (NFR-006). `line`/`column` are 1-based; file-level findings
/// (`missing`/`parse`) use `line == 0`.
#[derive(Debug)]
pub struct TixIssueFinding {
    /// Whether this finding sets the exit code. See [`Severity`].
    pub severity: Severity,
    /// Issue file, relative to the issues dir.
    pub file: String,
    /// 1-based line of the offending value; `0` for file-level findings.
    pub line: usize,
    /// 1-based column of the offending value; `0` for file-level findings.
    pub column: usize,
    /// The offending value (or an empty string for file-level findings).
    pub value: String,
    /// `status`/`priority`/`created`/`updated`/`estimate`/`number`/`missing`/`parse`.
    pub kind: String,
    /// Human message, e.g. the valid set or the parse error.
    pub message: String,
}

#[cfg(test)]
mod tests;