use crate::standards_review_patch::{NewSpan, new_spans};
use crate::standards_review_text::{utf16_length, utf16_prefix};
use std::collections::BTreeMap;
pub const TRACKED_LOG_RELATIVE: &str = "doc/audit/standards-review-gate-shadow.md";
pub const SHARED_LOG_RELATIVE: &str = "monochromatic-git-policy-cli/standards-review-shadow.md";
pub fn shared_log_path(common_dir: &std::path::Path) -> std::path::PathBuf {
return common_dir.join(SHARED_LOG_RELATIVE);
}
pub fn has_shadow_log(repository: &std::path::Path) -> bool {
return repository.join(TRACKED_LOG_RELATIVE).is_file();
}
pub const CAP_LINES: usize = 2000;
pub const MAX_EXCERPT_FILES: usize = 8;
pub const EXCERPT_CONTEXT: i64 = 30;
pub const EXCERPT_LINE_CAP: i64 = 4000;
pub const HEAD_WINDOW: i64 = 2 * EXCERPT_CONTEXT + 1;
pub const INPUT_UNIT_CAP: usize = 24000;
pub const CODE_SUFFIXES: [&str; 6] = [".ts", ".tsx", ".rs", ".js", ".mjs", ".cjs"];
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Plan {
Silent,
NoCode,
Review(Vec<String>),
}
pub fn plan_for(names: &[String]) -> Plan {
let mut only_the_log: bool = true;
for name in names {
if name != TRACKED_LOG_RELATIVE {
only_the_log = false;
break;
}
}
if only_the_log {
return Plan::Silent;
}
let mut code_files: Vec<String> = Vec::new();
for name in names {
let mut is_code: bool = false;
for suffix in CODE_SUFFIXES {
if name.ends_with(suffix) {
is_code = true;
break;
}
}
if is_code {
code_files.push(name.clone());
}
}
if code_files.is_empty() {
return Plan::NoCode;
}
return Plan::Review(code_files);
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LandedCommit {
pub sha: String,
pub subject: String,
pub patch: String,
pub contents: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReviewInput {
pub code_file_count: usize,
pub diff_line_count: usize,
pub truncated: bool,
pub model_input: String,
pub model_input_units: usize,
}
fn excerpt(name: &str, content: &str, spans: &[NewSpan]) -> (Vec<String>, i64) {
let file_lines: Vec<&str> = content.split('\n').collect();
let total: i64 = i64::try_from(file_lines.len()).unwrap_or(i64::MAX);
let mut first_changed: Option<i64> = None;
let mut last_changed: Option<i64> = None;
for span in spans {
let first: i64 = i64::try_from(span.first).unwrap_or(i64::MAX);
let last: i64 = i64::try_from(span.last).unwrap_or(i64::MAX);
first_changed = match first_changed {
Some(known) => Some(known.min(first)),
None => Some(first),
};
last_changed = match last_changed {
Some(known) => Some(known.max(last)),
None => Some(last),
};
}
let (from, to) = match (first_changed, last_changed) {
(Some(first), Some(last)) => (1.max(first.saturating_sub(EXCERPT_CONTEXT)), total.min(last.saturating_add(EXCERPT_CONTEXT))),
_ => (1, total.min(HEAD_WINDOW)),
};
let header: String = format!("// candidate excerpt of {name}, lines {from} to {to} of {total}");
let begin: usize = usize::try_from(from - 1).unwrap_or(0);
let end: usize = usize::try_from(to).unwrap_or(0);
let body: String = if begin < end { file_lines[begin..end].join("\n") } else { String::new() };
return (vec![header, body], to - from + 1);
}
pub fn build_input(commit: &LandedCommit, code_files: &[String]) -> ReviewInput {
let lines: Vec<&str> = commit.patch.split('\n').collect();
let kept: &[&str] = &lines[..lines.len().min(CAP_LINES)];
let mut parts: Vec<String> = Vec::new();
let mut excerpt_lines: i64 = 0;
for name in code_files.iter().take(MAX_EXCERPT_FILES) {
if excerpt_lines >= EXCERPT_LINE_CAP {
break;
}
let Some(content) = commit.contents.get(name) else {
continue;
};
let spans: Vec<NewSpan> = new_spans(lines.as_slice(), name);
let (pieces, covered) = excerpt(name, content, spans.as_slice());
parts.extend(pieces);
excerpt_lines += covered;
}
let full: String = format!("{}\n\n{}", kept.join("\n"), parts.join("\n\n"));
let units: usize = utf16_length(full.as_str()).min(INPUT_UNIT_CAP);
return ReviewInput {
code_file_count: code_files.len(),
diff_line_count: lines.len(),
truncated: lines.len() > CAP_LINES,
model_input: utf16_prefix(full.as_str(), INPUT_UNIT_CAP),
model_input_units: units,
};
}
#[cfg(test)]
#[path = "standards_review_input_tests.rs"]
mod tests;