pub mod collect;
pub mod writeback;
pub use collect::{
ExportCollectOptions, ExportCollection, ExportRule, collect_rules_for_export,
collect_rules_for_export_with_scopes, is_explicit_local_rule, repo_scope_matches,
};
pub use writeback::{
BEGIN_MARKER, END_MARKER, MarkerBlockWrite, WriteAction, WriteOutcome, has_marker_block,
upsert_marker_block,
};
use crate::context::rule_render::{RuleExportRenderInput, render_rule_export};
pub struct ExportBlockMeta<'a> {
pub tool_version: &'a str,
pub generated_at_utc: &'a str,
pub rule_count: usize,
pub repo_scopes: &'a [String],
pub local_only: bool,
}
#[must_use]
pub fn export_content_hash(body: &str) -> String {
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
hasher.update(body.as_bytes());
let digest = hasher.finalize();
let mut hex = String::with_capacity(12);
for byte in digest.iter().take(6) {
hex.push_str(&format!("{byte:02x}"));
}
hex
}
#[must_use]
pub fn render_export_body(rules: &[ExportRule]) -> String {
if rules.is_empty() {
return "_No DiffLore rules are in scope for this repo yet. Run `difflore import-reviews` to capture review memory._\n".to_owned();
}
let blocks: Vec<String> = rules
.iter()
.map(|rule| {
render_rule_export(&RuleExportRenderInput {
name: &rule.name,
repo_scope: rule.repo_scope.as_deref(),
description: &rule.description,
check_prompt: rule.check_prompt.as_deref(),
examples: (!rule.examples.is_empty()).then_some(rule.examples.as_slice()),
})
})
.collect();
blocks.join("\n---\n\n")
}
#[must_use]
pub fn build_export_block(meta: &ExportBlockMeta<'_>, body: &str) -> String {
let scope_label = if meta.repo_scopes.is_empty() {
"(no git remote detected)".to_owned()
} else {
meta.repo_scopes.join(", ")
};
let mode_segment = if meta.local_only {
" | mode: local-only"
} else {
""
};
let hash = export_content_hash(body);
format!(
"{begin}\n\
<!-- AUTO-GENERATED by difflore export v{version} - do not edit between the BEGIN/END DIFFLORE RULES markers; the next export overwrites this section. -->\n\
<!-- generated-at: {generated_at} | rules: {count} | content-hash: {hash} | repo-scope: {scope_label}{mode_segment} -->\n\
<!-- Static snapshot: it goes stale as team rules evolve. Run `difflore agents install` for live diff-aware rule injection. -->\n\
\n\
# DiffLore team rules\n\
\n\
{body}\n\
{end}",
begin = BEGIN_MARKER,
version = meta.tool_version,
generated_at = meta.generated_at_utc,
count = meta.rule_count,
end = END_MARKER,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn meta<'a>(generated_at: &'a str, scopes: &'a [String]) -> ExportBlockMeta<'a> {
ExportBlockMeta {
tool_version: "9.9.9",
generated_at_utc: generated_at,
rule_count: 2,
repo_scopes: scopes,
local_only: false,
}
}
#[test]
fn content_hash_is_stable_and_body_sensitive() {
assert_eq!(export_content_hash("body"), export_content_hash("body"));
assert_ne!(export_content_hash("body"), export_content_hash("body2"));
assert_eq!(export_content_hash("body").len(), 12);
}
#[test]
fn block_embeds_header_fields_and_markers() {
let scopes = vec!["acme/widgets".to_owned()];
let block = build_export_block(&meta("2026-06-11T00:00:00Z", &scopes), "RULES BODY");
assert!(block.starts_with(BEGIN_MARKER));
assert!(block.ends_with(END_MARKER));
assert!(block.contains("AUTO-GENERATED by difflore export v9.9.9"));
assert!(block.contains("generated-at: 2026-06-11T00:00:00Z"));
assert!(block.contains("rules: 2"));
assert!(block.contains(&format!(
"content-hash: {}",
export_content_hash("RULES BODY")
)));
assert!(block.contains("repo-scope: acme/widgets"));
assert!(block.contains("difflore agents install"));
assert!(block.contains("RULES BODY"));
}
#[test]
fn block_hash_ignores_generated_at_churn() {
let scopes = vec!["acme/widgets".to_owned()];
let a = build_export_block(&meta("2026-06-11T00:00:00Z", &scopes), "BODY");
let b = build_export_block(&meta("2026-06-12T13:37:00Z", &scopes), "BODY");
let hash = format!("content-hash: {}", export_content_hash("BODY"));
assert!(a.contains(&hash));
assert!(b.contains(&hash));
}
#[test]
fn block_marks_local_only_mode_and_missing_remote() {
let block = build_export_block(
&ExportBlockMeta {
tool_version: "1.0.0",
generated_at_utc: "2026-06-11T00:00:00Z",
rule_count: 0,
repo_scopes: &[],
local_only: true,
},
"BODY",
);
assert!(block.contains("mode: local-only"));
assert!(block.contains("repo-scope: (no git remote detected)"));
}
#[test]
fn empty_body_explains_and_points_at_import() {
let body = render_export_body(&[]);
assert!(body.contains("difflore import-reviews"));
}
}