1use crate::config::Config;
2
3pub fn pre_commit() -> anyhow::Result<()> {
4 println!("=== dstack quality gate ===\n");
5 let questions = [
6 ("Negative tests?", "Did I test what happens when things go WRONG? (wrong password → 403, invalid input → error, missing field → handled)"),
7 ("Live verification?", "Did I verify against the LIVE system? (not just compilation — actual curl, DB query, or Chrome DevTools)"),
8 ("Companion doc updated?", "Did I update the .implementation.md with DETAILS? (bugs found, DB state, commit hashes — not one-liner 'DONE')"),
9 ("Tests prove the change?", "Would these tests FAIL without my code change? (decorative tests that pass regardless are worthless)"),
10 ("Truly done?", "Am I moving forward because it's TRULY done, or because I want to show progress?"),
11 ];
12 for (i, (short, detail)) in questions.iter().enumerate() {
13 println!(" {}. {} — {}", i + 1, short, detail);
14 }
15 println!("\nAnswer honestly before committing. These exist because skipping them cost us a client meeting.");
16 Ok(())
17}
18
19pub fn stale(cfg: &Config) -> anyhow::Result<()> {
20 eprintln!("Scanning for stale companion docs across tracked repos...\n");
21 let mut found = 0;
22 let now = std::time::SystemTime::now();
23
24 for repo in &cfg.repos.tracked {
25 let path = format!("{}/{}", cfg.repos.root, repo);
26 if !std::path::Path::new(&path).exists() {
27 continue;
28 }
29 let output = std::process::Command::new("find")
31 .args([&path, "-name", "*.implementation.md", "-type", "f"])
32 .output()?;
33 for line in String::from_utf8_lossy(&output.stdout).lines() {
34 if line.is_empty() {
35 continue;
36 }
37 let file_path = std::path::Path::new(line);
38 if let Ok(meta) = file_path.metadata() {
39 if let Ok(modified) = meta.modified() {
40 if let Ok(age) = now.duration_since(modified) {
41 let days = age.as_secs() / 86400;
42 if days > 14 {
43 println!("STALE ({:>3}d): {}", days, line);
44 found += 1;
45 } else if days > 7 {
46 println!("AGING ({:>3}d): {}", days, line);
47 }
48 }
49 }
50 }
51 }
52 }
53 if found == 0 {
54 println!("No stale companion docs found (>14 days).");
55 } else {
56 println!("\n{} stale file(s). Update or remove them.", found);
57 }
58 Ok(())
59}
60
61pub fn summary(cfg: &Config) -> anyhow::Result<()> {
62 println!("=== dstack audit summary ===\n");
63
64 let total_repos = cfg.repos.tracked.len();
66 let existing: Vec<_> = cfg
67 .repos
68 .tracked
69 .iter()
70 .filter(|r| {
71 std::path::Path::new(&format!("{}/{}", cfg.repos.root, r)).exists()
72 })
73 .collect();
74
75 println!("Repos: {}/{} found", existing.len(), total_repos);
76 println!("Deploy targets: {}", cfg.deploy.len());
77 println!("Memory backend: {}", cfg.memory.backend);
78
79 let mut dirty = 0;
81 for repo in &existing {
82 let path = format!("{}/{}", cfg.repos.root, repo);
83 let output = std::process::Command::new("git")
84 .args(["-C", &path, "status", "--porcelain"])
85 .output()?;
86 let out = String::from_utf8_lossy(&output.stdout);
87 if !out.trim().is_empty() {
88 dirty += 1;
89 }
90 }
91 println!("Dirty repos: {}/{}", dirty, existing.len());
92
93 println!("\nQuality gate: dstack audit --pre-commit");
94 println!("Stale docs: dstack audit --stale");
95 Ok(())
96}