@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
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
49
|
+
normalized = replace_expected_integrity_block(&normalized);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
if relative_path ==
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|