tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
//! Relative Markdown links in the user-facing docs resolve to real files.
//!
//! The README, the user guides, and the reference docs cross-reference each
//! other. A renamed guide leaves a link that points nowhere, which is the
//! reader's first navigation failure and is invisible to every other test. This
//! walks the user-facing documents and checks each relative link target exists
//! on disk.

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

/// The documents whose links must resolve: the README, the top-level docs, and
/// the `docs/for-users/` guides. `docs/issues/` is excluded — those are user
/// issue files, not shipped documentation.
fn user_documents() -> Vec<PathBuf> {
    let mut docs = vec![PathBuf::from("README.md")];
    for dir in ["docs", "docs/for-users"] {
        for entry in std::fs::read_dir(dir).expect("docs directory exists") {
            let path = entry.expect("readable entry").path();
            if path.extension().and_then(|e| e.to_str()) == Some("md") {
                docs.push(path);
            }
        }
    }
    docs
}

/// Every `](target)` link target in `text`, in order.
fn link_targets(text: &str) -> Vec<String> {
    let mut targets = Vec::new();
    let mut rest = text;
    while let Some(open) = rest.find("](") {
        let after = &rest[open + 2..];
        let Some(close) = after.find(')') else { break };
        targets.push(after[..close].to_string());
        rest = &after[close..];
    }
    targets
}

/// A link target reduced to a local filesystem path, or `None` when it is not
/// a relative path (external URL, in-page anchor, or mail address).
fn local_target(target: &str) -> Option<&str> {
    let target = target.trim();
    if target.is_empty()
        || target.starts_with('#')
        || target.contains("://")
        || target.starts_with("mailto:")
    {
        return None;
    }
    let path = target.split('#').next().unwrap_or(target);
    if path.is_empty() {
        return None;
    }
    Some(path)
}

#[test]
fn relative_doc_links_resolve() {
    let mut missing = Vec::new();
    for document in user_documents() {
        let text = std::fs::read_to_string(&document)
            .unwrap_or_else(|e| panic!("{}: {e}", document.display()));
        let base: &Path = document.parent().unwrap_or_else(|| Path::new("."));
        for target in link_targets(&text) {
            let Some(local) = local_target(&target) else {
                continue;
            };
            if !base.join(local).exists() {
                missing.push(format!("{} -> {target}", document.display()));
            }
        }
    }
    assert!(
        missing.is_empty(),
        "these documented links point at files that do not exist:\n  {}",
        missing.join("\n  ")
    );
}

#[test]
fn every_mode_has_a_user_guide_referenced_from_the_readme() {
    let readme = std::fs::read_to_string("README.md").expect("README exists");
    for guide in [
        "docs/for-users/navigation.md",
        "docs/for-users/triage.md",
        "docs/for-users/cycles.md",
        "docs/for-users/new-issue.md",
        "docs/for-users/config.md",
    ] {
        assert!(Path::new(guide).exists(), "{guide} must exist");
        assert!(readme.contains(guide), "the README must reference {guide}");
    }
}