1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! `architecture-violations` analysis.
//!
//! Joins the `imports` table to the [`LayerRules`](crate::arch_rules::LayerRules)
//! config; reports every import edge that crosses a forbidden
//! layer boundary.
//!
//! ## Workflow
//!
//! 1. Discover `.codelore-arch-rules.toml` at the repo root (or
//! explicit `--arch-rules-file`).
//! 2. Walk every `(src_path, target_path)` row in the `imports`
//! table where both endpoints classify into a declared layer.
//! 3. Call [`LayerRules::validate`] for each; collect violations.
//!
//! ## Empty rule set
//!
//! When no `.codelore-arch-rules.toml` exists in the repo, the
//! analysis returns an empty Vec — users opt INTO architectural
//! validation rather than being forced to declare layers upfront.
use crate::arch_rules::LayerRules;
use crate::facts::FactsDb;
use crate::{CodeLoreError, Options, Result};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ArchViolationRow {
/// File that contains the offending import.
pub src_path: String,
/// Resolved target file of the offending import (only resolvable
/// imports surface here — external imports skip).
pub target_path: String,
/// Layer the source belongs to (per the config's first-match
/// path classification).
pub src_layer: String,
/// Layer the target belongs to.
pub target_layer: String,
/// Raw `target` string from the imports table — useful for
/// jumping straight to the offending import line.
pub raw_target: String,
}
/// Run the architecture-violations analysis. Returns a (possibly
/// empty) Vec of violations sorted by `(src_path, target_path)`.
///
/// # Errors
///
/// Returns [`CodeLoreError::Analysis`] on `DuckDB` query errors or
/// arch-rules file I/O / parse errors.
#[tracing::instrument(name = "arch-violations", skip_all, fields(min_revs = opts.min_revs))]
pub fn run_arch_violations(db: &FactsDb, opts: &Options) -> Result<Vec<ArchViolationRow>> {
let rules = LayerRules::discover(&opts.repo_path)?;
if rules.is_empty() {
return Ok(Vec::new());
}
let mut stmt = db
.conn()
.prepare(
"SELECT src_path, target_path, target \
FROM imports \
WHERE target_path IS NOT NULL \
ORDER BY src_path ASC, target_path ASC",
)
.map_err(|e| CodeLoreError::Analysis(format!("prepare arch-violations scan: {e}")))?;
let rows = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, String>(2)?,
))
})
.map_err(|e| CodeLoreError::Analysis(format!("query arch-violations scan: {e}")))?;
// Stream-validate: walk the rows iterator directly without
// materialising every import row into an intermediate Vec, and
// early-break as soon as the row-limit is hit. The SQL above
// `ORDER BY src_path, target_path ASC` makes the early-break
// deterministic — the first N violations are the same N the prior
// `collect → loop → truncate` shape produced. On a monorepo with
// millions of imports and `--rows 50`, this stops after finding
// the first 50 violations instead of iterating every row.
let limit = opts.rows_limit.map(|n| n as usize);
let mut out: Vec<ArchViolationRow> = Vec::new();
for row in rows {
let (src_path, target_path, raw_target) =
row.map_err(|e| CodeLoreError::Analysis(format!("read arch-violations row: {e}")))?;
if let Some(v) = rules.validate(&src_path, &target_path) {
out.push(ArchViolationRow {
src_path,
target_path,
src_layer: v.src_layer,
target_layer: v.target_layer,
raw_target,
});
if let Some(cap) = limit
&& out.len() >= cap
{
break;
}
}
}
Ok(out)
}