@lamentis/naome 1.3.9 → 1.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +10 -0
  3. package/bin/naome.js +1 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/architecture_commands.rs +127 -0
  6. package/crates/naome-cli/src/cli_args.rs +4 -0
  7. package/crates/naome-cli/src/dispatcher.rs +2 -0
  8. package/crates/naome-cli/src/main.rs +6 -0
  9. package/crates/naome-core/Cargo.toml +1 -1
  10. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  11. package/crates/naome-core/src/architecture/config/parser/sections.rs +154 -0
  12. package/crates/naome-core/src/architecture/config/parser.rs +97 -0
  13. package/crates/naome-core/src/architecture/config.rs +126 -0
  14. package/crates/naome-core/src/architecture/model.rs +80 -0
  15. package/crates/naome-core/src/architecture/output.rs +178 -0
  16. package/crates/naome-core/src/architecture/rules.rs +212 -0
  17. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +118 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +87 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder.rs +211 -0
  20. package/crates/naome-core/src/architecture/scan/imports/extractors.rs +407 -0
  21. package/crates/naome-core/src/architecture/scan/imports/resolver.rs +334 -0
  22. package/crates/naome-core/src/architecture/scan/imports.rs +59 -0
  23. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  24. package/crates/naome-core/src/architecture/scan.rs +95 -0
  25. package/crates/naome-core/src/architecture.rs +31 -0
  26. package/crates/naome-core/src/install_plan.rs +2 -0
  27. package/crates/naome-core/src/lib.rs +16 -8
  28. package/crates/naome-core/tests/architecture.rs +548 -0
  29. package/crates/naome-core/tests/harness_health.rs +1 -0
  30. package/installer/harness-files.js +3 -0
  31. package/native/darwin-arm64/naome +0 -0
  32. package/native/linux-x64/naome +0 -0
  33. package/package.json +1 -1
  34. package/templates/naome-root/.naome/bin/check-harness-health.js +7 -7
  35. package/templates/naome-root/.naome/bin/check-task-state.js +7 -7
  36. package/templates/naome-root/.naome/bin/naome.js +2 -2
  37. package/templates/naome-root/.naome/manifest.json +10 -8
  38. package/templates/naome-root/.naome/verification.json +15 -1
  39. package/templates/naome-root/docs/naome/architecture-fitness.md +109 -0
  40. package/templates/naome-root/docs/naome/index.md +4 -3
  41. package/templates/naome-root/docs/naome/testing.md +6 -3
@@ -0,0 +1,334 @@
1
+ use std::collections::BTreeSet;
2
+ use std::fs;
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use super::{external_target, language_for_path};
6
+ use crate::architecture::scan::ImportTarget;
7
+
8
+ pub(super) fn resolve_import(
9
+ root: &Path,
10
+ from_path: &str,
11
+ specifier: &str,
12
+ repository_files: &BTreeSet<String>,
13
+ ) -> ImportTarget {
14
+ let language = language_for_path(from_path);
15
+ if from_path.ends_with(".rs") {
16
+ if let Some(module) = specifier.strip_prefix("rustmod:") {
17
+ return resolve_rust_module_declaration(from_path, module, repository_files)
18
+ .map(ImportTarget::File)
19
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
20
+ }
21
+ }
22
+ if specifier.starts_with('.') {
23
+ if from_path.ends_with(".py") {
24
+ return resolve_python_relative(from_path, specifier, repository_files)
25
+ .map(ImportTarget::File)
26
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
27
+ }
28
+ return resolve_relative(from_path, specifier, repository_files, language)
29
+ .map(ImportTarget::File)
30
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
31
+ }
32
+ if from_path.ends_with(".rs") && is_rust_local(specifier) {
33
+ return resolve_rust_local(from_path, specifier, repository_files)
34
+ .map(ImportTarget::File)
35
+ .unwrap_or_else(|| ImportTarget::Unknown(specifier.to_string()));
36
+ }
37
+ if let Some(path) = resolve_repo_absolute(specifier, repository_files, language) {
38
+ return ImportTarget::File(path);
39
+ }
40
+ if from_path.ends_with(".go") {
41
+ if let Some(path) = resolve_go_module_path(root, from_path, specifier, repository_files) {
42
+ return ImportTarget::File(path);
43
+ }
44
+ }
45
+ external_target(specifier)
46
+ }
47
+
48
+ fn resolve_relative(
49
+ from_path: &str,
50
+ specifier: &str,
51
+ repository_files: &BTreeSet<String>,
52
+ language: Option<&str>,
53
+ ) -> Option<String> {
54
+ let base = Path::new(from_path)
55
+ .parent()
56
+ .unwrap_or_else(|| Path::new(""));
57
+ let candidate = normalize(base.join(specifier));
58
+ resolve_candidates(&candidate, repository_files, language)
59
+ }
60
+
61
+ fn resolve_python_relative(
62
+ from_path: &str,
63
+ specifier: &str,
64
+ repository_files: &BTreeSet<String>,
65
+ ) -> Option<String> {
66
+ let dots = specifier
67
+ .chars()
68
+ .take_while(|character| *character == '.')
69
+ .count();
70
+ let module = specifier[dots..].replace('.', "/");
71
+ let mut base = Path::new(from_path)
72
+ .parent()
73
+ .unwrap_or_else(|| Path::new(""));
74
+ for _ in 1..dots {
75
+ base = base.parent().unwrap_or_else(|| Path::new(""));
76
+ }
77
+ resolve_progressively(
78
+ &normalize(base.join(module)),
79
+ repository_files,
80
+ Some("python"),
81
+ '/',
82
+ )
83
+ }
84
+
85
+ fn resolve_repo_absolute(
86
+ specifier: &str,
87
+ repository_files: &BTreeSet<String>,
88
+ language: Option<&str>,
89
+ ) -> Option<String> {
90
+ let candidate = specifier.strip_prefix("@/").unwrap_or(specifier);
91
+ if candidate.starts_with("src/")
92
+ || candidate.starts_with("packages/")
93
+ || candidate.starts_with("apps/")
94
+ {
95
+ return resolve_candidates(candidate, repository_files, language);
96
+ }
97
+ let dotted_candidate = specifier.replace('.', "/");
98
+ if dotted_candidate.starts_with("src/")
99
+ || dotted_candidate.starts_with("packages/")
100
+ || dotted_candidate.starts_with("apps/")
101
+ {
102
+ return resolve_progressively(&dotted_candidate, repository_files, language, '/');
103
+ }
104
+ None
105
+ }
106
+
107
+ fn resolve_rust_local(
108
+ from_path: &str,
109
+ specifier: &str,
110
+ repository_files: &BTreeSet<String>,
111
+ ) -> Option<String> {
112
+ let mut parts = specifier
113
+ .split("::")
114
+ .map(str::trim)
115
+ .filter(|part| !part.is_empty())
116
+ .collect::<Vec<_>>();
117
+ if parts.is_empty() {
118
+ return None;
119
+ }
120
+ match parts[0] {
121
+ "crate" => {
122
+ parts.remove(0);
123
+ let crate_root = rust_crate_src_root(from_path);
124
+ resolve_progressively(
125
+ &normalize(Path::new(&crate_root).join(parts.join("/"))),
126
+ repository_files,
127
+ Some("rust"),
128
+ '/',
129
+ )
130
+ }
131
+ "self" => {
132
+ parts.remove(0);
133
+ let base = PathBuf::from(rust_module_directory(from_path));
134
+ resolve_progressively(
135
+ &normalize(base.join(parts.join("/"))),
136
+ repository_files,
137
+ Some("rust"),
138
+ '/',
139
+ )
140
+ }
141
+ "super" => {
142
+ let mut base = PathBuf::from(rust_module_directory(from_path));
143
+ while parts.first() == Some(&"super") {
144
+ parts.remove(0);
145
+ base.pop();
146
+ }
147
+ resolve_progressively(
148
+ &normalize(base.join(parts.join("/"))),
149
+ repository_files,
150
+ Some("rust"),
151
+ '/',
152
+ )
153
+ }
154
+ _ => None,
155
+ }
156
+ }
157
+
158
+ fn resolve_rust_module_declaration(
159
+ from_path: &str,
160
+ module: &str,
161
+ repository_files: &BTreeSet<String>,
162
+ ) -> Option<String> {
163
+ let module = module.trim();
164
+ if module.is_empty() || module.contains("::") || module.contains('/') {
165
+ return None;
166
+ }
167
+ let module_base = normalize(Path::new(&rust_module_directory(from_path)).join(module));
168
+ resolve_candidates(&module_base, repository_files, Some("rust"))
169
+ }
170
+
171
+ fn rust_module_directory(from_path: &str) -> String {
172
+ let source = Path::new(from_path);
173
+ let parent = source.parent().unwrap_or_else(|| Path::new(""));
174
+ let stem = source
175
+ .file_stem()
176
+ .and_then(|value| value.to_str())
177
+ .unwrap_or("");
178
+ if matches!(stem, "lib" | "main" | "mod") {
179
+ normalize(parent)
180
+ } else {
181
+ normalize(parent.join(stem))
182
+ }
183
+ }
184
+
185
+ fn rust_crate_src_root(from_path: &str) -> String {
186
+ let parts = from_path.split('/').collect::<Vec<_>>();
187
+ if let Some(index) = parts.iter().rposition(|part| *part == "src") {
188
+ return parts[..=index].join("/");
189
+ }
190
+ "src".to_string()
191
+ }
192
+
193
+ fn resolve_go_module_path(
194
+ root: &Path,
195
+ from_path: &str,
196
+ specifier: &str,
197
+ repository_files: &BTreeSet<String>,
198
+ ) -> Option<String> {
199
+ let module = go_module_for_file(root, from_path, repository_files)?;
200
+ let suffix = specifier.strip_prefix(&module)?;
201
+ let suffix = suffix.strip_prefix('/')?;
202
+ if suffix.is_empty() {
203
+ return None;
204
+ }
205
+ if let Some(path) = resolve_candidates(suffix, repository_files, Some("go")) {
206
+ return Some(path);
207
+ }
208
+ let package_prefix = format!("{suffix}/");
209
+ repository_files
210
+ .iter()
211
+ .find(|path| path.starts_with(&package_prefix) && path.ends_with(".go"))
212
+ .cloned()
213
+ }
214
+
215
+ fn go_module_for_file(
216
+ root: &Path,
217
+ from_path: &str,
218
+ repository_files: &BTreeSet<String>,
219
+ ) -> Option<String> {
220
+ let mut current = Path::new(from_path).parent().unwrap_or_else(|| Path::new(""));
221
+ loop {
222
+ let go_mod_path = normalize(current.join("go.mod"));
223
+ if repository_files.contains(&go_mod_path) {
224
+ return go_mod_module(root, &go_mod_path);
225
+ }
226
+ let Some(parent) = current.parent() else {
227
+ break;
228
+ };
229
+ if parent == current {
230
+ break;
231
+ }
232
+ current = parent;
233
+ }
234
+ None
235
+ }
236
+
237
+ fn go_mod_module(root: &Path, go_mod_path: &str) -> Option<String> {
238
+ let content = fs::read_to_string(root.join(go_mod_path)).ok()?;
239
+ content.lines().find_map(|line| {
240
+ let line = line.trim();
241
+ let module = line.strip_prefix("module ")?;
242
+ module.split_whitespace().next().map(ToString::to_string)
243
+ })
244
+ }
245
+
246
+ fn resolve_candidates(
247
+ candidate: &str,
248
+ repository_files: &BTreeSet<String>,
249
+ language: Option<&str>,
250
+ ) -> Option<String> {
251
+ candidate_paths(candidate, language)
252
+ .into_iter()
253
+ .find(|path| repository_files.contains(path))
254
+ }
255
+
256
+ fn resolve_progressively(
257
+ candidate: &str,
258
+ repository_files: &BTreeSet<String>,
259
+ language: Option<&str>,
260
+ separator: char,
261
+ ) -> Option<String> {
262
+ let mut current = candidate.to_string();
263
+ loop {
264
+ if let Some(path) = resolve_candidates(&current, repository_files, language) {
265
+ return Some(path);
266
+ }
267
+ let Some((parent, _)) = current.rsplit_once(separator) else {
268
+ return None;
269
+ };
270
+ current = parent.to_string();
271
+ }
272
+ }
273
+
274
+ fn candidate_paths(candidate: &str, language: Option<&str>) -> Vec<String> {
275
+ let extensions = extensions_for_language(language);
276
+ let mut candidates = extensions
277
+ .iter()
278
+ .map(|extension| format!("{candidate}{extension}"))
279
+ .collect::<Vec<_>>();
280
+ match language {
281
+ Some("rust") => candidates.push(format!("{candidate}/mod.rs")),
282
+ Some("python") => candidates.push(format!("{candidate}/__init__.py")),
283
+ Some("go") => {}
284
+ Some("typescript") | Some("javascript") => candidates.extend([
285
+ format!("{candidate}/index.ts"),
286
+ format!("{candidate}/index.tsx"),
287
+ format!("{candidate}/index.js"),
288
+ format!("{candidate}/index.jsx"),
289
+ ]),
290
+ _ => candidates.extend([
291
+ format!("{candidate}/index.ts"),
292
+ format!("{candidate}/index.tsx"),
293
+ format!("{candidate}/index.js"),
294
+ format!("{candidate}/index.jsx"),
295
+ format!("{candidate}/mod.rs"),
296
+ format!("{candidate}/__init__.py"),
297
+ ]),
298
+ }
299
+ candidates
300
+ }
301
+
302
+ fn extensions_for_language(language: Option<&str>) -> &'static [&'static str] {
303
+ match language {
304
+ Some("rust") => &["", ".rs"],
305
+ Some("python") => &["", ".py"],
306
+ Some("go") => &["", ".go"],
307
+ Some("typescript") => &["", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
308
+ Some("javascript") => &["", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"],
309
+ _ => &[
310
+ "", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go",
311
+ ],
312
+ }
313
+ }
314
+
315
+ fn is_rust_local(specifier: &str) -> bool {
316
+ specifier.starts_with("crate::")
317
+ || specifier.starts_with("self::")
318
+ || specifier.starts_with("super::")
319
+ || specifier.starts_with('.')
320
+ }
321
+
322
+ fn normalize(path: impl AsRef<Path>) -> String {
323
+ let mut normalized = PathBuf::new();
324
+ for component in path.as_ref().components() {
325
+ match component {
326
+ std::path::Component::ParentDir => {
327
+ normalized.pop();
328
+ }
329
+ std::path::Component::CurDir => {}
330
+ other => normalized.push(other.as_os_str()),
331
+ }
332
+ }
333
+ normalized.to_string_lossy().replace('\\', "/")
334
+ }
@@ -0,0 +1,59 @@
1
+ use std::collections::BTreeSet;
2
+ use std::path::Path;
3
+
4
+ use super::{ImportFact, ImportTarget};
5
+
6
+ mod extractors;
7
+ mod resolver;
8
+
9
+ pub(super) fn extract_imports(
10
+ root: &Path,
11
+ path: &str,
12
+ content: &str,
13
+ repository_files: &BTreeSet<String>,
14
+ ) -> Vec<ImportFact> {
15
+ let raw_imports = extractors::extract_raw_imports(path, content);
16
+ raw_imports
17
+ .into_iter()
18
+ .map(|raw| ImportFact {
19
+ target: resolver::resolve_import(root, path, &raw.specifier, repository_files),
20
+ specifier: raw.specifier,
21
+ source_range: Some(raw.source_range),
22
+ confidence: raw.confidence,
23
+ extractor: raw.extractor,
24
+ })
25
+ .collect()
26
+ }
27
+
28
+ pub(super) fn package_name(specifier: &str) -> String {
29
+ if let Some(rest) = specifier.strip_prefix('@') {
30
+ let mut parts = rest.split('/');
31
+ let scope = parts.next().unwrap_or_default();
32
+ let package = parts.next().unwrap_or_default();
33
+ return format!("@{scope}/{package}");
34
+ }
35
+ specifier
36
+ .split("::")
37
+ .next()
38
+ .unwrap_or(specifier)
39
+ .split('/')
40
+ .next()
41
+ .unwrap_or(specifier)
42
+ .to_string()
43
+ }
44
+
45
+ pub(super) fn external_target(specifier: &str) -> ImportTarget {
46
+ ImportTarget::ExternalDependency(package_name(specifier))
47
+ }
48
+
49
+ pub(super) fn language_for_path(path: &str) -> Option<&'static str> {
50
+ let extension = path.rsplit('.').next()?;
51
+ match extension {
52
+ "ts" | "tsx" => Some("typescript"),
53
+ "js" | "jsx" | "mjs" | "cjs" => Some("javascript"),
54
+ "rs" => Some("rust"),
55
+ "py" => Some("python"),
56
+ "go" => Some("go"),
57
+ _ => None,
58
+ }
59
+ }
@@ -0,0 +1,92 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+ use crate::paths;
6
+
7
+ use crate::architecture::config::ArchitectureConfig;
8
+
9
+ pub(super) fn repository_files(
10
+ root: &Path,
11
+ config: &ArchitectureConfig,
12
+ ) -> Result<Vec<String>, NaomeError> {
13
+ let ignored_patterns = ignored_patterns(root, config);
14
+ let mut files = Vec::new();
15
+ collect_files(root, root, &ignored_patterns, &mut files)?;
16
+ files.sort();
17
+ Ok(files)
18
+ }
19
+
20
+ fn collect_files(
21
+ root: &Path,
22
+ dir: &Path,
23
+ ignored_patterns: &[String],
24
+ files: &mut Vec<String>,
25
+ ) -> Result<(), NaomeError> {
26
+ for entry in fs::read_dir(dir)? {
27
+ let entry = entry?;
28
+ let path = entry.path();
29
+ let relative = normalize_path(path.strip_prefix(root).unwrap_or(&path));
30
+ if is_ignored(&relative, ignored_patterns) {
31
+ continue;
32
+ }
33
+ let metadata = fs::symlink_metadata(&path)?;
34
+ if metadata.is_symlink() {
35
+ continue;
36
+ }
37
+ if metadata.is_dir() {
38
+ collect_files(root, &path, ignored_patterns, files)?;
39
+ } else if metadata.is_file() {
40
+ files.push(relative);
41
+ }
42
+ }
43
+ Ok(())
44
+ }
45
+
46
+ fn is_ignored(path: &str, ignored_patterns: &[String]) -> bool {
47
+ path.is_empty() || is_default_ignored_path(path) || paths::matches_any(path, ignored_patterns)
48
+ }
49
+
50
+ fn ignored_patterns(root: &Path, config: &ArchitectureConfig) -> Vec<String> {
51
+ let mut patterns = paths::naomeignore_patterns(root);
52
+ patterns.extend([
53
+ ".git/**".to_string(),
54
+ ".naome/archive/**".to_string(),
55
+ ".naome/cache/**".to_string(),
56
+ ".naome/local/**".to_string(),
57
+ ".naome/tasks/**".to_string(),
58
+ "node_modules/**".to_string(),
59
+ "target/**".to_string(),
60
+ "dist/**".to_string(),
61
+ "build/**".to_string(),
62
+ ]);
63
+ patterns.extend(config.ignore.iter().map(|rule| rule.path.clone()));
64
+ patterns
65
+ }
66
+
67
+ fn is_default_ignored_path(path: &str) -> bool {
68
+ path == ".git"
69
+ || path.starts_with(".git/")
70
+ || path == ".naome/archive"
71
+ || path.starts_with(".naome/archive/")
72
+ || path == ".naome/cache"
73
+ || path.starts_with(".naome/cache/")
74
+ || path == ".naome/local"
75
+ || path.starts_with(".naome/local/")
76
+ || path == ".naome/tasks"
77
+ || path.starts_with(".naome/tasks/")
78
+ || path == ".npm"
79
+ || path.starts_with(".npm/")
80
+ || path == "node_modules"
81
+ || path.contains("/node_modules/")
82
+ || path == "target"
83
+ || path.contains("/target/")
84
+ || path == "dist"
85
+ || path.contains("/dist/")
86
+ || path == "build"
87
+ || path.contains("/build/")
88
+ }
89
+
90
+ fn normalize_path(path: impl AsRef<Path>) -> String {
91
+ path.as_ref().to_string_lossy().replace('\\', "/")
92
+ }
@@ -0,0 +1,95 @@
1
+ use std::collections::BTreeMap;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use serde::Serialize;
5
+
6
+ use crate::git;
7
+ use crate::models::NaomeError;
8
+
9
+ use super::config::{read_architecture_config, ArchitectureConfig};
10
+ use super::model::ArchitectureGraph;
11
+
12
+ mod graph_builder;
13
+ mod imports;
14
+ mod path_scan;
15
+
16
+ #[derive(Debug, Clone, Default)]
17
+ pub struct ArchitectureScanOptions {
18
+ pub config_path: Option<PathBuf>,
19
+ pub changed_only: bool,
20
+ }
21
+
22
+ #[derive(Debug, Clone, Serialize)]
23
+ #[serde(rename_all = "camelCase")]
24
+ pub struct ArchitectureScanReport {
25
+ pub schema: String,
26
+ pub graph: ArchitectureGraph,
27
+ pub files_scanned: usize,
28
+ pub changed_only_requested: bool,
29
+ pub changed_only_degraded_to_full_scan: bool,
30
+ pub changed_paths: Vec<String>,
31
+ #[serde(skip_serializing)]
32
+ pub config: ArchitectureConfig,
33
+ #[serde(skip_serializing)]
34
+ pub file_facts: BTreeMap<String, FileFact>,
35
+ }
36
+
37
+ #[derive(Debug, Clone, Serialize)]
38
+ #[serde(rename_all = "camelCase")]
39
+ pub struct FileFact {
40
+ pub path: String,
41
+ pub language: Option<String>,
42
+ pub line_count: usize,
43
+ pub layers: Vec<String>,
44
+ pub contexts: Vec<String>,
45
+ pub ignored: Option<String>,
46
+ pub imports: Vec<ImportFact>,
47
+ }
48
+
49
+ #[derive(Debug, Clone, Serialize)]
50
+ #[serde(rename_all = "camelCase")]
51
+ pub struct ImportFact {
52
+ pub specifier: String,
53
+ pub target: ImportTarget,
54
+ pub source_range: Option<super::model::SourceRange>,
55
+ pub confidence: f32,
56
+ pub extractor: String,
57
+ }
58
+
59
+ #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
60
+ #[serde(rename_all = "camelCase")]
61
+ pub enum ImportTarget {
62
+ File(String),
63
+ ExternalDependency(String),
64
+ Unknown(String),
65
+ }
66
+
67
+ pub fn scan_architecture(
68
+ root: &Path,
69
+ options: ArchitectureScanOptions,
70
+ ) -> Result<ArchitectureScanReport, NaomeError> {
71
+ let config = read_architecture_config(root, options.config_path.as_deref())?;
72
+ let changed_paths = changed_paths(root, options.changed_only)?;
73
+ let files = path_scan::repository_files(root, &config)?;
74
+ let (mut graph, file_facts) = graph_builder::build_path_graph(root, files, &config);
75
+
76
+ graph.sort_stable();
77
+ Ok(ArchitectureScanReport {
78
+ schema: "naome.arch.scan.v1".to_string(),
79
+ files_scanned: file_facts.len(),
80
+ graph,
81
+ changed_only_requested: options.changed_only,
82
+ changed_only_degraded_to_full_scan: options.changed_only,
83
+ changed_paths,
84
+ config,
85
+ file_facts,
86
+ })
87
+ }
88
+
89
+ fn changed_paths(root: &Path, changed_only: bool) -> Result<Vec<String>, NaomeError> {
90
+ if changed_only {
91
+ git::changed_paths(root)
92
+ } else {
93
+ Ok(Vec::new())
94
+ }
95
+ }
@@ -0,0 +1,31 @@
1
+ mod config;
2
+ mod model;
3
+ mod output;
4
+ mod rules;
5
+ mod scan;
6
+
7
+ use std::path::Path;
8
+
9
+ use crate::models::NaomeError;
10
+
11
+ pub use config::{
12
+ default_architecture_config_text, ArchitectureConfig, ContextConfig, LayerConfig, RuleConfig,
13
+ };
14
+ pub use model::{
15
+ ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata,
16
+ ArchitectureNode, ArchitectureNodeKind, SourceRange,
17
+ };
18
+ pub use output::{
19
+ format_architecture_explain, format_architecture_scan, format_architecture_validation,
20
+ ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, Severity,
21
+ ViolationSummary,
22
+ };
23
+ pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
24
+
25
+ pub fn validate_architecture(
26
+ root: &Path,
27
+ options: ArchitectureScanOptions,
28
+ ) -> Result<ArchitectureValidation, NaomeError> {
29
+ let scan = scan_architecture(root, options)?;
30
+ Ok(rules::validate_scan(scan))
31
+ }
@@ -12,6 +12,7 @@ pub const MACHINE_OWNED_PATHS: &[&str] = &[
12
12
  "docs/naome/agent-workflow.md",
13
13
  "docs/naome/context-economy.md",
14
14
  "docs/naome/task-ledger.md",
15
+ "docs/naome/architecture-fitness.md",
15
16
  "docs/naome/execution.md",
16
17
  "docs/naome/upgrade.md",
17
18
  ];
@@ -48,6 +49,7 @@ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
48
49
  "docs/naome/agent-workflow.md",
49
50
  "docs/naome/context-economy.md",
50
51
  "docs/naome/task-ledger.md",
52
+ "docs/naome/architecture-fitness.md",
51
53
  "docs/naome/execution.md",
52
54
  "docs/naome/upgrade.md",
53
55
  ];
@@ -1,3 +1,4 @@
1
+ mod architecture;
1
2
  mod context;
2
3
  mod decision;
3
4
  mod git;
@@ -19,6 +20,14 @@ mod verification_contract;
19
20
  mod verification_contract_policy;
20
21
  mod workflow;
21
22
 
23
+ pub use architecture::{
24
+ default_architecture_config_text, format_architecture_explain, format_architecture_scan,
25
+ format_architecture_validation, scan_architecture, validate_architecture,
26
+ ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureEdge, ArchitectureEdgeKind,
27
+ ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
28
+ ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
29
+ ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
30
+ };
22
31
  pub use context::{
23
32
  select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
24
33
  ContextCapsule, ContextItem, ContextSelection,
@@ -36,14 +45,13 @@ pub use journal::{append_task_journal, TaskJournalEntry};
36
45
  pub use models::{Decision, NaomeError};
37
46
  pub use quality::{
38
47
  check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
39
- check_semantic_legacy_paths,
40
- clear_quality_cache, explain_repository_structure, init_repository_quality,
41
- init_repository_quality_with_mode, plan_quality_cleanup, quality_cache_status,
42
- reconcile_repository_quality, route_quality_cleanup, semantic_route_for_finding,
43
- QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask,
44
- QualityInitMode, QualityInitResult, QualityMode, QualityReconcileReport, QualityReport,
45
- QualitySummary, QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig,
46
- SemanticFinding, SemanticReport, StructurePathExplanation,
48
+ check_semantic_legacy_paths, clear_quality_cache, explain_repository_structure,
49
+ init_repository_quality, init_repository_quality_with_mode, plan_quality_cleanup,
50
+ quality_cache_status, reconcile_repository_quality, route_quality_cleanup,
51
+ semantic_route_for_finding, QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute,
52
+ QualityCleanupTask, QualityInitMode, QualityInitResult, QualityMode, QualityReconcileReport,
53
+ QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
54
+ RepositoryStructureConfig, SemanticFinding, SemanticReport, StructurePathExplanation,
47
55
  };
48
56
  pub use repository_model::{
49
57
  explain_repository_model_path, refresh_repository_model, repository_model_drift,