@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.
- package/Cargo.lock +2 -2
- package/README.md +10 -0
- package/bin/naome.js +1 -1
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +127 -0
- package/crates/naome-cli/src/cli_args.rs +4 -0
- package/crates/naome-cli/src/dispatcher.rs +2 -0
- package/crates/naome-cli/src/main.rs +6 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
- package/crates/naome-core/src/architecture/config/parser/sections.rs +154 -0
- package/crates/naome-core/src/architecture/config/parser.rs +97 -0
- package/crates/naome-core/src/architecture/config.rs +126 -0
- package/crates/naome-core/src/architecture/model.rs +80 -0
- package/crates/naome-core/src/architecture/output.rs +178 -0
- package/crates/naome-core/src/architecture/rules.rs +212 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +118 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +87 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +211 -0
- package/crates/naome-core/src/architecture/scan/imports/extractors.rs +407 -0
- package/crates/naome-core/src/architecture/scan/imports/resolver.rs +334 -0
- package/crates/naome-core/src/architecture/scan/imports.rs +59 -0
- package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
- package/crates/naome-core/src/architecture/scan.rs +95 -0
- package/crates/naome-core/src/architecture.rs +31 -0
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +16 -8
- package/crates/naome-core/tests/architecture.rs +548 -0
- package/crates/naome-core/tests/harness_health.rs +1 -0
- package/installer/harness-files.js +3 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +7 -7
- package/templates/naome-root/.naome/bin/check-task-state.js +7 -7
- package/templates/naome-root/.naome/bin/naome.js +2 -2
- package/templates/naome-root/.naome/manifest.json +10 -8
- package/templates/naome-root/.naome/verification.json +15 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +109 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- 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.
|
|
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.
|
|
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();
|
|
@@ -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
|
+
}
|
|
@@ -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",
|
|
@@ -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
|
+
}
|