Skip to content

Commit 2f62404

Browse files
committed
refactor: improve extraction
1 parent 0760e23 commit 2f62404

3 files changed

Lines changed: 91 additions & 2 deletions

File tree

src/extract/exclude.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const DEFAULT_EXCLUDES: &[&str] = &[
2929
".output",
3030
".turbo",
3131
".worktrees",
32+
".claude/worktrees",
33+
".astro",
3234
".diecut-answers.toml",
3335
];
3436

@@ -119,6 +121,27 @@ pub fn detect_copy_without_render(
119121
found
120122
}
121123

124+
/// Check if a file should be copied without rendering (lock files, binary-like assets).
125+
///
126+
/// These files are included in the template but should never have replacements
127+
/// applied during extraction — they're copied verbatim.
128+
pub fn is_copy_without_render(path: &Path) -> bool {
129+
for pattern in DEFAULT_COPY_WITHOUT_RENDER {
130+
if let Some(ext) = pattern.strip_prefix("*.") {
131+
if let Some(file_ext) = path.extension() {
132+
if file_ext.to_string_lossy().eq_ignore_ascii_case(ext) {
133+
return true;
134+
}
135+
}
136+
} else if let Some(file_name) = path.file_name() {
137+
if file_name.to_string_lossy() == *pattern {
138+
return true;
139+
}
140+
}
141+
}
142+
false
143+
}
144+
122145
/// Check if a path should be excluded based on the exclude patterns.
123146
pub fn should_exclude(relative_path: &Path, excludes: &[String]) -> bool {
124147
let path_str = relative_path.to_string_lossy();
@@ -232,6 +255,41 @@ mod tests {
232255
assert!(!relevant.contains(&"node_modules".to_string()));
233256
}
234257

258+
#[test]
259+
fn test_should_exclude_claude_worktrees() {
260+
let excludes = all_default_excludes();
261+
assert!(should_exclude(
262+
Path::new(".claude/worktrees/agent-abc/Cargo.toml"),
263+
&excludes
264+
));
265+
// .claude/settings.local.json should NOT be excluded
266+
assert!(!should_exclude(
267+
Path::new(".claude/settings.local.json"),
268+
&excludes
269+
));
270+
}
271+
272+
#[test]
273+
fn test_should_exclude_astro() {
274+
let excludes = all_default_excludes();
275+
assert!(should_exclude(
276+
Path::new("docs/.astro/data-store.json"),
277+
&excludes
278+
));
279+
assert!(should_exclude(Path::new(".astro/settings.json"), &excludes));
280+
}
281+
282+
#[test]
283+
fn test_is_copy_without_render() {
284+
assert!(is_copy_without_render(Path::new("Cargo.lock")));
285+
assert!(is_copy_without_render(Path::new("pnpm-lock.yaml")));
286+
assert!(is_copy_without_render(Path::new("package-lock.json")));
287+
assert!(is_copy_without_render(Path::new("logo.png")));
288+
assert!(is_copy_without_render(Path::new("deep/nested/file.lock")));
289+
assert!(!is_copy_without_render(Path::new("src/main.rs")));
290+
assert!(!is_copy_without_render(Path::new("README.md")));
291+
}
292+
235293
#[test]
236294
fn test_detect_copy_without_render() {
237295
let files = vec![

src/extract/interactive.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use inquire::{Confirm, Select, Text};
66
use crate::config::schema::DEFAULT_TEMPLATES_SUFFIX;
77
use crate::error::{DicecutError, Result};
88

9-
use super::auto_detect::DetectedCandidate;
9+
use super::auto_detect::{ConfidenceTier, DetectedCandidate};
1010
use super::conditional::DetectedConditional;
1111
use super::variants::generate_variants;
1212
use super::{ExtractVariable, PlannedExtractFile};
@@ -194,6 +194,7 @@ pub fn resolve_candidates_yes(
194194
}
195195

196196
let mut result = Vec::new();
197+
let mut skipped_freq = 0;
197198

198199
for (name, mut group) in groups {
199200
// Skip names already covered by explicit --var
@@ -210,6 +211,12 @@ pub fn resolve_candidates_yes(
210211
group.sort_by(|a, b| b.confidence.total_cmp(&a.confidence));
211212
let winner = group[0];
212213

214+
// Skip frequency-analysis candidates in -y mode — too noisy for auto-accept
215+
if winner.tier == ConfidenceTier::FrequencyAnalysis {
216+
skipped_freq += 1;
217+
continue;
218+
}
219+
213220
eprintln!(
214221
" {} {} = {:?} ({:.0}% confidence, {})",
215222
style("✓").green(),
@@ -231,6 +238,14 @@ pub fn resolve_candidates_yes(
231238
result.push((winner.suggested_name.clone(), winner.value.clone()));
232239
}
233240

241+
if skipped_freq > 0 {
242+
eprintln!(
243+
" {} {} frequency-detected candidate(s) skipped (use interactive mode to review)",
244+
style("·").dim(),
245+
skipped_freq
246+
);
247+
}
248+
234249
result
235250
}
236251

src/extract/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ use self::conditional::{detect_conditional_files, patterns_for_variable};
2121
use self::config_gen::{
2222
generate_config_toml, ComputedVariable, ConditionalEntry, ConfigGenOptions, PromptedVariable,
2323
};
24-
use self::exclude::{all_default_excludes, detect_copy_without_render, relevant_config_excludes};
24+
use self::exclude::{
25+
all_default_excludes, detect_copy_without_render, is_copy_without_render,
26+
relevant_config_excludes,
27+
};
2528
use self::interactive::{
2629
confirm_auto_detected_interactive, confirm_conditionals_interactive,
2730
confirm_excludes_interactive, confirm_files_interactive, confirm_variants_interactive,
@@ -328,6 +331,19 @@ pub fn plan_extraction(options: &ExtractOptions) -> Result<ExtractionPlan> {
328331
stubbed: false,
329332
});
330333
} else if let Some(ref content) = file.content {
334+
// Lock files and other copy-without-render files: skip replacement
335+
if is_copy_without_render(&file.relative_path) {
336+
planned_files.push(PlannedExtractFile {
337+
template_path,
338+
content: ExtractedContent::Text {
339+
content: content.clone(),
340+
replacement_count: 0,
341+
},
342+
stubbed: false,
343+
});
344+
continue;
345+
}
346+
331347
let (replaced, count) = apply_replacements(content, &rules);
332348

333349
if count > 0 {

0 commit comments

Comments
 (0)