@lamentis/naome 1.3.8 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/bin/naome.js +1 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/architecture_commands.rs +123 -0
  6. package/crates/naome-cli/src/cli_args.rs +4 -0
  7. package/crates/naome-cli/src/dispatcher.rs +2 -0
  8. package/crates/naome-cli/src/install_bridge.rs +56 -8
  9. package/crates/naome-cli/src/main.rs +6 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  12. package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
  13. package/crates/naome-core/src/architecture/config/parser.rs +96 -0
  14. package/crates/naome-core/src/architecture/config.rs +114 -0
  15. package/crates/naome-core/src/architecture/model.rs +80 -0
  16. package/crates/naome-core/src/architecture/output.rs +178 -0
  17. package/crates/naome-core/src/architecture/rules.rs +140 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
  21. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  22. package/crates/naome-core/src/architecture/scan.rs +75 -0
  23. package/crates/naome-core/src/architecture.rs +31 -0
  24. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  25. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  26. package/crates/naome-core/src/harness_health.rs +58 -106
  27. package/crates/naome-core/src/install_plan.rs +2 -0
  28. package/crates/naome-core/src/lib.rs +16 -8
  29. package/crates/naome-core/src/quality/cache.rs +122 -19
  30. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  31. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  32. package/crates/naome-core/src/quality/scanner.rs +5 -2
  33. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  34. package/crates/naome-core/tests/architecture.rs +209 -0
  35. package/crates/naome-core/tests/harness_health.rs +150 -0
  36. package/crates/naome-core/tests/quality_performance.rs +63 -2
  37. package/installer/filesystem.js +38 -0
  38. package/installer/flows.js +6 -1
  39. package/installer/harness-file-ops.js +36 -8
  40. package/installer/harness-files.js +3 -0
  41. package/installer/manifest-state.js +2 -2
  42. package/installer/native.js +63 -18
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
  46. package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
  47. package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
  48. package/templates/naome-root/.naome/bin/naome.js +2 -2
  49. package/templates/naome-root/.naome/manifest.json +8 -6
  50. package/templates/naome-root/.naome/verification.json +15 -1
  51. package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
  52. package/templates/naome-root/docs/naome/index.md +4 -3
  53. package/templates/naome-root/docs/naome/testing.md +6 -3
@@ -0,0 +1,134 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::Path;
3
+
4
+ use serde_json::json;
5
+
6
+ use super::FileFact;
7
+ use crate::architecture::config::ArchitectureConfig;
8
+ use crate::architecture::model::{ArchitectureEdgeKind, ArchitectureGraph, ArchitectureNodeKind};
9
+
10
+ mod emit;
11
+ mod facts;
12
+
13
+ pub(super) fn build_path_graph(
14
+ root: &Path,
15
+ files: Vec<String>,
16
+ config: &ArchitectureConfig,
17
+ ) -> (ArchitectureGraph, BTreeMap<String, FileFact>) {
18
+ let mut graph = ArchitectureGraph::default();
19
+ let mut file_facts = BTreeMap::new();
20
+
21
+ push_repository_and_policy_nodes(&mut graph, config);
22
+ push_directories(&mut graph, &files);
23
+
24
+ for path in files {
25
+ let fact = facts::file_fact(root, &path, config);
26
+ push_file(&mut graph, &fact);
27
+ file_facts.insert(path, fact);
28
+ }
29
+
30
+ (graph, file_facts)
31
+ }
32
+
33
+ fn push_repository_and_policy_nodes(graph: &mut ArchitectureGraph, config: &ArchitectureConfig) {
34
+ emit::push_node(
35
+ graph,
36
+ "repository:.",
37
+ ArchitectureNodeKind::Repository,
38
+ "repository",
39
+ None,
40
+ None,
41
+ json!({ "root": "." }),
42
+ );
43
+ for layer in config.layers.keys() {
44
+ emit::push_node(
45
+ graph,
46
+ &format!("layer:{layer}"),
47
+ ArchitectureNodeKind::Layer,
48
+ layer,
49
+ None,
50
+ None,
51
+ json!({ "layer": layer }),
52
+ );
53
+ }
54
+ for context in config.contexts.keys() {
55
+ emit::push_node(
56
+ graph,
57
+ &format!("context:{context}"),
58
+ ArchitectureNodeKind::BoundedContext,
59
+ context,
60
+ None,
61
+ None,
62
+ json!({ "context": context }),
63
+ );
64
+ }
65
+ }
66
+
67
+ fn push_directories(graph: &mut ArchitectureGraph, files: &[String]) {
68
+ let mut directories = BTreeSet::new();
69
+ for path in files {
70
+ facts::collect_directories(path, &mut directories);
71
+ }
72
+ for directory in directories {
73
+ emit::push_node(
74
+ graph,
75
+ &format!("directory:{directory}"),
76
+ ArchitectureNodeKind::Directory,
77
+ &directory,
78
+ Some(directory.clone()),
79
+ None,
80
+ json!({ "path": directory }),
81
+ );
82
+ emit::push_edge(
83
+ graph,
84
+ "repository:.",
85
+ &format!("directory:{directory}"),
86
+ ArchitectureEdgeKind::Contains,
87
+ "contains",
88
+ Some(directory),
89
+ );
90
+ }
91
+ }
92
+
93
+ fn push_file(graph: &mut ArchitectureGraph, fact: &FileFact) {
94
+ let path = &fact.path;
95
+ emit::push_node(
96
+ graph,
97
+ &format!("file:{path}"),
98
+ ArchitectureNodeKind::File,
99
+ path,
100
+ Some(path.clone()),
101
+ fact.language.clone(),
102
+ json!({ "path": path, "extractor": "path" }),
103
+ );
104
+ emit::push_edge(
105
+ graph,
106
+ facts::parent_node_id(path)
107
+ .as_deref()
108
+ .unwrap_or("repository:."),
109
+ &format!("file:{path}"),
110
+ ArchitectureEdgeKind::Contains,
111
+ "contains",
112
+ Some(path.clone()),
113
+ );
114
+ push_membership_edges(graph, path, "layer", &fact.layers);
115
+ push_membership_edges(graph, path, "context", &fact.contexts);
116
+ }
117
+
118
+ fn push_membership_edges(
119
+ graph: &mut ArchitectureGraph,
120
+ path: &str,
121
+ prefix: &str,
122
+ names: &[String],
123
+ ) {
124
+ for name in names {
125
+ emit::push_edge(
126
+ graph,
127
+ &format!("{prefix}:{name}"),
128
+ &format!("file:{path}"),
129
+ ArchitectureEdgeKind::Contains,
130
+ "contains",
131
+ Some(path.to_string()),
132
+ );
133
+ }
134
+ }
@@ -0,0 +1,92 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use crate::models::NaomeError;
5
+ use crate::paths;
6
+
7
+ use crate::architecture::config::ArchitectureConfig;
8
+
9
+ pub(super) fn repository_files(
10
+ root: &Path,
11
+ config: &ArchitectureConfig,
12
+ ) -> Result<Vec<String>, NaomeError> {
13
+ let ignored_patterns = ignored_patterns(root, config);
14
+ let mut files = Vec::new();
15
+ collect_files(root, root, &ignored_patterns, &mut files)?;
16
+ files.sort();
17
+ Ok(files)
18
+ }
19
+
20
+ fn collect_files(
21
+ root: &Path,
22
+ dir: &Path,
23
+ ignored_patterns: &[String],
24
+ files: &mut Vec<String>,
25
+ ) -> Result<(), NaomeError> {
26
+ for entry in fs::read_dir(dir)? {
27
+ let entry = entry?;
28
+ let path = entry.path();
29
+ let relative = normalize_path(path.strip_prefix(root).unwrap_or(&path));
30
+ if is_ignored(&relative, ignored_patterns) {
31
+ continue;
32
+ }
33
+ let metadata = fs::symlink_metadata(&path)?;
34
+ if metadata.is_symlink() {
35
+ continue;
36
+ }
37
+ if metadata.is_dir() {
38
+ collect_files(root, &path, ignored_patterns, files)?;
39
+ } else if metadata.is_file() {
40
+ files.push(relative);
41
+ }
42
+ }
43
+ Ok(())
44
+ }
45
+
46
+ fn is_ignored(path: &str, ignored_patterns: &[String]) -> bool {
47
+ path.is_empty() || is_default_ignored_path(path) || paths::matches_any(path, ignored_patterns)
48
+ }
49
+
50
+ fn ignored_patterns(root: &Path, config: &ArchitectureConfig) -> Vec<String> {
51
+ let mut patterns = paths::naomeignore_patterns(root);
52
+ patterns.extend([
53
+ ".git/**".to_string(),
54
+ ".naome/archive/**".to_string(),
55
+ ".naome/cache/**".to_string(),
56
+ ".naome/local/**".to_string(),
57
+ ".naome/tasks/**".to_string(),
58
+ "node_modules/**".to_string(),
59
+ "target/**".to_string(),
60
+ "dist/**".to_string(),
61
+ "build/**".to_string(),
62
+ ]);
63
+ patterns.extend(config.ignore.iter().map(|rule| rule.path.clone()));
64
+ patterns
65
+ }
66
+
67
+ fn is_default_ignored_path(path: &str) -> bool {
68
+ path == ".git"
69
+ || path.starts_with(".git/")
70
+ || path == ".naome/archive"
71
+ || path.starts_with(".naome/archive/")
72
+ || path == ".naome/cache"
73
+ || path.starts_with(".naome/cache/")
74
+ || path == ".naome/local"
75
+ || path.starts_with(".naome/local/")
76
+ || path == ".naome/tasks"
77
+ || path.starts_with(".naome/tasks/")
78
+ || path == ".npm"
79
+ || path.starts_with(".npm/")
80
+ || path == "node_modules"
81
+ || path.contains("/node_modules/")
82
+ || path == "target"
83
+ || path.contains("/target/")
84
+ || path == "dist"
85
+ || path.contains("/dist/")
86
+ || path == "build"
87
+ || path.contains("/build/")
88
+ }
89
+
90
+ fn normalize_path(path: impl AsRef<Path>) -> String {
91
+ path.as_ref().to_string_lossy().replace('\\', "/")
92
+ }
@@ -0,0 +1,75 @@
1
+ use std::collections::BTreeMap;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use serde::Serialize;
5
+
6
+ use crate::git;
7
+ use crate::models::NaomeError;
8
+
9
+ use super::config::{read_architecture_config, ArchitectureConfig};
10
+ use super::model::ArchitectureGraph;
11
+
12
+ mod graph_builder;
13
+ mod path_scan;
14
+
15
+ #[derive(Debug, Clone, Default)]
16
+ pub struct ArchitectureScanOptions {
17
+ pub config_path: Option<PathBuf>,
18
+ pub changed_only: bool,
19
+ }
20
+
21
+ #[derive(Debug, Clone, Serialize)]
22
+ #[serde(rename_all = "camelCase")]
23
+ pub struct ArchitectureScanReport {
24
+ pub schema: String,
25
+ pub graph: ArchitectureGraph,
26
+ pub files_scanned: usize,
27
+ pub changed_only_requested: bool,
28
+ pub changed_only_degraded_to_full_scan: bool,
29
+ pub changed_paths: Vec<String>,
30
+ #[serde(skip_serializing)]
31
+ pub config: ArchitectureConfig,
32
+ #[serde(skip_serializing)]
33
+ pub file_facts: BTreeMap<String, FileFact>,
34
+ }
35
+
36
+ #[derive(Debug, Clone, Serialize)]
37
+ #[serde(rename_all = "camelCase")]
38
+ pub struct FileFact {
39
+ pub path: String,
40
+ pub language: Option<String>,
41
+ pub line_count: usize,
42
+ pub layers: Vec<String>,
43
+ pub contexts: Vec<String>,
44
+ pub ignored: Option<String>,
45
+ }
46
+
47
+ pub fn scan_architecture(
48
+ root: &Path,
49
+ options: ArchitectureScanOptions,
50
+ ) -> Result<ArchitectureScanReport, NaomeError> {
51
+ let config = read_architecture_config(root, options.config_path.as_deref())?;
52
+ let changed_paths = changed_paths(root, options.changed_only)?;
53
+ let files = path_scan::repository_files(root, &config)?;
54
+ let (mut graph, file_facts) = graph_builder::build_path_graph(root, files, &config);
55
+
56
+ graph.sort_stable();
57
+ Ok(ArchitectureScanReport {
58
+ schema: "naome.arch.scan.v1".to_string(),
59
+ files_scanned: file_facts.len(),
60
+ graph,
61
+ changed_only_requested: options.changed_only,
62
+ changed_only_degraded_to_full_scan: options.changed_only,
63
+ changed_paths,
64
+ config,
65
+ file_facts,
66
+ })
67
+ }
68
+
69
+ fn changed_paths(root: &Path, changed_only: bool) -> Result<Vec<String>, NaomeError> {
70
+ if changed_only {
71
+ git::changed_paths(root)
72
+ } else {
73
+ Ok(Vec::new())
74
+ }
75
+ }
@@ -0,0 +1,31 @@
1
+ mod config;
2
+ mod model;
3
+ mod output;
4
+ mod rules;
5
+ mod scan;
6
+
7
+ use std::path::Path;
8
+
9
+ use crate::models::NaomeError;
10
+
11
+ pub use config::{
12
+ default_architecture_config_text, ArchitectureConfig, ContextConfig, LayerConfig, RuleConfig,
13
+ };
14
+ pub use model::{
15
+ ArchitectureEdge, ArchitectureEdgeKind, ArchitectureGraph, ArchitectureMetadata,
16
+ ArchitectureNode, ArchitectureNodeKind, SourceRange,
17
+ };
18
+ pub use output::{
19
+ format_architecture_explain, format_architecture_scan, format_architecture_validation,
20
+ ArchitectureAgentFeedback, ArchitectureValidation, ArchitectureViolation, Severity,
21
+ ViolationSummary,
22
+ };
23
+ pub use scan::{scan_architecture, ArchitectureScanOptions, ArchitectureScanReport};
24
+
25
+ pub fn validate_architecture(
26
+ root: &Path,
27
+ options: ArchitectureScanOptions,
28
+ ) -> Result<ArchitectureValidation, NaomeError> {
29
+ let scan = scan_architecture(root, options)?;
30
+ Ok(rules::validate_scan(scan))
31
+ }
@@ -29,12 +29,10 @@ pub(crate) fn is_integrity_hash(value: &str) -> bool {
29
29
  }
30
30
 
31
31
  pub(crate) fn native_integrity_from_naome_command(content: &str) -> Option<String> {
32
- let prefix = "const expectedNativeBinaryIntegrity = \"";
33
- let start = content.find(prefix)? + prefix.len();
34
- let rest = &content[start..];
35
- let end = rest.find("\";")?;
36
- let value = &rest[..end];
37
- is_integrity_hash(value).then(|| value.to_string())
32
+ content
33
+ .lines()
34
+ .find_map(native_integrity_assignment_value)
35
+ .map(ToString::to_string)
38
36
  }
39
37
 
40
38
  fn machine_sha256(relative_path: &str, content: &[u8]) -> String {
@@ -43,17 +41,22 @@ fn machine_sha256(relative_path: &str, content: &[u8]) -> String {
43
41
  }
44
42
 
45
43
  fn normalize_machine_owned_content(relative_path: &str, content: &[u8]) -> Vec<u8> {
44
+ let mut normalized = content.to_vec();
45
+
46
46
  if relative_path == HEALTH_CHECKER_RELATIVE_PATH
47
47
  || relative_path == TASK_STATE_CHECKER_RELATIVE_PATH
48
48
  {
49
- return replace_expected_integrity_block(content);
49
+ normalized = replace_expected_integrity_block(&normalized);
50
50
  }
51
51
 
52
- if relative_path == NAOME_COMMAND_RELATIVE_PATH {
53
- return replace_native_integrity_line(content);
52
+ if relative_path == HEALTH_CHECKER_RELATIVE_PATH
53
+ || relative_path == TASK_STATE_CHECKER_RELATIVE_PATH
54
+ || relative_path == NAOME_COMMAND_RELATIVE_PATH
55
+ {
56
+ normalized = replace_native_integrity_line(&normalized);
54
57
  }
55
58
 
56
- content.to_vec()
59
+ normalized
57
60
  }
58
61
 
59
62
  fn replace_expected_integrity_block(content: &[u8]) -> Vec<u8> {
@@ -79,18 +82,33 @@ fn replace_expected_integrity_block(content: &[u8]) -> Vec<u8> {
79
82
 
80
83
  fn replace_native_integrity_line(content: &[u8]) -> Vec<u8> {
81
84
  let text = String::from_utf8_lossy(content);
82
- let prefix = "const expectedNativeBinaryIntegrity = \"";
83
- let Some(start) = text.find(prefix) else {
84
- return content.to_vec();
85
- };
86
- let Some(relative_end) = text[start..].find(";\n") else {
87
- return content.to_vec();
88
- };
89
- let end = start + relative_end + ";\n".len();
90
-
85
+ let mut changed = false;
91
86
  let mut next = String::with_capacity(text.len());
92
- next.push_str(&text[..start]);
93
- next.push_str("const expectedNativeBinaryIntegrity = \"sha256:generated\";\n");
94
- next.push_str(&text[end..]);
95
- next.into_bytes()
87
+
88
+ for segment in text.split_inclusive('\n') {
89
+ let (line, ending) = segment
90
+ .strip_suffix('\n')
91
+ .map(|line| (line, "\n"))
92
+ .unwrap_or((segment, ""));
93
+ if native_integrity_assignment_value(line).is_some() {
94
+ next.push_str("const expectedNativeBinaryIntegrity = \"sha256:generated\";");
95
+ next.push_str(ending);
96
+ changed = true;
97
+ } else {
98
+ next.push_str(segment);
99
+ }
100
+ }
101
+
102
+ if changed {
103
+ next.into_bytes()
104
+ } else {
105
+ content.to_vec()
106
+ }
107
+ }
108
+
109
+ fn native_integrity_assignment_value(line: &str) -> Option<&str> {
110
+ let value = line
111
+ .strip_prefix("const expectedNativeBinaryIntegrity = \"")?
112
+ .strip_suffix("\";")?;
113
+ is_integrity_hash(value).then_some(value)
96
114
  }
@@ -0,0 +1,97 @@
1
+ use std::collections::HashSet;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
6
+
7
+ pub(super) fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
8
+ let Some(object) = manifest.as_object() else {
9
+ errors.push(".naome/manifest.json must be a JSON object.".to_string());
10
+ return;
11
+ };
12
+
13
+ if object.get("name").and_then(Value::as_str) != Some("naome") {
14
+ errors.push(".naome/manifest.json name must be naome.".to_string());
15
+ }
16
+
17
+ if !object
18
+ .get("harnessVersion")
19
+ .and_then(Value::as_str)
20
+ .is_some_and(is_version)
21
+ {
22
+ errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
23
+ }
24
+
25
+ if !string_array(object.get("machineOwned")).is_some() {
26
+ errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
27
+ }
28
+
29
+ if !string_array(object.get("projectOwned")).is_some() {
30
+ errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
31
+ }
32
+
33
+ if !object.get("integrity").is_some_and(Value::is_object) {
34
+ errors.push(".naome/manifest.json integrity must be an object.".to_string());
35
+ }
36
+ }
37
+
38
+ pub(super) fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
39
+ let Some(object) = manifest.as_object() else {
40
+ return;
41
+ };
42
+ let Some(machine_owned) = string_array(object.get("machineOwned")) else {
43
+ return;
44
+ };
45
+ let Some(project_owned) = string_array(object.get("projectOwned")) else {
46
+ return;
47
+ };
48
+
49
+ validate_contains_all(
50
+ &machine_owned,
51
+ MACHINE_OWNED_PATHS,
52
+ ".naome/manifest.json machineOwned",
53
+ errors,
54
+ );
55
+ validate_contains_all(
56
+ &project_owned,
57
+ PROJECT_OWNED_PATHS,
58
+ ".naome/manifest.json projectOwned",
59
+ errors,
60
+ );
61
+ }
62
+
63
+ pub(super) fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
64
+ value.and_then(Value::as_array).and_then(|entries| {
65
+ entries
66
+ .iter()
67
+ .map(|entry| {
68
+ entry
69
+ .as_str()
70
+ .filter(|text| !text.trim().is_empty())
71
+ .map(ToString::to_string)
72
+ })
73
+ .collect()
74
+ })
75
+ }
76
+
77
+ fn validate_contains_all(
78
+ actual_values: &[String],
79
+ expected_values: &[&str],
80
+ field_name: &str,
81
+ errors: &mut Vec<String>,
82
+ ) {
83
+ let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
84
+ for expected in expected_values {
85
+ if !actual.contains(expected) {
86
+ errors.push(format!("{field_name} must include {expected}."));
87
+ }
88
+ }
89
+ }
90
+
91
+ fn is_version(value: &str) -> bool {
92
+ let parts: Vec<&str> = value.split('.').collect();
93
+ parts.len() == 3
94
+ && parts
95
+ .iter()
96
+ .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
97
+ }