@lamentis/naome 1.3.8 → 1.3.10

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 (53) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -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 +123 -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/install_bridge.rs +56 -8
  9. package/crates/naome-cli/src/main.rs +6 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  12. package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
  13. package/crates/naome-core/src/architecture/config/parser.rs +96 -0
  14. package/crates/naome-core/src/architecture/config.rs +114 -0
  15. package/crates/naome-core/src/architecture/model.rs +80 -0
  16. package/crates/naome-core/src/architecture/output.rs +178 -0
  17. package/crates/naome-core/src/architecture/rules.rs +140 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
  21. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  22. package/crates/naome-core/src/architecture/scan.rs +75 -0
  23. package/crates/naome-core/src/architecture.rs +31 -0
  24. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  25. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  26. package/crates/naome-core/src/harness_health.rs +58 -106
  27. package/crates/naome-core/src/install_plan.rs +2 -0
  28. package/crates/naome-core/src/lib.rs +16 -8
  29. package/crates/naome-core/src/quality/cache.rs +122 -19
  30. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  31. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  32. package/crates/naome-core/src/quality/scanner.rs +5 -2
  33. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  34. package/crates/naome-core/tests/architecture.rs +209 -0
  35. package/crates/naome-core/tests/harness_health.rs +150 -0
  36. package/crates/naome-core/tests/quality_performance.rs +63 -2
  37. package/installer/filesystem.js +38 -0
  38. package/installer/flows.js +6 -1
  39. package/installer/harness-file-ops.js +36 -8
  40. package/installer/harness-files.js +3 -0
  41. package/installer/manifest-state.js +2 -2
  42. package/installer/native.js +63 -18
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
  46. package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
  47. package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
  48. package/templates/naome-root/.naome/bin/naome.js +2 -2
  49. package/templates/naome-root/.naome/manifest.json +8 -6
  50. package/templates/naome-root/.naome/verification.json +15 -1
  51. package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
  52. package/templates/naome-root/docs/naome/index.md +4 -3
  53. package/templates/naome-root/docs/naome/testing.md +6 -3
package/Cargo.lock CHANGED
@@ -76,7 +76,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
76
76
 
77
77
  [[package]]
78
78
  name = "naome-cli"
79
- version = "1.3.8"
79
+ version = "1.3.10"
80
80
  dependencies = [
81
81
  "naome-core",
82
82
  "serde_json",
@@ -84,7 +84,7 @@ dependencies = [
84
84
 
85
85
  [[package]]
86
86
  name = "naome-core"
87
- version = "1.3.8"
87
+ version = "1.3.10"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/README.md CHANGED
@@ -83,6 +83,7 @@ naome quality report
83
83
  naome quality report --deep
84
84
  naome quality check --changed
85
85
  naome semantic check --changed
86
+ naome arch validate --changed-only
86
87
  naome task render-state --write --json
87
88
  naome commit -m "type(scope): summary"
88
89
  ```
@@ -103,6 +104,8 @@ After sync, NAOME writes the agent-facing workflow into `docs/naome/`:
103
104
  - `docs/naome/testing.md` maps change types to required checks.
104
105
  - `docs/naome/repository-quality.md` explains quality, structure, and cleanup
105
106
  policy.
107
+ - `docs/naome/architecture-fitness.md` explains architecture graph validation
108
+ and agent feedback.
106
109
 
107
110
  Agents should follow the repository's NAOME docs instead of guessing workflow
108
111
  rules from generic project files.
@@ -117,6 +120,7 @@ The main local policy files are:
117
120
  quality policy.
118
121
  - `.naome/repository-structure.json` for path role, module, and directory
119
122
  structure policy.
123
+ - `naome.arch.yaml` for language-agnostic architecture fitness rules.
120
124
  - `.naome/task-state.json` for the compact committed active task projection.
121
125
 
122
126
  Product defaults stay generic. Repository-specific policy belongs in the local
@@ -133,6 +137,7 @@ npm run test:naome-installer
133
137
  npm run pack:dry-run
134
138
  node .naome/bin/naome.js quality check --changed --json
135
139
  node .naome/bin/naome.js semantic check --changed --json
140
+ node .naome/bin/naome.js arch validate --changed-only --json
136
141
  git diff --check
137
142
  ```
138
143
 
package/bin/naome.js CHANGED
@@ -12,7 +12,7 @@ const packageVersion = packageMetadata.version;
12
12
  const nativeBinaryName = process.platform === "win32" ? "naome.exe" : "naome";
13
13
  const args = process.argv.slice(2);
14
14
  const [command] = args;
15
- const helpCommands = "status [--json]|next [--json]|intent --prompt-file <path> [--json]|intent --prompt <text> [--json]|route --prompt-file <path> [--execute] [--json]|route --prompt <text> [--execute] [--json]|explain --prompt-file <path> [--json]|explain --prompt <text> [--json]|context select --changed [--json]|context select --prompt-file <path> [--json]|context select --prompt <text> [--json]|doctor [--json]|install|sync [--check-update]|update [--json] [--execute]|task render-state [--write] [--json]|task migrate-ledger [--write] [--json]|quality init [--baseline|--deep-baseline] [--json]|quality reconcile [--write] [--json]|quality check --changed [--include-scanned-paths] [--json]|quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]|quality report [--deep] [--include-scanned-paths] [--json]|quality cache status [--json]|quality cache clear|semantic report [--deep] [--json]|semantic check --changed [--json]|semantic check --path <path> [--path <path>...] [--json]|semantic route --finding <id> [--json]|semantic loop [--json]|repo model [--write] [--json]|repo check [--json]|repo explain --path <path> [--json]|structure report [--json]|structure explain --path <path> [--json]|cleanup plan [--json]|cleanup route --path <path> [--json]|refresh-integrity [--json]|workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]|workflow search-profile|check-search|phases|processes|mutations [--json]|commit -m \"type(scope): message\"".split("|");
15
+ const helpCommands = "status [--json]|next [--json]|intent --prompt-file <path> [--json]|intent --prompt <text> [--json]|route --prompt-file <path> [--execute] [--json]|route --prompt <text> [--execute] [--json]|explain --prompt-file <path> [--json]|explain --prompt <text> [--json]|context select --changed [--json]|context select --prompt-file <path> [--json]|context select --prompt <text> [--json]|doctor [--json]|install|sync [--check-update]|update [--json] [--execute]|task render-state [--write] [--json]|task migrate-ledger [--write] [--json]|quality init [--baseline|--deep-baseline] [--json]|quality reconcile [--write] [--json]|quality check --changed [--include-scanned-paths] [--json]|quality check --path <path> [--path <path>...] [--include-scanned-paths] [--json]|quality report [--deep] [--include-scanned-paths] [--json]|quality cache status [--json]|quality cache clear|semantic report [--deep] [--json]|semantic check --changed [--json]|semantic check --path <path> [--path <path>...] [--json]|semantic route --finding <id> [--json]|semantic loop [--json]|arch init [--config <path>] [--json]|arch explain [--config <path>] [--json]|arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]|arch validate [--config <path>] [--changed-only] [--json|--agent-feedback]|repo model [--write] [--json]|repo check [--json]|repo explain --path <path> [--json]|structure report [--json]|structure explain --path <path> [--json]|cleanup plan [--json]|cleanup route --path <path> [--json]|refresh-integrity [--json]|workflow agent-plan|context-delta|proof-plan|capabilities|edit-watchdog|decision-gate|digest [--json]|workflow search-profile|check-search|phases|processes|mutations [--json]|commit -m \"type(scope): message\"".split("|");
16
16
 
17
17
  if (isHelpRequest(args)) {
18
18
  printHelp();
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-cli"
3
- version = "1.3.8"
3
+ version = "1.3.10"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -0,0 +1,123 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use naome_core::{
5
+ default_architecture_config_text, format_architecture_explain, format_architecture_scan,
6
+ format_architecture_validation, scan_architecture, validate_architecture,
7
+ ArchitectureScanOptions,
8
+ };
9
+
10
+ use crate::cli_args::{has_flag, option_value};
11
+
12
+ pub fn run_arch_command(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
13
+ let Some(subcommand) = args.get(1).map(String::as_str) else {
14
+ return Err("naome arch requires init, explain, scan, or validate".into());
15
+ };
16
+ match subcommand {
17
+ "init" => run_arch_init(root, args)?,
18
+ "explain" => run_arch_explain(root, args)?,
19
+ "scan" => run_arch_scan(root, args)?,
20
+ "validate" => run_arch_validate(root, args)?,
21
+ other => return Err(format!("unknown arch subcommand: {other}").into()),
22
+ }
23
+ Ok(())
24
+ }
25
+
26
+ fn run_arch_init(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
27
+ let path = config_path(root, args);
28
+ if path.exists() {
29
+ return Err(format!("{} already exists", display_path(root, &path)).into());
30
+ }
31
+ fs::write(&path, default_architecture_config_text())?;
32
+ if has_flag(args, "--json") {
33
+ println!(
34
+ "{}",
35
+ serde_json::json!({
36
+ "schema": "naome.arch.init.v1",
37
+ "created": display_path(root, &path)
38
+ })
39
+ );
40
+ } else {
41
+ println!("Created {}", display_path(root, &path));
42
+ }
43
+ Ok(())
44
+ }
45
+
46
+ fn run_arch_explain(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
47
+ let scan = scan_architecture(root, scan_options(root, args, false))?;
48
+ if has_flag(args, "--json") {
49
+ println!(
50
+ "{}",
51
+ serde_json::to_string_pretty(&serde_json::json!({
52
+ "schema": "naome.arch.explain.v1",
53
+ "layers": scan.config.layers.keys().collect::<Vec<_>>(),
54
+ "contexts": scan.config.contexts.keys().collect::<Vec<_>>(),
55
+ "rules": ["arch.max_file_lines", "arch.generated_manual_boundary"],
56
+ "extractors": ["path"]
57
+ }))?
58
+ );
59
+ } else {
60
+ print!("{}", format_architecture_explain(&scan));
61
+ }
62
+ Ok(())
63
+ }
64
+
65
+ fn run_arch_scan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
66
+ let scan = scan_architecture(
67
+ root,
68
+ scan_options(root, args, has_flag(args, "--changed-only")),
69
+ )?;
70
+ if has_flag(args, "--write") {
71
+ let output = option_value(args, "--output")
72
+ .map(PathBuf::from)
73
+ .unwrap_or_else(|| root.join(".naome/architecture-graph.json"));
74
+ fs::write(&output, serde_json::to_string_pretty(&scan.graph)?)?;
75
+ }
76
+ if has_flag(args, "--json") {
77
+ println!("{}", serde_json::to_string_pretty(&scan)?);
78
+ } else {
79
+ print!("{}", format_architecture_scan(&scan));
80
+ }
81
+ Ok(())
82
+ }
83
+
84
+ fn run_arch_validate(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
85
+ let report = validate_architecture(
86
+ root,
87
+ scan_options(root, args, has_flag(args, "--changed-only")),
88
+ )?;
89
+ let json = has_flag(args, "--json") || has_flag(args, "--agent-feedback");
90
+ if json {
91
+ if has_flag(args, "--agent-feedback") {
92
+ println!("{}", serde_json::to_string_pretty(&report.agent_feedback)?);
93
+ } else {
94
+ println!("{}", serde_json::to_string_pretty(&report)?);
95
+ }
96
+ } else {
97
+ print!("{}", format_architecture_validation(&report));
98
+ }
99
+ if report.status == "fail" {
100
+ std::process::exit(1);
101
+ }
102
+ Ok(())
103
+ }
104
+
105
+ fn scan_options(root: &Path, args: &[String], changed_only: bool) -> ArchitectureScanOptions {
106
+ ArchitectureScanOptions {
107
+ config_path: option_value(args, "--config").map(|path| root.join(path)),
108
+ changed_only,
109
+ }
110
+ }
111
+
112
+ fn config_path(root: &Path, args: &[String]) -> PathBuf {
113
+ option_value(args, "--config")
114
+ .map(|path| root.join(path))
115
+ .unwrap_or_else(|| root.join("naome.arch.yaml"))
116
+ }
117
+
118
+ fn display_path(root: &Path, path: &Path) -> String {
119
+ path.strip_prefix(root)
120
+ .unwrap_or(path)
121
+ .to_string_lossy()
122
+ .replace('\\', "/")
123
+ }
@@ -3,3 +3,7 @@ pub fn option_value(args: &[String], option: &str) -> Option<String> {
3
3
  .find(|window| window[0] == option)
4
4
  .map(|window| window[1].clone())
5
5
  }
6
+
7
+ pub fn has_flag(args: &[String], flag: &str) -> bool {
8
+ args.iter().any(|arg| arg == flag)
9
+ }
@@ -1,5 +1,6 @@
1
1
  use std::path::Path;
2
2
 
3
+ use crate::architecture_commands::run_arch_command;
3
4
  use crate::check_commands::{run_harness_health, run_task_state, run_verification_contract};
4
5
  use crate::context_commands::run_context_command;
5
6
  use crate::install_bridge::run_install_bridge;
@@ -32,6 +33,7 @@ pub fn dispatch_command(
32
33
  "repo" => run_repo_command(root, args)?,
33
34
  "structure" => run_structure_command(root, args)?,
34
35
  "cleanup" => run_cleanup_command(root, args)?,
36
+ "arch" => run_arch_command(root, args)?,
35
37
  "workflow" => run_workflow_command(root, args)?,
36
38
  "check-harness-health" => run_harness_health(root, args)?,
37
39
  "check-task-state" => run_task_state(root, args)?,
@@ -10,8 +10,7 @@ pub fn run_install_bridge(
10
10
  let package_root = option_value(args, "--package-root")
11
11
  .map(PathBuf::from)
12
12
  .or_else(|| std::env::var("NAOME_PACKAGE_ROOT").ok().map(PathBuf::from))
13
- .or_else(resolve_package_root_from_exe)
14
- .or_else(resolve_package_root_from_cwd);
13
+ .or_else(resolve_package_root_from_exe);
15
14
  let installer_js = option_value(args, "--installer-js")
16
15
  .map(PathBuf::from)
17
16
  .or_else(|| std::env::var("NAOME_INSTALLER_JS").ok().map(PathBuf::from))
@@ -72,12 +71,61 @@ fn resolve_package_root_from_exe() -> Option<PathBuf> {
72
71
  None
73
72
  }
74
73
 
75
- fn resolve_package_root_from_cwd() -> Option<PathBuf> {
76
- let current = std::env::current_dir().ok()?;
77
- for candidate in [current.join("packages").join("naome"), current] {
78
- if candidate.join("bin").join("naome-node.js").is_file() {
79
- return Some(candidate);
74
+ #[cfg(test)]
75
+ mod tests {
76
+ use std::fs;
77
+ use std::sync::{Mutex, OnceLock};
78
+ use std::time::{SystemTime, UNIX_EPOCH};
79
+
80
+ use super::run_install_bridge;
81
+
82
+ fn env_lock() -> &'static Mutex<()> {
83
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
84
+ LOCK.get_or_init(|| Mutex::new(()))
85
+ }
86
+
87
+ #[test]
88
+ fn install_bridge_does_not_discover_installer_from_cwd() {
89
+ let _guard = env_lock().lock().unwrap();
90
+ let original_dir = std::env::current_dir().unwrap();
91
+ let original_package_root = std::env::var_os("NAOME_PACKAGE_ROOT");
92
+ let original_installer_js = std::env::var_os("NAOME_INSTALLER_JS");
93
+ let temp_root = std::env::temp_dir().join(format!(
94
+ "naome-cwd-installer-test-{}",
95
+ SystemTime::now()
96
+ .duration_since(UNIX_EPOCH)
97
+ .unwrap()
98
+ .as_nanos()
99
+ ));
100
+ let installer_path = temp_root
101
+ .join("packages")
102
+ .join("naome")
103
+ .join("bin")
104
+ .join("naome-node.js");
105
+ fs::create_dir_all(installer_path.parent().unwrap()).unwrap();
106
+ fs::write(&installer_path, "console.log('untrusted cwd installer');\n").unwrap();
107
+
108
+ std::env::remove_var("NAOME_PACKAGE_ROOT");
109
+ std::env::remove_var("NAOME_INSTALLER_JS");
110
+ std::env::set_current_dir(&temp_root).unwrap();
111
+
112
+ let args = vec!["install".to_string()];
113
+ let error = run_install_bridge("install", &args)
114
+ .expect_err("cwd-local installer must not be treated as trusted");
115
+ assert!(
116
+ error.to_string().contains("needs naome-node.js"),
117
+ "unexpected error: {error}"
118
+ );
119
+
120
+ std::env::set_current_dir(original_dir).unwrap();
121
+ match original_package_root {
122
+ Some(value) => std::env::set_var("NAOME_PACKAGE_ROOT", value),
123
+ None => std::env::remove_var("NAOME_PACKAGE_ROOT"),
80
124
  }
125
+ match original_installer_js {
126
+ Some(value) => std::env::set_var("NAOME_INSTALLER_JS", value),
127
+ None => std::env::remove_var("NAOME_INSTALLER_JS"),
128
+ }
129
+ let _ = fs::remove_dir_all(temp_root);
81
130
  }
82
- None
83
131
  }
@@ -1,3 +1,4 @@
1
+ mod architecture_commands;
1
2
  mod check_commands;
2
3
  mod cli_args;
3
4
  mod context_commands;
@@ -57,6 +58,10 @@ const HELP: &str = r#"Usage:
57
58
  naome structure explain --path <path> [--json]
58
59
  naome cleanup plan [--json]
59
60
  naome cleanup route --path <path> [--json]
61
+ naome arch init [--config <path>] [--json]
62
+ naome arch explain [--config <path>] [--json]
63
+ naome arch scan [--config <path>] [--changed-only] [--write] [--output <path>] [--json]
64
+ naome arch validate [--config <path>] [--changed-only] [--agent-feedback] [--json]
60
65
  naome workflow search-profile [--json]
61
66
  naome workflow agent-plan [--json]
62
67
  naome workflow context-delta [--read-path <path>...] [--json]
@@ -93,6 +98,7 @@ const PUBLIC_COMMANDS: &[&str] = &[
93
98
  "repo",
94
99
  "structure",
95
100
  "cleanup",
101
+ "arch",
96
102
  "install-plan",
97
103
  "install",
98
104
  "sync",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.3.8"
3
+ version = "1.3.10"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -0,0 +1,26 @@
1
+ pub(super) fn indent(line: &str) -> usize {
2
+ line.chars().take_while(|ch| *ch == ' ').count()
3
+ }
4
+
5
+ pub(super) fn section_name(line: &str, expected_indent: usize) -> Option<&str> {
6
+ (indent(line) == expected_indent)
7
+ .then_some(line.trim())?
8
+ .strip_suffix(':')
9
+ .filter(|value| !value.is_empty())
10
+ }
11
+
12
+ pub(super) fn clean(value: &str) -> String {
13
+ value
14
+ .trim()
15
+ .trim_matches('"')
16
+ .trim_matches('\'')
17
+ .to_string()
18
+ }
19
+
20
+ pub(super) fn parse_bool(value: &str) -> Option<bool> {
21
+ match value {
22
+ "true" => Some(true),
23
+ "false" => Some(false),
24
+ _ => None,
25
+ }
26
+ }
@@ -0,0 +1,137 @@
1
+ use crate::models::NaomeError;
2
+
3
+ use super::scalar::{clean, indent, parse_bool, section_name};
4
+ use super::ConfigParser;
5
+ use crate::architecture::config::{ContextConfig, IgnoreRule, LayerConfig, RuleConfig};
6
+ use crate::architecture::output::Severity;
7
+
8
+ pub(super) fn parse_layers(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
9
+ while let Some((_, line)) = parser.peek_line() {
10
+ if indent(line) == 0 {
11
+ break;
12
+ }
13
+ let (line_number, line) = parser.next_line().unwrap();
14
+ let name = section_name(line, 2)
15
+ .ok_or_else(|| parser.error(line_number, "expected layer name".to_string()))?;
16
+ let paths = parser.parse_paths_block(4)?;
17
+ parser
18
+ .config
19
+ .layers
20
+ .insert(name.to_string(), LayerConfig { paths });
21
+ }
22
+ Ok(())
23
+ }
24
+
25
+ pub(super) fn parse_contexts(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
26
+ while let Some((_, line)) = parser.peek_line() {
27
+ if indent(line) == 0 {
28
+ break;
29
+ }
30
+ let (line_number, line) = parser.next_line().unwrap();
31
+ let name = section_name(line, 2)
32
+ .ok_or_else(|| parser.error(line_number, "expected context name".to_string()))?;
33
+ let context = parse_context(parser)?;
34
+ parser.config.contexts.insert(name.to_string(), context);
35
+ }
36
+ Ok(())
37
+ }
38
+
39
+ fn parse_context(parser: &mut ConfigParser<'_>) -> Result<ContextConfig, NaomeError> {
40
+ let mut context = ContextConfig::default();
41
+ while let Some((_, child)) = parser.peek_line() {
42
+ if indent(child) <= 2 {
43
+ break;
44
+ }
45
+ let (child_line, child) = parser.next_line().unwrap();
46
+ match child.trim() {
47
+ "paths:" => context.paths = parser.parse_list(6)?,
48
+ "public_api:" => context.public_api = parser.parse_list(6)?,
49
+ other => {
50
+ return Err(parser.error(child_line, format!("unsupported context key: {other}")))
51
+ }
52
+ }
53
+ }
54
+ Ok(context)
55
+ }
56
+
57
+ pub(super) fn parse_rules(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
58
+ while let Some((_, line)) = parser.peek_line() {
59
+ if indent(line) == 0 {
60
+ break;
61
+ }
62
+ let (line_number, line) = parser.next_line().unwrap();
63
+ let name = section_name(line, 2)
64
+ .ok_or_else(|| parser.error(line_number, "expected rule name".to_string()))?;
65
+ let rule = parse_rule(parser)?;
66
+ parser.config.rules.insert(name.to_string(), rule);
67
+ }
68
+ Ok(())
69
+ }
70
+
71
+ fn parse_rule(parser: &mut ConfigParser<'_>) -> Result<RuleConfig, NaomeError> {
72
+ let mut rule = RuleConfig::default();
73
+ while let Some((_, child)) = parser.peek_line() {
74
+ if indent(child) <= 2 {
75
+ break;
76
+ }
77
+ apply_rule_property(parser, &mut rule)?;
78
+ }
79
+ Ok(rule)
80
+ }
81
+
82
+ fn apply_rule_property(
83
+ parser: &mut ConfigParser<'_>,
84
+ rule: &mut RuleConfig,
85
+ ) -> Result<(), NaomeError> {
86
+ let (child_line, child) = parser.next_line().unwrap();
87
+ let trimmed = child.trim();
88
+ if let Some(value) = trimmed.strip_prefix("enabled:") {
89
+ rule.enabled = parse_bool(value.trim())
90
+ .ok_or_else(|| parser.error(child_line, "enabled must be true or false".to_string()))?;
91
+ } else if let Some(value) = trimmed.strip_prefix("severity:") {
92
+ rule.severity = Severity::parse(value.trim()).ok_or_else(|| {
93
+ parser.error(
94
+ child_line,
95
+ "severity must be error, warning, or info".to_string(),
96
+ )
97
+ })?;
98
+ } else if let Some(value) = trimmed.strip_prefix("value:") {
99
+ rule.value = Some(value.trim().parse::<usize>().map_err(|_| {
100
+ parser.error(child_line, "value must be an unsigned integer".to_string())
101
+ })?);
102
+ } else {
103
+ return Err(parser.error(child_line, format!("unsupported rule key: {trimmed}")));
104
+ }
105
+ Ok(())
106
+ }
107
+
108
+ pub(super) fn parse_ignore(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
109
+ while let Some((_, line)) = parser.peek_line() {
110
+ if indent(line) == 0 {
111
+ break;
112
+ }
113
+ let (line_number, line) = parser.next_line().unwrap();
114
+ let Some(path) = line.trim().strip_prefix("- path:").map(clean) else {
115
+ return Err(parser.error(
116
+ line_number,
117
+ "ignore entries must start with path".to_string(),
118
+ ));
119
+ };
120
+ let reason = parse_ignore_reason(parser);
121
+ if reason.trim().is_empty() {
122
+ return Err(parser.error(line_number, "ignore entries require a reason".to_string()));
123
+ }
124
+ parser.config.ignore.push(IgnoreRule { path, reason });
125
+ }
126
+ Ok(())
127
+ }
128
+
129
+ fn parse_ignore_reason(parser: &mut ConfigParser<'_>) -> String {
130
+ if let Some((_, next)) = parser.peek_line() {
131
+ if indent(next) > 2 && next.trim().starts_with("reason:") {
132
+ let (_, reason_line) = parser.next_line().unwrap();
133
+ return clean(reason_line.trim().trim_start_matches("reason:"));
134
+ }
135
+ }
136
+ String::new()
137
+ }
@@ -0,0 +1,96 @@
1
+ use crate::models::NaomeError;
2
+
3
+ use super::ArchitectureConfig;
4
+
5
+ mod scalar;
6
+ mod sections;
7
+
8
+ pub(super) fn parse_config(content: &str, source: &str) -> Result<ArchitectureConfig, NaomeError> {
9
+ let mut parser = ConfigParser::new(content, source);
10
+ parser.parse()
11
+ }
12
+
13
+ pub(super) struct ConfigParser<'a> {
14
+ pub(super) lines: Vec<(usize, &'a str)>,
15
+ pub(super) source: &'a str,
16
+ pub(super) index: usize,
17
+ pub(super) config: ArchitectureConfig,
18
+ }
19
+
20
+ impl<'a> ConfigParser<'a> {
21
+ fn new(content: &'a str, source: &'a str) -> Self {
22
+ let lines = content
23
+ .lines()
24
+ .enumerate()
25
+ .filter_map(|(index, line)| {
26
+ let trimmed = line.trim();
27
+ (!trimmed.is_empty() && !trimmed.starts_with('#')).then_some((index + 1, line))
28
+ })
29
+ .collect();
30
+ Self {
31
+ lines,
32
+ source,
33
+ index: 0,
34
+ config: ArchitectureConfig::default(),
35
+ }
36
+ }
37
+
38
+ fn parse(&mut self) -> Result<ArchitectureConfig, NaomeError> {
39
+ while let Some((line_number, line)) = self.next_line() {
40
+ match line.trim() {
41
+ "layers:" => sections::parse_layers(self)?,
42
+ "contexts:" => sections::parse_contexts(self)?,
43
+ "rules:" => sections::parse_rules(self)?,
44
+ "ignore:" => sections::parse_ignore(self)?,
45
+ other => {
46
+ return Err(
47
+ self.error(line_number, format!("unsupported top-level key: {other}"))
48
+ )
49
+ }
50
+ }
51
+ }
52
+ Ok(std::mem::take(&mut self.config))
53
+ }
54
+
55
+ pub(super) fn parse_paths_block(
56
+ &mut self,
57
+ key_indent: usize,
58
+ ) -> Result<Vec<String>, NaomeError> {
59
+ let Some((line_number, line)) = self.next_line() else {
60
+ return Ok(Vec::new());
61
+ };
62
+ if scalar::indent(line) != key_indent || line.trim() != "paths:" {
63
+ return Err(self.error(line_number, "expected paths block".to_string()));
64
+ }
65
+ self.parse_list(key_indent + 2)
66
+ }
67
+
68
+ pub(super) fn parse_list(&mut self, item_indent: usize) -> Result<Vec<String>, NaomeError> {
69
+ let mut values = Vec::new();
70
+ while let Some((_, line)) = self.peek_line() {
71
+ if scalar::indent(line) < item_indent || !line.trim().starts_with("- ") {
72
+ break;
73
+ }
74
+ let (line_number, line) = self.next_line().unwrap();
75
+ if scalar::indent(line) != item_indent {
76
+ return Err(self.error(line_number, "invalid list indentation".to_string()));
77
+ }
78
+ values.push(scalar::clean(line.trim().trim_start_matches("- ")));
79
+ }
80
+ Ok(values)
81
+ }
82
+
83
+ pub(super) fn next_line(&mut self) -> Option<(usize, &'a str)> {
84
+ let line = self.lines.get(self.index).copied();
85
+ self.index += usize::from(line.is_some());
86
+ line
87
+ }
88
+
89
+ pub(super) fn peek_line(&self) -> Option<(usize, &'a str)> {
90
+ self.lines.get(self.index).copied()
91
+ }
92
+
93
+ pub(super) fn error(&self, line: usize, message: String) -> NaomeError {
94
+ NaomeError::new(format!("{}:{line}: {message}", self.source))
95
+ }
96
+ }