|
7 | 7 | use std::collections::{BTreeMap, BTreeSet}; |
8 | 8 | use std::path::{Path, PathBuf}; |
9 | 9 |
|
10 | | -use straymark_core::architecture::{ArchModel, Component, Layer}; |
| 10 | +use straymark_core::architecture::{ |
| 11 | + collect_source_files_with, component_dir_for, resolve_scan_config, ArchModel, Component, Layer, |
| 12 | +}; |
11 | 13 | use straymark_core::document::{detect_doc_type, discover_documents, DocType}; |
12 | 14 | use straymark_core::drift::glob_match; |
13 | 15 |
|
14 | 16 | use super::adr_mining; |
15 | 17 | use crate::utils; |
16 | 18 |
|
17 | | -// The source-file walker (`collect_source_files` + its `EXCLUDED_DIRS`/ |
18 | | -// `SOURCE_EXTENSIONS`) moved to `core::architecture::gather` in Loom A2.0 so the |
19 | | -// CLI generator and the Loom server share one inventory scan. Re-exported so the |
20 | | -// `common::collect_source_files` call sites (generate/sync/validate) are |
21 | | -// unchanged. |
| 19 | +// The source-file walker + the component-dir shaping (extensions, excluded dirs, |
| 20 | +// container descent, build-scaffolding) live in `core::architecture::{gather, |
| 21 | +// scan}` (Loom A2.0; made config-driven in #279) so the CLI generator and the |
| 22 | +// Loom server share one inventory scan. Re-exported so the |
| 23 | +// `common::collect_source_files` call sites (validate/sync) are unchanged. |
22 | 24 | pub(crate) use straymark_core::architecture::collect_source_files; |
23 | 25 |
|
24 | 26 | /// The placeholder layer code-derived components land in until the human |
@@ -65,49 +67,23 @@ pub(crate) fn artifact_paths(root: &Path, out: Option<&str>) -> (PathBuf, PathBu |
65 | 67 | (out_dir, model, drawio) |
66 | 68 | } |
67 | 69 |
|
68 | | -/// Organizational directories that hold *other* components rather than being |
69 | | -/// one themselves — the seed descends through them one level so an `internal/` |
70 | | -/// or `src/` tree breaks out into real components instead of one giant blob |
71 | | -/// (#273). A leaf like `cmd/` or `db/` is not a container and stays whole. |
72 | | -pub(crate) const CONTAINER_DIRS: &[&str] = |
73 | | - &["internal", "src", "pkg", "lib", "libs", "app", "apps", "modules", "packages"]; |
74 | | - |
75 | | -/// Distinct component directories under `root` that contain ≥1 source file. A |
76 | | -/// component dir is the path up to and including the first **non-container** |
77 | | -/// segment, so `internal/modules/agents/x.go` → `internal/modules/agents`, |
78 | | -/// `internal/core/bus.go` → `internal/core`, and `cmd/main.go` → `cmd`. |
| 70 | +/// Distinct component directories under `root` that contain ≥1 source file, |
| 71 | +/// shaped by the project's [`ScanConfig`] (#273, #279): the seed descends |
| 72 | +/// through *container* dirs and skips build *scaffolding* (e.g. Maven |
| 73 | +/// `src/main/java`), so `internal/modules/agents/x.go` → `internal/modules/agents`, |
| 74 | +/// `cmd/main.go` → `cmd`, and `billing/src/main/java/…` → `billing`. The config |
| 75 | +/// is resolved once and reused for the file walk. |
79 | 76 | pub(crate) fn source_component_dirs(root: &Path) -> Vec<String> { |
| 77 | + let cfg = resolve_scan_config(root); |
80 | 78 | let mut dirs: BTreeSet<String> = BTreeSet::new(); |
81 | | - for rel in collect_source_files(root) { |
82 | | - if let Some(dir) = component_dir_for(&rel) { |
| 79 | + for rel in collect_source_files_with(root, &cfg) { |
| 80 | + if let Some(dir) = component_dir_for(&rel, &cfg) { |
83 | 81 | dirs.insert(dir); |
84 | 82 | } |
85 | 83 | } |
86 | 84 | dirs.into_iter().collect() |
87 | 85 | } |
88 | 86 |
|
89 | | -/// The component directory a source file belongs to: descend through container |
90 | | -/// segments, stop at (and include) the first non-container one. Returns `None` |
91 | | -/// for a root-level file (no directory to attribute it to). |
92 | | -fn component_dir_for(rel: &Path) -> Option<String> { |
93 | | - let segs: Vec<String> = rel |
94 | | - .parent()? |
95 | | - .components() |
96 | | - .map(|c| c.as_os_str().to_string_lossy().to_string()) |
97 | | - .collect(); |
98 | | - if segs.is_empty() { |
99 | | - return None; |
100 | | - } |
101 | | - let mut taken: Vec<&str> = Vec::new(); |
102 | | - for seg in &segs { |
103 | | - taken.push(seg); |
104 | | - if !CONTAINER_DIRS.contains(&seg.as_str()) { |
105 | | - break; |
106 | | - } |
107 | | - } |
108 | | - Some(taken.join("/")) |
109 | | -} |
110 | | - |
111 | 87 | /// A component proposed from a source dir (`dir/**`, `unassigned`). The id is |
112 | 88 | /// the kebab of the full path (`internal/core` → `internal-core`, unique across |
113 | 89 | /// containers); the label is the title-cased leaf segment. |
@@ -456,22 +432,8 @@ mod tests { |
456 | 432 | assert_eq!(c.globs, vec!["internal/modules/agents/**"]); |
457 | 433 | } |
458 | 434 |
|
459 | | - #[test] |
460 | | - fn component_dir_descends_through_container_dirs() { |
461 | | - // #273: a container tree breaks out; a leaf dir stays whole. |
462 | | - let cases = [ |
463 | | - ("internal/modules/agents/runner.go", Some("internal/modules/agents")), |
464 | | - ("internal/core/eventbus/bus.go", Some("internal/core")), |
465 | | - ("cmd/server/main.go", Some("cmd")), |
466 | | - ("db/seeds/seeds.go", Some("db")), |
467 | | - ("src/index.ts", Some("src")), // files directly under a container → the container |
468 | | - ("main.go", None), // root-level file: no dir to attribute |
469 | | - ]; |
470 | | - for (path, want) in cases { |
471 | | - let got = component_dir_for(Path::new(path)); |
472 | | - assert_eq!(got.as_deref(), want, "for {path}"); |
473 | | - } |
474 | | - } |
| 435 | + // (component-dir descent + Maven scaffolding are tested in |
| 436 | + // `core::architecture::scan` — the logic moved there in #279.) |
475 | 437 |
|
476 | 438 | #[test] |
477 | 439 | fn generated_model_passes_core_validation() { |
|
0 commit comments