agentis-ctx 0.3.3

Fast CLI tool that generates AI-ready context from your codebase, with built-in code intelligence
Documentation
//! Generated-file headers and content checksums for `ctx harness`.
//!
//! Every file that `ctx harness init` writes carries a two-line header:
//!
//! ```text
//! # generated by ctx v0.2.1 — regenerate with 'ctx harness init'
//! # ctx:checksum sha256:<hex>
//! ```
//!
//! The checksum is a SHA-256 over the file content *minus* every
//! `ctx:checksum` line itself (so the recorded value does not depend on
//! itself), which lets `init` distinguish files it owns and the user has not
//! touched (safe to regenerate) from user-modified or foreign files (never
//! overwritten without `--force`).
//!
//! JSON files cannot carry comments; they get no in-file header and are
//! tracked exclusively through the `.ctx/harness.lock` manifest instead.

use sha2::{Digest, Sha256};

/// Where (and how) the generated-file header is written.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeaderStyle {
    /// `#` comments right after the `#!` shebang line.
    Shell,
    /// `#` comments at the top of the file.
    Toml,
    /// `#` comment lines inserted *inside* the YAML frontmatter (after the
    /// opening `---`), so frontmatter parsers still see valid YAML.
    YamlFrontmatter,
    /// HTML comments at the top of the file (plain Markdown).
    HtmlComment,
    /// No in-file header (JSON); tracked via `.ctx/harness.lock` only.
    None,
}

/// The `generated by ...` header line (without comment leader).
fn generated_line(version: &str) -> String {
    format!("generated by ctx v{version} — regenerate with 'ctx harness init'")
}

/// True if `line` is a `ctx:checksum` line under any comment leader
/// (`#`, `//`, or `<!--`).
fn is_checksum_line(line: &str) -> bool {
    let trimmed = line.trim();
    let body = trimmed
        .strip_prefix("<!--")
        .or_else(|| trimmed.strip_prefix("//"))
        .or_else(|| trimmed.strip_prefix('#'))
        .unwrap_or(trimmed);
    body.trim_start().starts_with("ctx:checksum")
}

/// SHA-256 (hex) of `bytes` with every `ctx:checksum` line removed.
///
/// Non-UTF-8 content has no checksum lines to strip and is hashed as-is.
pub fn content_checksum(bytes: &[u8]) -> String {
    let mut hasher = Sha256::new();
    match std::str::from_utf8(bytes) {
        Ok(text) => {
            for line in text.split_inclusive('\n') {
                if !is_checksum_line(line) {
                    hasher.update(line.as_bytes());
                }
            }
        }
        Err(_) => hasher.update(bytes),
    }
    format!("{:x}", hasher.finalize())
}

/// Extract the checksum recorded in a file's `ctx:checksum` line, if any.
pub fn recorded_checksum(content: &str) -> Option<String> {
    for line in content.lines() {
        if is_checksum_line(line) {
            let after = line.split("ctx:checksum").nth(1)?;
            let token = after.split_whitespace().next()?;
            let hex = token.strip_prefix("sha256:").unwrap_or(token);
            let hex = hex.trim_end_matches("-->").trim();
            if !hex.is_empty() {
                return Some(hex.to_string());
            }
        }
    }
    None
}

/// Extract the ctx version recorded in a file's `generated by ctx v...`
/// header line, if any.
pub fn generated_version(content: &str) -> Option<String> {
    for line in content.lines() {
        if let Some(after) = line.split("generated by ctx v").nth(1) {
            let version: String = after
                .chars()
                .take_while(|c| c.is_ascii_digit() || *c == '.' || *c == '-' || c.is_alphanumeric())
                .collect();
            if !version.is_empty() {
                return Some(version);
            }
        }
    }
    None
}

/// Insert the generated-file header (with a real checksum) into `content`.
///
/// For [`HeaderStyle::None`] the content is returned unchanged.
pub fn finalize(content: &str, style: HeaderStyle, version: &str) -> String {
    const PLACEHOLDER: &str = "{{CHECKSUM}}";

    let headered = match style {
        HeaderStyle::None => return content.to_string(),
        HeaderStyle::Shell => {
            let header = format!(
                "# {}\n# ctx:checksum sha256:{}\n",
                generated_line(version),
                PLACEHOLDER
            );
            match content.split_once('\n') {
                Some((first, rest)) if first.starts_with("#!") => {
                    format!("{first}\n{header}{rest}")
                }
                _ => format!("{header}{content}"),
            }
        }
        HeaderStyle::Toml => format!(
            "# {}\n# ctx:checksum sha256:{}\n{}",
            generated_line(version),
            PLACEHOLDER,
            content
        ),
        HeaderStyle::YamlFrontmatter => {
            let header = format!(
                "# {}\n# ctx:checksum sha256:{}\n",
                generated_line(version),
                PLACEHOLDER
            );
            match content.split_once('\n') {
                Some((first, rest)) if first.trim_end() == "---" => {
                    format!("{first}\n{header}{rest}")
                }
                _ => format!("---\n{header}---\n{content}"),
            }
        }
        HeaderStyle::HtmlComment => format!(
            "<!-- {} -->\n<!-- ctx:checksum sha256:{} -->\n{}",
            generated_line(version),
            PLACEHOLDER,
            content
        ),
    };

    // The checksum line is excluded from the hash, so hashing with the
    // placeholder still present yields the final value.
    let checksum = content_checksum(headered.as_bytes());
    headered.replace(PLACEHOLDER, &checksum)
}

/// The header style implied by a generated file's relative path.
pub fn style_for_path(rel_path: &str) -> HeaderStyle {
    let name = rel_path.rsplit('/').next().unwrap_or(rel_path);
    if name.ends_with(".sh") {
        HeaderStyle::Shell
    } else if name.ends_with(".toml") || name.ends_with(".lock") {
        HeaderStyle::Toml
    } else if name == "SKILL.md" {
        HeaderStyle::YamlFrontmatter
    } else if name.ends_with(".md") {
        HeaderStyle::HtmlComment
    } else {
        // JSON and anything else that cannot carry comments.
        HeaderStyle::None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_checksum_roundtrip_shell() {
        let raw = "#!/bin/sh\nset -u\necho hello\n";
        let finalized = finalize(raw, HeaderStyle::Shell, "1.2.3");

        // Header sits right after the shebang and carries the version.
        assert!(finalized.starts_with("#!/bin/sh\n# generated by ctx v1.2.3"));
        assert!(finalized.contains("regenerate with 'ctx harness init'"));

        // Recorded checksum matches a recomputation over the final bytes.
        let recorded = recorded_checksum(&finalized).unwrap();
        assert_eq!(recorded, content_checksum(finalized.as_bytes()));
        assert_eq!(generated_version(&finalized).as_deref(), Some("1.2.3"));
    }

    #[test]
    fn test_one_byte_tamper_changes_checksum() {
        let finalized = finalize("#!/bin/sh\necho hi\n", HeaderStyle::Shell, "1.2.3");
        let recorded = recorded_checksum(&finalized).unwrap();

        let tampered = finalized.replace("echo hi", "echo ho");
        assert_ne!(recorded, content_checksum(tampered.as_bytes()));
        // The recorded line itself is untouched, so it still parses.
        assert_eq!(recorded_checksum(&tampered).unwrap(), recorded);
    }

    #[test]
    fn test_checksum_line_is_leader_agnostic() {
        // The same body hashes identically regardless of the comment leader
        // used for the ctx:checksum line.
        let body = "some content\nmore content\n";
        let hash = |c: &str| content_checksum(c.as_bytes());
        assert_eq!(
            hash(&format!("# ctx:checksum sha256:abc\n{body}")),
            hash(&format!("// ctx:checksum sha256:abc\n{body}")),
        );
        assert_eq!(
            hash(&format!("<!-- ctx:checksum sha256:abc -->\n{body}")),
            hash(body),
        );
        // Different recorded values also do not change the hash.
        assert_eq!(
            hash(&format!("# ctx:checksum sha256:abc\n{body}")),
            hash(&format!("# ctx:checksum sha256:def\n{body}")),
        );
    }

    #[test]
    fn test_json_degenerates_to_full_content_hash() {
        // HeaderStyle::None leaves content untouched; the checksum is over
        // the full content (no lines are stripped from JSON).
        let json = "{\n  \"name\": \"ctx\"\n}\n";
        assert_eq!(finalize(json, HeaderStyle::None, "1.2.3"), json);

        let mut hasher = Sha256::new();
        hasher.update(json.as_bytes());
        assert_eq!(
            content_checksum(json.as_bytes()),
            format!("{:x}", hasher.finalize())
        );
    }

    #[test]
    fn test_yaml_frontmatter_header_stays_inside_frontmatter() {
        let raw = "---\nname: ctx\ndescription: test\n---\n\n# Body\n";
        let finalized = finalize(raw, HeaderStyle::YamlFrontmatter, "1.2.3");
        assert!(finalized.starts_with("---\n# generated by ctx v1.2.3"));
        // Frontmatter fence count is unchanged (header is inside it).
        assert_eq!(finalized.matches("---\n").count(), 2);
        let recorded = recorded_checksum(&finalized).unwrap();
        assert_eq!(recorded, content_checksum(finalized.as_bytes()));
    }

    #[test]
    fn test_html_comment_header() {
        let raw = "# Title\n\nBody text.\n";
        let finalized = finalize(raw, HeaderStyle::HtmlComment, "1.2.3");
        assert!(finalized.starts_with("<!-- generated by ctx v1.2.3"));
        let recorded = recorded_checksum(&finalized).unwrap();
        assert_eq!(recorded, content_checksum(finalized.as_bytes()));
    }

    #[test]
    fn test_style_for_path() {
        assert_eq!(
            style_for_path(".claude/hooks/ctx/stop.sh"),
            HeaderStyle::Shell
        );
        assert_eq!(style_for_path(".ctx/rules.toml"), HeaderStyle::Toml);
        assert_eq!(style_for_path(".ctx/harness.lock"), HeaderStyle::Toml);
        assert_eq!(
            style_for_path("skills/ctx/SKILL.md"),
            HeaderStyle::YamlFrontmatter
        );
        assert_eq!(style_for_path("README.md"), HeaderStyle::HtmlComment);
        assert_eq!(
            style_for_path(".claude-plugin/plugin.json"),
            HeaderStyle::None
        );
    }
}