1use std::collections::HashSet;
4
5use crate::schema;
6
7use crate::EngineError;
8use crate::artefact::graph::ClassGraph;
9use crate::config::Config;
10use crate::invariants::InvariantReport;
11use crate::model::{DiffView, Disposition, GeneratedBy};
12use crate::plan::HunkId;
13use crate::shape::{Partition, hunk_digest};
14
15pub fn builtin_generated(path: &[u8]) -> bool {
19 let base = path.rsplit(|&b| b == b'/').next().unwrap_or(path);
20 const NAMES: &[&[u8]] = &[
21 b"Cargo.lock",
22 b"package-lock.json",
23 b"pnpm-lock.yaml",
24 b"yarn.lock",
25 b"bun.lockb",
26 b"composer.lock",
27 b"Gemfile.lock",
28 b"poetry.lock",
29 b"uv.lock",
30 b"go.sum",
31 b"flake.lock",
32 ];
33 if NAMES.contains(&base) {
34 return true;
35 }
36 const SUFFIXES: &[&[u8]] = &[b".lock", b".snap", b".min.js", b".min.css"];
37 SUFFIXES.iter().any(|s| base.ends_with(s))
38}
39
40pub fn mark_generated(view: &mut DiffView, config: &Config, attr_marked: &HashSet<Vec<u8>>) {
43 for f in &mut view.files {
44 let lossy = String::from_utf8_lossy(&f.path).into_owned();
45 if config.not_generated.is_match(&lossy) {
46 f.generated = None;
47 } else if config.generated.is_match(&lossy) {
48 f.generated = Some(GeneratedBy::Config);
49 } else if attr_marked.contains(&f.path) {
50 f.generated = Some(GeneratedBy::Attr);
51 } else if builtin_generated(&f.path) {
52 f.generated = Some(GeneratedBy::Builtin);
53 } else {
54 f.generated = None;
55 }
56 }
57}
58
59pub struct SourceInfo {
60 pub kind: schema::SourceKind,
61 pub base: String,
62 pub head: String,
63 pub remote: Option<schema::Remote>,
64}
65
66pub fn assemble(
69 view: &DiffView,
70 partition: &Partition,
71 graph: ClassGraph,
72 source: &SourceInfo,
73 report: &InvariantReport,
74) -> Result<schema::PlanDocument, EngineError> {
75 let files = view
76 .files
77 .iter()
78 .map(|f| {
79 Ok(schema::FileEntry {
80 path: path_str(&f.path)?,
81 disposition: match f.disposition {
82 Disposition::Added => schema::Disposition::A,
83 Disposition::Deleted => schema::Disposition::D,
84 Disposition::Modified => schema::Disposition::M,
85 },
86 mode: f.new_mode.clone(),
87 old_mode: f.old_mode.clone(),
88 old_path: f.rename_from.as_deref().map(path_str).transpose()?,
89 new_path: f.rename_to.as_deref().map(path_str).transpose()?,
90 rename_similarity: f.rename_similarity,
91 binary: f.binary,
92 submodule: f
93 .submodule
94 .as_ref()
95 .map(|(old, new)| schema::SubmoduleChange {
96 old: old.clone(),
97 new: new.clone(),
98 }),
99 generated: f.generated.is_some(),
100 generated_by: f.generated.map(|g| match g {
101 GeneratedBy::Builtin => schema::GeneratedBy::Builtin,
102 GeneratedBy::Attr => schema::GeneratedBy::Attr,
103 GeneratedBy::Config => schema::GeneratedBy::Config,
104 }),
105 hunk_ids: f
106 .hunks
107 .iter()
108 .map(|&i| HunkId::from_index(i).to_string())
109 .collect(),
110 })
111 })
112 .collect::<Result<Vec<_>, EngineError>>()?;
113
114 let hunks = view
115 .hunks
116 .iter()
117 .enumerate()
118 .map(|(i, h)| {
119 Ok(schema::HunkEntry {
120 id: HunkId::from_index(i).to_string(),
121 file: path_str(&view.file_of(h).path)?,
122 old_start: h.old_start,
123 old_count: h.old_count,
124 new_start: h.new_start,
125 new_count: h.new_count,
126 class: format!("C{}", partition.class_of[i]),
127 digest: hunk_digest(h),
128 nonl_old: h.nonl_old,
129 nonl_new: h.nonl_new,
130 forge_position: schema::ForgePosition {
131 new_line: (h.new_count > 0).then_some(h.new_start),
132 old_line: (h.old_count > 0).then_some(h.old_start),
133 },
134 })
135 })
136 .collect::<Result<Vec<_>, EngineError>>()?;
137
138 let mut graph = graph;
141 let classes = partition
142 .classes
143 .iter()
144 .enumerate()
145 .map(|(ci, members)| schema::ClassEntry {
146 id: format!("C{ci}"),
147 hunk_ids: members
148 .iter()
149 .map(|&i| HunkId::from_index(i).to_string())
150 .collect(),
151 exemplar: HunkId::from_index(members[0]).to_string(),
152 pure_substitution: partition.pure[ci],
153 defines: std::mem::take(&mut graph.defines[ci]),
154 depends_on: std::mem::take(&mut graph.depends_on[ci]),
155 })
156 .collect::<Vec<_>>();
157
158 Ok(schema::PlanDocument {
159 schema_version: schema::SCHEMA_VERSION,
160 generator: schema::Generator::current(&["enumerate", "classify"]),
161 source: schema::Source {
162 kind: source.kind,
163 base: source.base.clone(),
164 head: source.head.clone(),
165 remote: source.remote.clone(),
166 },
167 stats: schema::Stats {
168 files: view.files.len() as u32,
169 hunks: view.hunks.len() as u32,
170 classes: partition.classes.len() as u32,
171 binary_files: view.files.iter().filter(|f| f.binary).count() as u32,
172 submodules: view.files.iter().filter(|f| f.submodule.is_some()).count() as u32,
173 },
174 files,
175 hunks,
176 classes,
177 groups: None,
178 reading_plan: None,
179 audit: schema::Audit {
180 applier_exact: report.applier_exact(),
181 tree_assertion: tree_assertion(report),
186 hunks_carried: report.hunks_total as u32,
187 recount: report.tree.as_ref().map_or(0, |t| t.recount) as u32,
188 ..schema::Audit::default()
189 },
190 symbols: Some(std::mem::take(&mut graph.symbols)),
193 })
194}
195
196fn tree_assertion(report: &InvariantReport) -> String {
197 match &report.tree {
198 Some(t) => if t.tree_ok { "pass" } else { "fail" }.to_string(),
199 None => "skipped".to_string(),
200 }
201}
202
203pub fn apply_tree_audit(doc: &mut schema::PlanDocument, tree: &crate::invariants::TreeReport) {
209 doc.audit.tree_assertion = if tree.tree_ok { "pass" } else { "fail" }.to_string();
210 doc.audit.recount = tree.recount as u32;
211 if !doc.generator.stages.iter().any(|s| s == "verify") {
212 doc.generator.stages.push("verify".to_string());
213 }
214}
215
216fn path_str(path: &[u8]) -> Result<String, EngineError> {
217 String::from_utf8(path.to_vec()).map_err(|_| EngineError::NonUtf8Path {
218 lossy: String::from_utf8_lossy(path).into_owned(),
219 })
220}
221
222#[cfg(test)]
223mod tests {
224 use super::builtin_generated;
225
226 #[test]
227 fn builtin_list_matches() {
228 assert!(builtin_generated(b"Cargo.lock"));
229 assert!(builtin_generated(b"nested/dir/package-lock.json"));
230 assert!(builtin_generated(b"ui/__snapshots__/thing.snap"));
231 assert!(builtin_generated(b"dist/app.min.js"));
232 assert!(!builtin_generated(b"src/main.rs"));
233 assert!(!builtin_generated(b"docs/lockfile-design.md"));
234 }
235}