@lamentis/naome 1.1.1 → 1.2.0
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/Cargo.toml +1 -1
- package/LICENSE +180 -21
- package/README.md +49 -6
- package/bin/naome-node.js +44 -4
- package/bin/naome.js +54 -16
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/check_commands.rs +135 -0
- package/crates/naome-cli/src/cli_args.rs +5 -0
- package/crates/naome-cli/src/dispatcher.rs +36 -0
- package/crates/naome-cli/src/install_bridge.rs +83 -0
- package/crates/naome-cli/src/main.rs +57 -341
- package/crates/naome-cli/src/prompt_commands.rs +68 -0
- package/crates/naome-cli/src/quality_commands.rs +141 -0
- package/crates/naome-cli/src/simple_commands.rs +53 -0
- package/crates/naome-cli/src/workflow_commands.rs +153 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/harness_health/integrity.rs +96 -0
- package/crates/naome-core/src/harness_health.rs +14 -126
- package/crates/naome-core/src/install_plan.rs +3 -0
- package/crates/naome-core/src/intent/classifier.rs +171 -0
- package/crates/naome-core/src/intent/envelope.rs +108 -0
- package/crates/naome-core/src/intent/legacy.rs +138 -0
- package/crates/naome-core/src/intent/legacy_response.rs +76 -0
- package/crates/naome-core/src/intent/model.rs +71 -0
- package/crates/naome-core/src/intent/patterns.rs +170 -0
- package/crates/naome-core/src/intent/resolver.rs +162 -0
- package/crates/naome-core/src/intent/resolver_active.rs +17 -0
- package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
- package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
- package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
- package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
- package/crates/naome-core/src/intent/risk.rs +40 -0
- package/crates/naome-core/src/intent/segment.rs +170 -0
- package/crates/naome-core/src/intent.rs +64 -879
- package/crates/naome-core/src/journal.rs +9 -20
- package/crates/naome-core/src/lib.rs +13 -0
- package/crates/naome-core/src/quality/adapters.rs +178 -0
- package/crates/naome-core/src/quality/baseline.rs +75 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
- package/crates/naome-core/src/quality/checks.rs +228 -0
- package/crates/naome-core/src/quality/cleanup.rs +72 -0
- package/crates/naome-core/src/quality/config.rs +109 -0
- package/crates/naome-core/src/quality/mod.rs +90 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
- package/crates/naome-core/src/quality/scanner.rs +367 -0
- package/crates/naome-core/src/quality/types.rs +289 -0
- package/crates/naome-core/src/route.rs +292 -17
- package/crates/naome-core/src/task_state/admission.rs +63 -0
- package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
- package/crates/naome-core/src/task_state/api.rs +130 -0
- package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
- package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
- package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
- package/crates/naome-core/src/task_state/completion.rs +72 -0
- package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
- package/crates/naome-core/src/task_state/diff.rs +95 -0
- package/crates/naome-core/src/task_state/evidence.rs +154 -0
- package/crates/naome-core/src/task_state/git_io.rs +86 -0
- package/crates/naome-core/src/task_state/git_parse.rs +86 -0
- package/crates/naome-core/src/task_state/git_refs.rs +37 -0
- package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
- package/crates/naome-core/src/task_state/mod.rs +38 -0
- package/crates/naome-core/src/task_state/process_guard.rs +40 -0
- package/crates/naome-core/src/task_state/progress.rs +123 -0
- package/crates/naome-core/src/task_state/proof.rs +139 -0
- package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
- package/crates/naome-core/src/task_state/proof_model.rs +70 -0
- package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
- package/crates/naome-core/src/task_state/push_gate.rs +49 -0
- package/crates/naome-core/src/task_state/reconcile.rs +7 -0
- package/crates/naome-core/src/task_state/repair.rs +168 -0
- package/crates/naome-core/src/task_state/shape.rs +117 -0
- package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
- package/crates/naome-core/src/task_state/task_records.rs +131 -0
- package/crates/naome-core/src/task_state/task_references.rs +126 -0
- package/crates/naome-core/src/task_state/types.rs +87 -0
- package/crates/naome-core/src/task_state/util.rs +137 -0
- package/crates/naome-core/src/verification/render.rs +122 -0
- package/crates/naome-core/src/verification.rs +176 -58
- package/crates/naome-core/src/verification_contract.rs +49 -21
- package/crates/naome-core/src/workflow/integrity.rs +123 -0
- package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
- package/crates/naome-core/src/workflow/mod.rs +18 -0
- package/crates/naome-core/src/workflow/mutation.rs +68 -0
- package/crates/naome-core/src/workflow/output.rs +111 -0
- package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
- package/crates/naome-core/src/workflow/phases.rs +169 -0
- package/crates/naome-core/src/workflow/policy.rs +156 -0
- package/crates/naome-core/src/workflow/processes.rs +91 -0
- package/crates/naome-core/src/workflow/types.rs +42 -0
- package/crates/naome-core/tests/harness_health.rs +3 -0
- package/crates/naome-core/tests/intent.rs +97 -792
- package/crates/naome-core/tests/intent_support/mod.rs +133 -0
- package/crates/naome-core/tests/intent_v2.rs +90 -0
- package/crates/naome-core/tests/quality.rs +425 -0
- package/crates/naome-core/tests/route.rs +221 -4
- package/crates/naome-core/tests/task_state.rs +3 -0
- package/crates/naome-core/tests/task_state_compact.rs +110 -0
- package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
- package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
- package/crates/naome-core/tests/workflow_integrity.rs +85 -0
- package/crates/naome-core/tests/workflow_policy.rs +139 -0
- package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -2
- package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
- package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
- package/templates/naome-root/.naome/bin/naome.js +34 -63
- package/templates/naome-root/.naome/manifest.json +20 -18
- package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
- package/templates/naome-root/.naome/repository-quality.json +24 -0
- package/templates/naome-root/.naome/task-contract.schema.json +93 -11
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/.naome/verification.json +37 -0
- package/templates/naome-root/AGENTS.md +3 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
- package/templates/naome-root/docs/naome/execution.md +25 -21
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +43 -0
- package/templates/naome-root/docs/naome/testing.md +12 -0
- package/crates/naome-core/src/task_state.rs +0 -2210
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
use std::collections::BTreeMap;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::integrity_normalize::{
|
|
8
|
+
HEALTH_CHECKER_PATH, NAOME_COMMAND_PATH, TASK_STATE_CHECKER_PATH,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
#[cfg(windows)]
|
|
12
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
|
|
13
|
+
#[cfg(not(windows))]
|
|
14
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
|
|
15
|
+
|
|
16
|
+
pub(super) fn refresh_support_files(
|
|
17
|
+
root: &Path,
|
|
18
|
+
integrity: &BTreeMap<String, String>,
|
|
19
|
+
) -> Result<Vec<String>, NaomeError> {
|
|
20
|
+
let mut changed = Vec::new();
|
|
21
|
+
for path in [HEALTH_CHECKER_PATH, TASK_STATE_CHECKER_PATH] {
|
|
22
|
+
if replace_expected_integrity_block(root, path, integrity)? {
|
|
23
|
+
changed.push(path.to_string());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if replace_native_integrity(root, integrity)? {
|
|
27
|
+
changed.push(NAOME_COMMAND_PATH.to_string());
|
|
28
|
+
}
|
|
29
|
+
Ok(changed)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn replace_expected_integrity_block(
|
|
33
|
+
root: &Path,
|
|
34
|
+
relative_path: &str,
|
|
35
|
+
integrity: &BTreeMap<String, String>,
|
|
36
|
+
) -> Result<bool, NaomeError> {
|
|
37
|
+
let path = root.join(relative_path);
|
|
38
|
+
if !path.is_file() {
|
|
39
|
+
return Ok(false);
|
|
40
|
+
}
|
|
41
|
+
let original = fs::read_to_string(&path)?;
|
|
42
|
+
let Some(next) = expected_integrity_replacement(&original, integrity) else {
|
|
43
|
+
return Ok(false);
|
|
44
|
+
};
|
|
45
|
+
if next == original {
|
|
46
|
+
return Ok(false);
|
|
47
|
+
}
|
|
48
|
+
fs::write(path, next)?;
|
|
49
|
+
Ok(true)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn expected_integrity_replacement(
|
|
53
|
+
content: &str,
|
|
54
|
+
integrity: &BTreeMap<String, String>,
|
|
55
|
+
) -> Option<String> {
|
|
56
|
+
let marker = "const expectedMachineOwnedIntegrity = Object.freeze({\n";
|
|
57
|
+
let start = content.find(marker)?;
|
|
58
|
+
let after_start = start + marker.len();
|
|
59
|
+
let end = after_start + content[after_start..].find("\n});\n")? + "\n});\n".len();
|
|
60
|
+
let replacement = render_expected_integrity_block(integrity);
|
|
61
|
+
let mut next = String::with_capacity(content.len() + replacement.len());
|
|
62
|
+
next.push_str(&content[..start]);
|
|
63
|
+
next.push_str(&replacement);
|
|
64
|
+
next.push_str(&content[end..]);
|
|
65
|
+
Some(next)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn render_expected_integrity_block(integrity: &BTreeMap<String, String>) -> String {
|
|
69
|
+
let body = integrity
|
|
70
|
+
.iter()
|
|
71
|
+
.map(|(path, hash)| format!(" {path:?}: {hash:?}"))
|
|
72
|
+
.collect::<Vec<_>>()
|
|
73
|
+
.join(",\n");
|
|
74
|
+
format!("const expectedMachineOwnedIntegrity = Object.freeze({{\n{body}\n}});\n")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn replace_native_integrity(
|
|
78
|
+
root: &Path,
|
|
79
|
+
integrity: &BTreeMap<String, String>,
|
|
80
|
+
) -> Result<bool, NaomeError> {
|
|
81
|
+
let Some(native_hash) = integrity.get(NATIVE_BINARY_PATH) else {
|
|
82
|
+
return Ok(false);
|
|
83
|
+
};
|
|
84
|
+
let path = root.join(NAOME_COMMAND_PATH);
|
|
85
|
+
if !path.is_file() {
|
|
86
|
+
return Ok(false);
|
|
87
|
+
}
|
|
88
|
+
let original = fs::read_to_string(&path)?;
|
|
89
|
+
let Some(next) = native_integrity_replacement(&original, native_hash) else {
|
|
90
|
+
return Ok(false);
|
|
91
|
+
};
|
|
92
|
+
if next == original {
|
|
93
|
+
return Ok(false);
|
|
94
|
+
}
|
|
95
|
+
fs::write(path, next)?;
|
|
96
|
+
Ok(true)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn native_integrity_replacement(content: &str, native_hash: &str) -> Option<String> {
|
|
100
|
+
let prefix = "const expectedNativeBinaryIntegrity = \"";
|
|
101
|
+
let start = content.find(prefix)?;
|
|
102
|
+
let end = start + content[start..].find(";\n")? + ";\n".len();
|
|
103
|
+
let replacement = format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n");
|
|
104
|
+
Some(format!(
|
|
105
|
+
"{}{}{}",
|
|
106
|
+
&content[..start],
|
|
107
|
+
replacement,
|
|
108
|
+
&content[end..]
|
|
109
|
+
))
|
|
110
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
mod integrity;
|
|
2
|
+
mod integrity_normalize;
|
|
3
|
+
mod integrity_support;
|
|
4
|
+
mod mutation;
|
|
5
|
+
mod output;
|
|
6
|
+
mod phase_inference;
|
|
7
|
+
mod phases;
|
|
8
|
+
mod policy;
|
|
9
|
+
mod processes;
|
|
10
|
+
mod types;
|
|
11
|
+
|
|
12
|
+
pub use integrity::{refresh_integrity, IntegrityRefreshReport};
|
|
13
|
+
pub use mutation::classify_mutations;
|
|
14
|
+
pub use output::{summarize_command_output, CommandOutputSummary};
|
|
15
|
+
pub use phases::{verification_phase_plan, CommandCheckResult, VerificationPhasePlan};
|
|
16
|
+
pub use policy::{safe_rg_args, validate_read_boundaries, validate_search_command};
|
|
17
|
+
pub use processes::{tracked_process_report, ProcessReport};
|
|
18
|
+
pub use types::{MutationClassification, ReadActivity, WorkflowFinding};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::models::NaomeError;
|
|
4
|
+
|
|
5
|
+
use super::types::MutationClassification;
|
|
6
|
+
|
|
7
|
+
pub fn classify_mutations(
|
|
8
|
+
_root: &Path,
|
|
9
|
+
paths: &[String],
|
|
10
|
+
) -> Result<Vec<MutationClassification>, NaomeError> {
|
|
11
|
+
Ok(paths
|
|
12
|
+
.iter()
|
|
13
|
+
.map(|path| {
|
|
14
|
+
let normalized = path.replace('\\', "/");
|
|
15
|
+
MutationClassification {
|
|
16
|
+
mutation_class: mutation_class(&normalized).to_string(),
|
|
17
|
+
path: normalized,
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.collect())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn mutation_class(path: &str) -> &'static str {
|
|
24
|
+
if path == ".naome/manifest.json"
|
|
25
|
+
|| path == ".naome/task-contract.schema.json"
|
|
26
|
+
|| path.ends_with("/manifest.json")
|
|
27
|
+
|| path.ends_with("/generated.json")
|
|
28
|
+
{
|
|
29
|
+
return "generated refresh";
|
|
30
|
+
}
|
|
31
|
+
if path.starts_with(".naome/archive/")
|
|
32
|
+
|| path.starts_with(".git/")
|
|
33
|
+
|| path == ".git/info/exclude"
|
|
34
|
+
{
|
|
35
|
+
return "local-only repair";
|
|
36
|
+
}
|
|
37
|
+
if path.starts_with("packages/naome/templates/") || path.starts_with(".naome/bin/") {
|
|
38
|
+
return "harness template";
|
|
39
|
+
}
|
|
40
|
+
if path.ends_with(".tgz")
|
|
41
|
+
|| path.starts_with("dist/")
|
|
42
|
+
|| path.contains("/dist/")
|
|
43
|
+
|| path.starts_with("packages/naome/native/")
|
|
44
|
+
|| path.starts_with("native/")
|
|
45
|
+
{
|
|
46
|
+
return "release artifact";
|
|
47
|
+
}
|
|
48
|
+
if path.starts_with("coverage/")
|
|
49
|
+
|| path.contains("/coverage/")
|
|
50
|
+
|| path.starts_with("test-results/")
|
|
51
|
+
|| path.ends_with(".snap")
|
|
52
|
+
{
|
|
53
|
+
return "test artifact";
|
|
54
|
+
}
|
|
55
|
+
if is_source_path(path) {
|
|
56
|
+
return "source change";
|
|
57
|
+
}
|
|
58
|
+
"user edit"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn is_source_path(path: &str) -> bool {
|
|
62
|
+
[
|
|
63
|
+
".rs", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".py", ".go", ".swift", ".kt",
|
|
64
|
+
".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".rb", ".php", ".cs", ".sh",
|
|
65
|
+
]
|
|
66
|
+
.iter()
|
|
67
|
+
.any(|extension| path.ends_with(extension))
|
|
68
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
6
|
+
#[serde(rename_all = "camelCase")]
|
|
7
|
+
pub struct CommandOutputSummary {
|
|
8
|
+
pub command: String,
|
|
9
|
+
pub cwd: String,
|
|
10
|
+
pub exit_code: i32,
|
|
11
|
+
pub truncated: bool,
|
|
12
|
+
pub omitted_line_count: usize,
|
|
13
|
+
pub relevant_lines: Vec<String>,
|
|
14
|
+
pub affected_paths: Vec<String>,
|
|
15
|
+
pub artifacts: Vec<String>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn summarize_command_output(
|
|
19
|
+
command: &str,
|
|
20
|
+
cwd: &str,
|
|
21
|
+
exit_code: i32,
|
|
22
|
+
output: &str,
|
|
23
|
+
max_lines: usize,
|
|
24
|
+
) -> CommandOutputSummary {
|
|
25
|
+
let lines = output.lines().map(ToString::to_string).collect::<Vec<_>>();
|
|
26
|
+
let truncated = lines.len() > max_lines;
|
|
27
|
+
let omitted_line_count = lines.len().saturating_sub(max_lines);
|
|
28
|
+
let mut relevant = lines
|
|
29
|
+
.iter()
|
|
30
|
+
.filter(|line| is_relevant_line(line))
|
|
31
|
+
.take(max_lines)
|
|
32
|
+
.cloned()
|
|
33
|
+
.collect::<Vec<_>>();
|
|
34
|
+
|
|
35
|
+
if relevant.is_empty() {
|
|
36
|
+
let head = max_lines.saturating_sub(2).max(1);
|
|
37
|
+
relevant.extend(lines.iter().take(head).cloned());
|
|
38
|
+
if truncated {
|
|
39
|
+
relevant.extend(lines.iter().rev().take(2).cloned().rev());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
relevant.truncate(max_lines);
|
|
44
|
+
let affected_paths = collect_paths(&relevant);
|
|
45
|
+
let artifacts = affected_paths
|
|
46
|
+
.iter()
|
|
47
|
+
.filter(|path| is_artifact_path(path))
|
|
48
|
+
.cloned()
|
|
49
|
+
.collect::<Vec<_>>();
|
|
50
|
+
|
|
51
|
+
CommandOutputSummary {
|
|
52
|
+
command: command.to_string(),
|
|
53
|
+
cwd: cwd.to_string(),
|
|
54
|
+
exit_code,
|
|
55
|
+
truncated,
|
|
56
|
+
omitted_line_count,
|
|
57
|
+
relevant_lines: relevant,
|
|
58
|
+
affected_paths,
|
|
59
|
+
artifacts,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn is_relevant_line(line: &str) -> bool {
|
|
64
|
+
let lower = line.to_ascii_lowercase();
|
|
65
|
+
[
|
|
66
|
+
"error",
|
|
67
|
+
"failed",
|
|
68
|
+
"failure",
|
|
69
|
+
"panic",
|
|
70
|
+
"violation",
|
|
71
|
+
"outside allowedpaths",
|
|
72
|
+
"denied",
|
|
73
|
+
"missing",
|
|
74
|
+
]
|
|
75
|
+
.iter()
|
|
76
|
+
.any(|needle| lower.contains(needle))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn collect_paths(lines: &[String]) -> Vec<String> {
|
|
80
|
+
let mut paths = BTreeSet::new();
|
|
81
|
+
for line in lines {
|
|
82
|
+
for token in line.split_whitespace() {
|
|
83
|
+
let cleaned = token.trim_matches(|ch: char| {
|
|
84
|
+
matches!(
|
|
85
|
+
ch,
|
|
86
|
+
'"' | '\'' | '`' | ':' | ',' | ';' | '(' | ')' | '[' | ']'
|
|
87
|
+
)
|
|
88
|
+
});
|
|
89
|
+
if looks_like_path(cleaned) {
|
|
90
|
+
paths.insert(cleaned.replace('\\', "/"));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
paths.into_iter().collect()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn looks_like_path(token: &str) -> bool {
|
|
98
|
+
(token.contains('/') || token.starts_with(".naome/"))
|
|
99
|
+
&& token.contains('.')
|
|
100
|
+
&& !token.starts_with("http://")
|
|
101
|
+
&& !token.starts_with("https://")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn is_artifact_path(path: &str) -> bool {
|
|
105
|
+
path.ends_with(".log")
|
|
106
|
+
|| path.ends_with(".json")
|
|
107
|
+
|| path.ends_with(".xml")
|
|
108
|
+
|| path.ends_with(".tgz")
|
|
109
|
+
|| path.contains("target/")
|
|
110
|
+
|| path.contains("coverage/")
|
|
111
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use super::phases::{CheckDefinition, PhaseDefinition};
|
|
6
|
+
|
|
7
|
+
pub(super) fn infer_phases(
|
|
8
|
+
verification: &Value,
|
|
9
|
+
checks: &[CheckDefinition],
|
|
10
|
+
) -> Vec<PhaseDefinition> {
|
|
11
|
+
let release_gate_ids = verification
|
|
12
|
+
.get("releaseGates")
|
|
13
|
+
.and_then(Value::as_array)
|
|
14
|
+
.into_iter()
|
|
15
|
+
.flatten()
|
|
16
|
+
.filter_map(|gate| gate.get("checkId").and_then(Value::as_str))
|
|
17
|
+
.collect::<HashSet<_>>();
|
|
18
|
+
let phase_order = [
|
|
19
|
+
("shape-health", 10),
|
|
20
|
+
("quality", 20),
|
|
21
|
+
("focused-tests", 30),
|
|
22
|
+
("broad-tests", 40),
|
|
23
|
+
("package-release", 50),
|
|
24
|
+
("diff-check", 60),
|
|
25
|
+
];
|
|
26
|
+
let mut grouped = phase_order
|
|
27
|
+
.iter()
|
|
28
|
+
.map(|(id, order)| ((*id).to_string(), (*order, Vec::new())))
|
|
29
|
+
.collect::<HashMap<_, _>>();
|
|
30
|
+
|
|
31
|
+
for check in checks {
|
|
32
|
+
let phase = inferred_phase(check, &release_gate_ids);
|
|
33
|
+
if let Some((_, ids)) = grouped.get_mut(phase) {
|
|
34
|
+
ids.push(check.id.clone());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
phase_order
|
|
39
|
+
.into_iter()
|
|
40
|
+
.filter_map(|(id, _)| {
|
|
41
|
+
let (order, check_ids) = grouped.remove(id)?;
|
|
42
|
+
(!check_ids.is_empty()).then_some(PhaseDefinition {
|
|
43
|
+
id: id.to_string(),
|
|
44
|
+
order,
|
|
45
|
+
check_ids,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
.collect()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn inferred_phase<'a>(check: &'a CheckDefinition, release_gate_ids: &HashSet<&str>) -> &'a str {
|
|
52
|
+
let id = check.id.as_str();
|
|
53
|
+
let command = check.command.as_str();
|
|
54
|
+
if id == "diff-check" || command.contains("diff --check") {
|
|
55
|
+
return "diff-check";
|
|
56
|
+
}
|
|
57
|
+
if id.contains("quality") {
|
|
58
|
+
return "quality";
|
|
59
|
+
}
|
|
60
|
+
if id.contains("health") || id.contains("task-state") || command.contains("check-task-state") {
|
|
61
|
+
return "shape-health";
|
|
62
|
+
}
|
|
63
|
+
if release_gate_ids.contains(id) || id.contains("package") || command.contains("pack") {
|
|
64
|
+
return "package-release";
|
|
65
|
+
}
|
|
66
|
+
if id.contains("test") && check.cost == "fast" {
|
|
67
|
+
return "focused-tests";
|
|
68
|
+
}
|
|
69
|
+
if id.contains("test") || id.contains("build") {
|
|
70
|
+
return "broad-tests";
|
|
71
|
+
}
|
|
72
|
+
"focused-tests"
|
|
73
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
|
|
8
|
+
use crate::models::NaomeError;
|
|
9
|
+
|
|
10
|
+
use super::phase_inference::infer_phases;
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
13
|
+
#[serde(rename_all = "camelCase")]
|
|
14
|
+
pub struct CommandCheckResult {
|
|
15
|
+
pub check_id: String,
|
|
16
|
+
pub exit_code: i32,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
20
|
+
#[serde(rename_all = "camelCase")]
|
|
21
|
+
pub struct VerificationPhasePlan {
|
|
22
|
+
pub schema: String,
|
|
23
|
+
pub phases: Vec<VerificationPhaseStatus>,
|
|
24
|
+
pub recommended_check_ids: Vec<String>,
|
|
25
|
+
pub withheld_check_ids: Vec<String>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
29
|
+
#[serde(rename_all = "camelCase")]
|
|
30
|
+
pub struct VerificationPhaseStatus {
|
|
31
|
+
pub id: String,
|
|
32
|
+
pub order: u32,
|
|
33
|
+
pub check_ids: Vec<String>,
|
|
34
|
+
pub status: String,
|
|
35
|
+
pub blocked_by: Vec<String>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Debug, Clone)]
|
|
39
|
+
pub(super) struct CheckDefinition {
|
|
40
|
+
pub(super) id: String,
|
|
41
|
+
pub(super) command: String,
|
|
42
|
+
pub(super) cost: String,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone)]
|
|
46
|
+
pub(super) struct PhaseDefinition {
|
|
47
|
+
pub(super) id: String,
|
|
48
|
+
pub(super) order: u32,
|
|
49
|
+
pub(super) check_ids: Vec<String>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn verification_phase_plan(
|
|
53
|
+
root: &Path,
|
|
54
|
+
completed: &[CommandCheckResult],
|
|
55
|
+
) -> Result<VerificationPhasePlan, NaomeError> {
|
|
56
|
+
let verification = read_verification(root)?;
|
|
57
|
+
let checks = read_checks(&verification);
|
|
58
|
+
let mut phases = read_phases(&verification);
|
|
59
|
+
if phases.is_empty() {
|
|
60
|
+
phases = infer_phases(&verification, &checks);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
phases.sort_by(|left, right| left.order.cmp(&right.order).then(left.id.cmp(&right.id)));
|
|
64
|
+
let results = completed
|
|
65
|
+
.iter()
|
|
66
|
+
.map(|result| (result.check_id.as_str(), result.exit_code))
|
|
67
|
+
.collect::<HashMap<_, _>>();
|
|
68
|
+
let failed = completed
|
|
69
|
+
.iter()
|
|
70
|
+
.filter(|result| result.exit_code != 0)
|
|
71
|
+
.map(|result| result.check_id.clone())
|
|
72
|
+
.collect::<HashSet<_>>();
|
|
73
|
+
|
|
74
|
+
let mut recommended = Vec::new();
|
|
75
|
+
let mut withheld = Vec::new();
|
|
76
|
+
let mut statuses = Vec::new();
|
|
77
|
+
let mut blocking_phase = None::<String>;
|
|
78
|
+
|
|
79
|
+
for phase in phases {
|
|
80
|
+
let phase_failed = phase
|
|
81
|
+
.check_ids
|
|
82
|
+
.iter()
|
|
83
|
+
.any(|check_id| failed.contains(check_id));
|
|
84
|
+
let missing = phase
|
|
85
|
+
.check_ids
|
|
86
|
+
.iter()
|
|
87
|
+
.filter(|check_id| results.get(check_id.as_str()).copied() != Some(0))
|
|
88
|
+
.cloned()
|
|
89
|
+
.collect::<Vec<_>>();
|
|
90
|
+
let status = if let Some(blocked_by) = &blocking_phase {
|
|
91
|
+
withheld.extend(phase.check_ids.clone());
|
|
92
|
+
statuses.push(VerificationPhaseStatus {
|
|
93
|
+
id: phase.id,
|
|
94
|
+
order: phase.order,
|
|
95
|
+
check_ids: phase.check_ids,
|
|
96
|
+
status: "withheld".to_string(),
|
|
97
|
+
blocked_by: vec![blocked_by.clone()],
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
} else if phase_failed {
|
|
101
|
+
blocking_phase = Some(phase.id.clone());
|
|
102
|
+
recommended = missing;
|
|
103
|
+
"failed"
|
|
104
|
+
} else if missing.is_empty() {
|
|
105
|
+
"passed"
|
|
106
|
+
} else {
|
|
107
|
+
blocking_phase = Some(phase.id.clone());
|
|
108
|
+
recommended = missing;
|
|
109
|
+
"pending"
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
statuses.push(VerificationPhaseStatus {
|
|
113
|
+
id: phase.id,
|
|
114
|
+
order: phase.order,
|
|
115
|
+
check_ids: phase.check_ids,
|
|
116
|
+
status: status.to_string(),
|
|
117
|
+
blocked_by: Vec::new(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Ok(VerificationPhasePlan {
|
|
122
|
+
schema: "naome.verification-phase-plan.v1".to_string(),
|
|
123
|
+
phases: statuses,
|
|
124
|
+
recommended_check_ids: recommended,
|
|
125
|
+
withheld_check_ids: withheld,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn read_verification(root: &Path) -> Result<Value, NaomeError> {
|
|
130
|
+
let content = fs::read_to_string(root.join(".naome/verification.json"))?;
|
|
131
|
+
Ok(serde_json::from_str(&content)?)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn read_checks(verification: &Value) -> Vec<CheckDefinition> {
|
|
135
|
+
verification
|
|
136
|
+
.get("checks")
|
|
137
|
+
.and_then(Value::as_array)
|
|
138
|
+
.into_iter()
|
|
139
|
+
.flatten()
|
|
140
|
+
.filter_map(|check| {
|
|
141
|
+
Some(CheckDefinition {
|
|
142
|
+
id: check.get("id")?.as_str()?.to_string(),
|
|
143
|
+
command: check.get("command")?.as_str()?.to_string(),
|
|
144
|
+
cost: check.get("cost")?.as_str()?.to_string(),
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
.collect()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn read_phases(verification: &Value) -> Vec<PhaseDefinition> {
|
|
151
|
+
verification
|
|
152
|
+
.get("phases")
|
|
153
|
+
.and_then(Value::as_array)
|
|
154
|
+
.into_iter()
|
|
155
|
+
.flatten()
|
|
156
|
+
.filter_map(|phase| {
|
|
157
|
+
Some(PhaseDefinition {
|
|
158
|
+
id: phase.get("id")?.as_str()?.to_string(),
|
|
159
|
+
order: phase.get("order")?.as_u64()? as u32,
|
|
160
|
+
check_ids: phase
|
|
161
|
+
.get("checkIds")?
|
|
162
|
+
.as_array()?
|
|
163
|
+
.iter()
|
|
164
|
+
.filter_map(|check| check.as_str().map(ToString::to_string))
|
|
165
|
+
.collect(),
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
.collect()
|
|
169
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::{models::NaomeError, paths};
|
|
5
|
+
|
|
6
|
+
use super::types::{ReadActivity, WorkflowFinding};
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BOUNDARIES: &str = r#"
|
|
9
|
+
.git/**
|
|
10
|
+
.naome/archive/**
|
|
11
|
+
node_modules/**
|
|
12
|
+
**/node_modules/**
|
|
13
|
+
.npm/**
|
|
14
|
+
target/**
|
|
15
|
+
**/target/**
|
|
16
|
+
dist/**
|
|
17
|
+
**/dist/**
|
|
18
|
+
build/**
|
|
19
|
+
**/build/**
|
|
20
|
+
.cache/**
|
|
21
|
+
**/.cache/**
|
|
22
|
+
coverage/**
|
|
23
|
+
**/coverage/**
|
|
24
|
+
"#;
|
|
25
|
+
|
|
26
|
+
const REQUIRED_RG_EXCLUDES: &[&str] = &[".git/**", ".naome/archive/**", "node_modules/**"];
|
|
27
|
+
|
|
28
|
+
pub fn validate_read_boundaries(
|
|
29
|
+
root: &Path,
|
|
30
|
+
activities: &[ReadActivity],
|
|
31
|
+
) -> Result<Vec<WorkflowFinding>, NaomeError> {
|
|
32
|
+
let patterns = boundary_patterns(root);
|
|
33
|
+
let mut findings = Vec::new();
|
|
34
|
+
|
|
35
|
+
for activity in activities {
|
|
36
|
+
let denied_paths = activity
|
|
37
|
+
.paths
|
|
38
|
+
.iter()
|
|
39
|
+
.map(|path| normalize_path(path))
|
|
40
|
+
.filter(|path| paths::matches_any(path, &patterns))
|
|
41
|
+
.collect::<Vec<_>>();
|
|
42
|
+
if denied_paths.is_empty() {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
findings.push(WorkflowFinding::blocking(
|
|
47
|
+
"read-boundary",
|
|
48
|
+
"Read activity touched ignored or generated repository paths.",
|
|
49
|
+
denied_paths,
|
|
50
|
+
Some(activity.command.clone()),
|
|
51
|
+
));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Ok(findings)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn safe_rg_args(root: &Path) -> Result<Vec<String>, NaomeError> {
|
|
58
|
+
let mut args = vec!["rg".to_string(), "--hidden".to_string()];
|
|
59
|
+
for pattern in boundary_patterns(root) {
|
|
60
|
+
args.push("--glob".to_string());
|
|
61
|
+
args.push(format!("!{pattern}"));
|
|
62
|
+
}
|
|
63
|
+
Ok(args)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn validate_search_command(
|
|
67
|
+
root: &Path,
|
|
68
|
+
command: &str,
|
|
69
|
+
) -> Result<Vec<WorkflowFinding>, NaomeError> {
|
|
70
|
+
let trimmed = command.trim();
|
|
71
|
+
if !is_rg_command(trimmed) || !trimmed.contains("--hidden") {
|
|
72
|
+
return Ok(Vec::new());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let mut required = REQUIRED_RG_EXCLUDES
|
|
76
|
+
.iter()
|
|
77
|
+
.map(|pattern| (*pattern).to_string())
|
|
78
|
+
.collect::<Vec<_>>();
|
|
79
|
+
for pattern in naomeignore_patterns(root) {
|
|
80
|
+
if !required.contains(&pattern) {
|
|
81
|
+
required.push(pattern);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let missing = required
|
|
86
|
+
.into_iter()
|
|
87
|
+
.filter(|pattern| !command_has_exclude(trimmed, pattern))
|
|
88
|
+
.collect::<Vec<_>>();
|
|
89
|
+
if missing.is_empty() {
|
|
90
|
+
return Ok(Vec::new());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Ok(vec![WorkflowFinding::blocking(
|
|
94
|
+
"unsafe-search-command",
|
|
95
|
+
format!(
|
|
96
|
+
"Hidden ripgrep search is missing deterministic excludes: {}.",
|
|
97
|
+
missing.join(", ")
|
|
98
|
+
),
|
|
99
|
+
missing,
|
|
100
|
+
Some(trimmed.to_string()),
|
|
101
|
+
)])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
pub(crate) fn boundary_patterns(root: &Path) -> Vec<String> {
|
|
105
|
+
let mut patterns = listed_patterns(DEFAULT_BOUNDARIES);
|
|
106
|
+
for pattern in naomeignore_patterns(root) {
|
|
107
|
+
if !patterns.contains(&pattern) {
|
|
108
|
+
patterns.push(pattern);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
patterns
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn listed_patterns(patterns: &str) -> Vec<String> {
|
|
115
|
+
patterns
|
|
116
|
+
.lines()
|
|
117
|
+
.map(str::trim)
|
|
118
|
+
.filter(|pattern| !pattern.is_empty())
|
|
119
|
+
.map(ToString::to_string)
|
|
120
|
+
.collect()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn naomeignore_patterns(root: &Path) -> Vec<String> {
|
|
124
|
+
let Ok(content) = fs::read_to_string(root.join(".naomeignore")) else {
|
|
125
|
+
return Vec::new();
|
|
126
|
+
};
|
|
127
|
+
content
|
|
128
|
+
.lines()
|
|
129
|
+
.map(str::trim)
|
|
130
|
+
.filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
|
|
131
|
+
.map(|line| {
|
|
132
|
+
let normalized = normalize_path(line.trim_start_matches("./"));
|
|
133
|
+
if normalized.ends_with('/') {
|
|
134
|
+
format!("{normalized}**")
|
|
135
|
+
} else {
|
|
136
|
+
normalized
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
.collect()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn is_rg_command(command: &str) -> bool {
|
|
143
|
+
command == "rg" || command.starts_with("rg ") || command.ends_with("/rg")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn command_has_exclude(command: &str, pattern: &str) -> bool {
|
|
147
|
+
let excluded = format!("!{pattern}");
|
|
148
|
+
command.contains(&excluded)
|
|
149
|
+
|| command.contains(&format!("--glob={excluded}"))
|
|
150
|
+
|| command.contains(&format!("--glob '{excluded}'"))
|
|
151
|
+
|| command.contains(&format!("--glob \"{excluded}\""))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn normalize_path(path: &str) -> String {
|
|
155
|
+
path.replace('\\', "/")
|
|
156
|
+
}
|