@lamentis/naome 1.3.9 → 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 (38) 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/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 +137 -0
  12. package/crates/naome-core/src/architecture/config/parser.rs +96 -0
  13. package/crates/naome-core/src/architecture/config.rs +114 -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 +140 -0
  17. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
  20. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  21. package/crates/naome-core/src/architecture/scan.rs +75 -0
  22. package/crates/naome-core/src/architecture.rs +31 -0
  23. package/crates/naome-core/src/install_plan.rs +2 -0
  24. package/crates/naome-core/src/lib.rs +16 -8
  25. package/crates/naome-core/tests/architecture.rs +209 -0
  26. package/crates/naome-core/tests/harness_health.rs +1 -0
  27. package/installer/harness-files.js +3 -0
  28. package/native/darwin-arm64/naome +0 -0
  29. package/native/linux-x64/naome +0 -0
  30. package/package.json +1 -1
  31. package/templates/naome-root/.naome/bin/check-harness-health.js +7 -7
  32. package/templates/naome-root/.naome/bin/check-task-state.js +7 -7
  33. package/templates/naome-root/.naome/bin/naome.js +2 -2
  34. package/templates/naome-root/.naome/manifest.json +10 -8
  35. package/templates/naome-root/.naome/verification.json +15 -1
  36. package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
  37. package/templates/naome-root/docs/naome/index.md +4 -3
  38. 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.9"
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.9"
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.9"
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)?,
@@ -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.9"
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
+ }
@@ -0,0 +1,114 @@
1
+ use std::collections::BTreeMap;
2
+ use std::fs;
3
+ use std::path::Path;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::output::Severity;
8
+
9
+ mod parser;
10
+
11
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
12
+ pub struct ArchitectureConfig {
13
+ pub layers: BTreeMap<String, LayerConfig>,
14
+ pub contexts: BTreeMap<String, ContextConfig>,
15
+ pub rules: BTreeMap<String, RuleConfig>,
16
+ pub ignore: Vec<IgnoreRule>,
17
+ }
18
+
19
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
20
+ pub struct LayerConfig {
21
+ pub paths: Vec<String>,
22
+ }
23
+
24
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
25
+ pub struct ContextConfig {
26
+ pub paths: Vec<String>,
27
+ pub public_api: Vec<String>,
28
+ }
29
+
30
+ #[derive(Debug, Clone, PartialEq, Eq)]
31
+ pub struct RuleConfig {
32
+ pub enabled: bool,
33
+ pub severity: Severity,
34
+ pub value: Option<usize>,
35
+ }
36
+
37
+ #[derive(Debug, Clone, PartialEq, Eq)]
38
+ pub struct IgnoreRule {
39
+ pub path: String,
40
+ pub reason: String,
41
+ }
42
+
43
+ impl Default for RuleConfig {
44
+ fn default() -> Self {
45
+ Self {
46
+ enabled: true,
47
+ severity: Severity::Warning,
48
+ value: None,
49
+ }
50
+ }
51
+ }
52
+
53
+ pub fn read_architecture_config(
54
+ root: &Path,
55
+ explicit_path: Option<&Path>,
56
+ ) -> Result<ArchitectureConfig, NaomeError> {
57
+ let path = explicit_path
58
+ .map(Path::to_path_buf)
59
+ .unwrap_or_else(|| root.join("naome.arch.yaml"));
60
+ if !path.exists() {
61
+ return Ok(ArchitectureConfig::starter());
62
+ }
63
+ ArchitectureConfig::parse(&fs::read_to_string(path)?, "naome.arch.yaml")
64
+ }
65
+
66
+ pub fn default_architecture_config_text() -> &'static str {
67
+ r#"# NAOME architecture fitness configuration.
68
+ layers:
69
+ application:
70
+ paths:
71
+ - "src/**"
72
+ - "packages/**/src/**"
73
+ domain:
74
+ paths:
75
+ - "src/domain/**"
76
+ infrastructure:
77
+ paths:
78
+ - "src/infrastructure/**"
79
+
80
+ contexts:
81
+ default:
82
+ paths:
83
+ - "src/**"
84
+ public_api:
85
+ - "src/index.*"
86
+
87
+ rules:
88
+ max_file_lines:
89
+ enabled: true
90
+ value: 400
91
+ severity: warning
92
+ generated_manual_boundary:
93
+ enabled: true
94
+ severity: error
95
+
96
+ ignore:
97
+ - path: "generated/**"
98
+ reason: "Generated code is not architecture-owned."
99
+ "#
100
+ }
101
+
102
+ impl ArchitectureConfig {
103
+ pub fn starter() -> Self {
104
+ Self::parse(default_architecture_config_text(), "default").unwrap_or_default()
105
+ }
106
+
107
+ pub fn parse(content: &str, source: &str) -> Result<Self, NaomeError> {
108
+ parser::parse_config(content, source)
109
+ }
110
+
111
+ pub fn rule(&self, id: &str) -> RuleConfig {
112
+ self.rules.get(id).cloned().unwrap_or_default()
113
+ }
114
+ }