@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.
- package/Cargo.lock +2 -2
- package/README.md +5 -0
- package/bin/naome.js +1 -1
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/architecture_commands.rs +123 -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/install_bridge.rs +56 -8
- 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 +137 -0
- package/crates/naome-core/src/architecture/config/parser.rs +96 -0
- package/crates/naome-core/src/architecture/config.rs +114 -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 +140 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
- package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
- package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
- package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
- package/crates/naome-core/src/architecture/scan.rs +75 -0
- package/crates/naome-core/src/architecture.rs +31 -0
- package/crates/naome-core/src/harness_health/integrity.rs +41 -23
- package/crates/naome-core/src/harness_health/manifest.rs +97 -0
- package/crates/naome-core/src/harness_health.rs +58 -106
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +16 -8
- package/crates/naome-core/src/quality/cache.rs +122 -19
- package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
- package/crates/naome-core/src/quality/scanner.rs +5 -2
- package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
- package/crates/naome-core/tests/architecture.rs +209 -0
- package/crates/naome-core/tests/harness_health.rs +150 -0
- package/crates/naome-core/tests/quality_performance.rs +63 -2
- package/installer/filesystem.js +38 -0
- package/installer/flows.js +6 -1
- package/installer/harness-file-ops.js +36 -8
- package/installer/harness-files.js +3 -0
- package/installer/manifest-state.js +2 -2
- package/installer/native.js +63 -18
- 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 +23 -19
- package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
- package/templates/naome-root/.naome/bin/naome.js +2 -2
- package/templates/naome-root/.naome/manifest.json +8 -6
- package/templates/naome-root/.naome/verification.json +15 -1
- package/templates/naome-root/docs/naome/architecture-fitness.md +97 -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.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.
|
|
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();
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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",
|
|
@@ -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
|
+
}
|