@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
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.11"
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.11"
88
88
  dependencies = [
89
89
  "serde",
90
90
  "serde_json",
package/README.md CHANGED
@@ -83,10 +83,16 @@ 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
  ```
89
90
 
91
+ Architecture fitness builds a language-agnostic graph, extracts direct imports
92
+ for TypeScript, JavaScript, Rust, Python, and Go, and can enforce configured
93
+ layer dependency rules such as keeping domain code independent from
94
+ infrastructure adapters.
95
+
90
96
  `naome sync` installs or repairs the local harness files. It does not run a
91
97
  hidden full-repository quality scan. It also migrates any active legacy
92
98
  task-state into the local task ledger automatically and untracks local
@@ -103,6 +109,8 @@ After sync, NAOME writes the agent-facing workflow into `docs/naome/`:
103
109
  - `docs/naome/testing.md` maps change types to required checks.
104
110
  - `docs/naome/repository-quality.md` explains quality, structure, and cleanup
105
111
  policy.
112
+ - `docs/naome/architecture-fitness.md` explains architecture graph validation
113
+ and agent feedback.
106
114
 
107
115
  Agents should follow the repository's NAOME docs instead of guessing workflow
108
116
  rules from generic project files.
@@ -117,6 +125,7 @@ The main local policy files are:
117
125
  quality policy.
118
126
  - `.naome/repository-structure.json` for path role, module, and directory
119
127
  structure policy.
128
+ - `naome.arch.yaml` for language-agnostic architecture fitness rules.
120
129
  - `.naome/task-state.json` for the compact committed active task projection.
121
130
 
122
131
  Product defaults stay generic. Repository-specific policy belongs in the local
@@ -133,6 +142,7 @@ npm run test:naome-installer
133
142
  npm run pack:dry-run
134
143
  node .naome/bin/naome.js quality check --changed --json
135
144
  node .naome/bin/naome.js semantic check --changed --json
145
+ node .naome/bin/naome.js arch validate --changed-only --json
136
146
  git diff --check
137
147
  ```
138
148
 
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.11"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -0,0 +1,127 @@
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": [
56
+ "arch.max_file_lines",
57
+ "arch.generated_manual_boundary",
58
+ "arch.no_forbidden_layer_dependencies"
59
+ ],
60
+ "extractors": ["path", "typescript", "javascript", "rust", "python", "go"]
61
+ }))?
62
+ );
63
+ } else {
64
+ print!("{}", format_architecture_explain(&scan));
65
+ }
66
+ Ok(())
67
+ }
68
+
69
+ fn run_arch_scan(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
70
+ let scan = scan_architecture(
71
+ root,
72
+ scan_options(root, args, has_flag(args, "--changed-only")),
73
+ )?;
74
+ if has_flag(args, "--write") {
75
+ let output = option_value(args, "--output")
76
+ .map(PathBuf::from)
77
+ .unwrap_or_else(|| root.join(".naome/architecture-graph.json"));
78
+ fs::write(&output, serde_json::to_string_pretty(&scan.graph)?)?;
79
+ }
80
+ if has_flag(args, "--json") {
81
+ println!("{}", serde_json::to_string_pretty(&scan)?);
82
+ } else {
83
+ print!("{}", format_architecture_scan(&scan));
84
+ }
85
+ Ok(())
86
+ }
87
+
88
+ fn run_arch_validate(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
89
+ let report = validate_architecture(
90
+ root,
91
+ scan_options(root, args, has_flag(args, "--changed-only")),
92
+ )?;
93
+ let json = has_flag(args, "--json") || has_flag(args, "--agent-feedback");
94
+ if json {
95
+ if has_flag(args, "--agent-feedback") {
96
+ println!("{}", serde_json::to_string_pretty(&report.agent_feedback)?);
97
+ } else {
98
+ println!("{}", serde_json::to_string_pretty(&report)?);
99
+ }
100
+ } else {
101
+ print!("{}", format_architecture_validation(&report));
102
+ }
103
+ if report.status == "fail" {
104
+ std::process::exit(1);
105
+ }
106
+ Ok(())
107
+ }
108
+
109
+ fn scan_options(root: &Path, args: &[String], changed_only: bool) -> ArchitectureScanOptions {
110
+ ArchitectureScanOptions {
111
+ config_path: option_value(args, "--config").map(|path| root.join(path)),
112
+ changed_only,
113
+ }
114
+ }
115
+
116
+ fn config_path(root: &Path, args: &[String]) -> PathBuf {
117
+ option_value(args, "--config")
118
+ .map(|path| root.join(path))
119
+ .unwrap_or_else(|| root.join("naome.arch.yaml"))
120
+ }
121
+
122
+ fn display_path(root: &Path, path: &Path) -> String {
123
+ path.strip_prefix(root)
124
+ .unwrap_or(path)
125
+ .to_string_lossy()
126
+ .replace('\\', "/")
127
+ }
@@ -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.11"
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,154 @@
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
+ pub(super) fn parse_allowed_dependencies(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
40
+ while let Some((_, line)) = parser.peek_line() {
41
+ if indent(line) == 0 {
42
+ break;
43
+ }
44
+ let (line_number, line) = parser.next_line().unwrap();
45
+ let name = section_name(line, 2)
46
+ .ok_or_else(|| parser.error(line_number, "expected dependency source".to_string()))?;
47
+ let dependencies = parser.parse_list(4)?;
48
+ parser
49
+ .config
50
+ .allowed_dependencies
51
+ .insert(name.to_string(), dependencies);
52
+ }
53
+ Ok(())
54
+ }
55
+
56
+ fn parse_context(parser: &mut ConfigParser<'_>) -> Result<ContextConfig, NaomeError> {
57
+ let mut context = ContextConfig::default();
58
+ while let Some((_, child)) = parser.peek_line() {
59
+ if indent(child) <= 2 {
60
+ break;
61
+ }
62
+ let (child_line, child) = parser.next_line().unwrap();
63
+ match child.trim() {
64
+ "paths:" => context.paths = parser.parse_list(6)?,
65
+ "public_api:" => context.public_api = parser.parse_list(6)?,
66
+ other => {
67
+ return Err(parser.error(child_line, format!("unsupported context key: {other}")))
68
+ }
69
+ }
70
+ }
71
+ Ok(context)
72
+ }
73
+
74
+ pub(super) fn parse_rules(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
75
+ while let Some((_, line)) = parser.peek_line() {
76
+ if indent(line) == 0 {
77
+ break;
78
+ }
79
+ let (line_number, line) = parser.next_line().unwrap();
80
+ let name = section_name(line, 2)
81
+ .ok_or_else(|| parser.error(line_number, "expected rule name".to_string()))?;
82
+ let rule = parse_rule(parser)?;
83
+ parser.config.rules.insert(name.to_string(), rule);
84
+ }
85
+ Ok(())
86
+ }
87
+
88
+ fn parse_rule(parser: &mut ConfigParser<'_>) -> Result<RuleConfig, NaomeError> {
89
+ let mut rule = RuleConfig::default();
90
+ while let Some((_, child)) = parser.peek_line() {
91
+ if indent(child) <= 2 {
92
+ break;
93
+ }
94
+ apply_rule_property(parser, &mut rule)?;
95
+ }
96
+ Ok(rule)
97
+ }
98
+
99
+ fn apply_rule_property(
100
+ parser: &mut ConfigParser<'_>,
101
+ rule: &mut RuleConfig,
102
+ ) -> Result<(), NaomeError> {
103
+ let (child_line, child) = parser.next_line().unwrap();
104
+ let trimmed = child.trim();
105
+ if let Some(value) = trimmed.strip_prefix("enabled:") {
106
+ rule.enabled = parse_bool(value.trim())
107
+ .ok_or_else(|| parser.error(child_line, "enabled must be true or false".to_string()))?;
108
+ } else if let Some(value) = trimmed.strip_prefix("severity:") {
109
+ rule.severity = Severity::parse(value.trim()).ok_or_else(|| {
110
+ parser.error(
111
+ child_line,
112
+ "severity must be error, warning, or info".to_string(),
113
+ )
114
+ })?;
115
+ } else if let Some(value) = trimmed.strip_prefix("value:") {
116
+ rule.value = Some(value.trim().parse::<usize>().map_err(|_| {
117
+ parser.error(child_line, "value must be an unsigned integer".to_string())
118
+ })?);
119
+ } else {
120
+ return Err(parser.error(child_line, format!("unsupported rule key: {trimmed}")));
121
+ }
122
+ Ok(())
123
+ }
124
+
125
+ pub(super) fn parse_ignore(parser: &mut ConfigParser<'_>) -> Result<(), NaomeError> {
126
+ while let Some((_, line)) = parser.peek_line() {
127
+ if indent(line) == 0 {
128
+ break;
129
+ }
130
+ let (line_number, line) = parser.next_line().unwrap();
131
+ let Some(path) = line.trim().strip_prefix("- path:").map(clean) else {
132
+ return Err(parser.error(
133
+ line_number,
134
+ "ignore entries must start with path".to_string(),
135
+ ));
136
+ };
137
+ let reason = parse_ignore_reason(parser);
138
+ if reason.trim().is_empty() {
139
+ return Err(parser.error(line_number, "ignore entries require a reason".to_string()));
140
+ }
141
+ parser.config.ignore.push(IgnoreRule { path, reason });
142
+ }
143
+ Ok(())
144
+ }
145
+
146
+ fn parse_ignore_reason(parser: &mut ConfigParser<'_>) -> String {
147
+ if let Some((_, next)) = parser.peek_line() {
148
+ if indent(next) > 2 && next.trim().starts_with("reason:") {
149
+ let (_, reason_line) = parser.next_line().unwrap();
150
+ return clean(reason_line.trim().trim_start_matches("reason:"));
151
+ }
152
+ }
153
+ String::new()
154
+ }
@@ -0,0 +1,97 @@
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
+ "allowed_dependencies:" => sections::parse_allowed_dependencies(self)?,
44
+ "rules:" => sections::parse_rules(self)?,
45
+ "ignore:" => sections::parse_ignore(self)?,
46
+ other => {
47
+ return Err(
48
+ self.error(line_number, format!("unsupported top-level key: {other}"))
49
+ )
50
+ }
51
+ }
52
+ }
53
+ Ok(std::mem::take(&mut self.config))
54
+ }
55
+
56
+ pub(super) fn parse_paths_block(
57
+ &mut self,
58
+ key_indent: usize,
59
+ ) -> Result<Vec<String>, NaomeError> {
60
+ let Some((line_number, line)) = self.next_line() else {
61
+ return Ok(Vec::new());
62
+ };
63
+ if scalar::indent(line) != key_indent || line.trim() != "paths:" {
64
+ return Err(self.error(line_number, "expected paths block".to_string()));
65
+ }
66
+ self.parse_list(key_indent + 2)
67
+ }
68
+
69
+ pub(super) fn parse_list(&mut self, item_indent: usize) -> Result<Vec<String>, NaomeError> {
70
+ let mut values = Vec::new();
71
+ while let Some((_, line)) = self.peek_line() {
72
+ if scalar::indent(line) < item_indent || !line.trim().starts_with("- ") {
73
+ break;
74
+ }
75
+ let (line_number, line) = self.next_line().unwrap();
76
+ if scalar::indent(line) != item_indent {
77
+ return Err(self.error(line_number, "invalid list indentation".to_string()));
78
+ }
79
+ values.push(scalar::clean(line.trim().trim_start_matches("- ")));
80
+ }
81
+ Ok(values)
82
+ }
83
+
84
+ pub(super) fn next_line(&mut self) -> Option<(usize, &'a str)> {
85
+ let line = self.lines.get(self.index).copied();
86
+ self.index += usize::from(line.is_some());
87
+ line
88
+ }
89
+
90
+ pub(super) fn peek_line(&self) -> Option<(usize, &'a str)> {
91
+ self.lines.get(self.index).copied()
92
+ }
93
+
94
+ pub(super) fn error(&self, line: usize, message: String) -> NaomeError {
95
+ NaomeError::new(format!("{}:{line}: {message}", self.source))
96
+ }
97
+ }
@@ -0,0 +1,126 @@
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 allowed_dependencies: BTreeMap<String, Vec<String>>,
16
+ pub rules: BTreeMap<String, RuleConfig>,
17
+ pub ignore: Vec<IgnoreRule>,
18
+ }
19
+
20
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
21
+ pub struct LayerConfig {
22
+ pub paths: Vec<String>,
23
+ }
24
+
25
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
26
+ pub struct ContextConfig {
27
+ pub paths: Vec<String>,
28
+ pub public_api: Vec<String>,
29
+ }
30
+
31
+ #[derive(Debug, Clone, PartialEq, Eq)]
32
+ pub struct RuleConfig {
33
+ pub enabled: bool,
34
+ pub severity: Severity,
35
+ pub value: Option<usize>,
36
+ }
37
+
38
+ #[derive(Debug, Clone, PartialEq, Eq)]
39
+ pub struct IgnoreRule {
40
+ pub path: String,
41
+ pub reason: String,
42
+ }
43
+
44
+ impl Default for RuleConfig {
45
+ fn default() -> Self {
46
+ Self {
47
+ enabled: true,
48
+ severity: Severity::Warning,
49
+ value: None,
50
+ }
51
+ }
52
+ }
53
+
54
+ pub fn read_architecture_config(
55
+ root: &Path,
56
+ explicit_path: Option<&Path>,
57
+ ) -> Result<ArchitectureConfig, NaomeError> {
58
+ let path = explicit_path
59
+ .map(Path::to_path_buf)
60
+ .unwrap_or_else(|| root.join("naome.arch.yaml"));
61
+ if !path.exists() {
62
+ return Ok(ArchitectureConfig::starter());
63
+ }
64
+ ArchitectureConfig::parse(&fs::read_to_string(path)?, "naome.arch.yaml")
65
+ }
66
+
67
+ pub fn default_architecture_config_text() -> &'static str {
68
+ r#"# NAOME architecture fitness configuration.
69
+ layers:
70
+ application:
71
+ paths:
72
+ - "src/**"
73
+ - "packages/**/src/**"
74
+ domain:
75
+ paths:
76
+ - "src/domain/**"
77
+ infrastructure:
78
+ paths:
79
+ - "src/infrastructure/**"
80
+
81
+ allowed_dependencies:
82
+ application:
83
+ - domain
84
+ - infrastructure
85
+ domain:
86
+ infrastructure:
87
+ - domain
88
+
89
+ contexts:
90
+ default:
91
+ paths:
92
+ - "src/**"
93
+ public_api:
94
+ - "src/index.*"
95
+
96
+ rules:
97
+ max_file_lines:
98
+ enabled: true
99
+ value: 400
100
+ severity: warning
101
+ generated_manual_boundary:
102
+ enabled: true
103
+ severity: error
104
+ no_forbidden_layer_dependencies:
105
+ enabled: true
106
+ severity: error
107
+
108
+ ignore:
109
+ - path: "generated/**"
110
+ reason: "Generated code is not architecture-owned."
111
+ "#
112
+ }
113
+
114
+ impl ArchitectureConfig {
115
+ pub fn starter() -> Self {
116
+ Self::parse(default_architecture_config_text(), "default").unwrap_or_default()
117
+ }
118
+
119
+ pub fn parse(content: &str, source: &str) -> Result<Self, NaomeError> {
120
+ parser::parse_config(content, source)
121
+ }
122
+
123
+ pub fn rule(&self, id: &str) -> RuleConfig {
124
+ self.rules.get(id).cloned().unwrap_or_default()
125
+ }
126
+ }