@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.
Files changed (126) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome-node.js +44 -4
  6. package/bin/naome.js +54 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +36 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +57 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +141 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  19. package/crates/naome-core/src/harness_health.rs +14 -126
  20. package/crates/naome-core/src/install_plan.rs +3 -0
  21. package/crates/naome-core/src/intent/classifier.rs +171 -0
  22. package/crates/naome-core/src/intent/envelope.rs +108 -0
  23. package/crates/naome-core/src/intent/legacy.rs +138 -0
  24. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  25. package/crates/naome-core/src/intent/model.rs +71 -0
  26. package/crates/naome-core/src/intent/patterns.rs +170 -0
  27. package/crates/naome-core/src/intent/resolver.rs +162 -0
  28. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  29. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  30. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  31. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  32. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  33. package/crates/naome-core/src/intent/risk.rs +40 -0
  34. package/crates/naome-core/src/intent/segment.rs +170 -0
  35. package/crates/naome-core/src/intent.rs +64 -879
  36. package/crates/naome-core/src/journal.rs +9 -20
  37. package/crates/naome-core/src/lib.rs +13 -0
  38. package/crates/naome-core/src/quality/adapters.rs +178 -0
  39. package/crates/naome-core/src/quality/baseline.rs +75 -0
  40. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  41. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  42. package/crates/naome-core/src/quality/checks.rs +228 -0
  43. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  44. package/crates/naome-core/src/quality/config.rs +109 -0
  45. package/crates/naome-core/src/quality/mod.rs +90 -0
  46. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  47. package/crates/naome-core/src/quality/scanner.rs +367 -0
  48. package/crates/naome-core/src/quality/types.rs +289 -0
  49. package/crates/naome-core/src/route.rs +292 -17
  50. package/crates/naome-core/src/task_state/admission.rs +63 -0
  51. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  52. package/crates/naome-core/src/task_state/api.rs +130 -0
  53. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  54. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  55. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  56. package/crates/naome-core/src/task_state/completion.rs +72 -0
  57. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  58. package/crates/naome-core/src/task_state/diff.rs +95 -0
  59. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  60. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  62. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  63. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  64. package/crates/naome-core/src/task_state/mod.rs +38 -0
  65. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  66. package/crates/naome-core/src/task_state/progress.rs +123 -0
  67. package/crates/naome-core/src/task_state/proof.rs +139 -0
  68. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  69. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  70. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  71. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  72. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  73. package/crates/naome-core/src/task_state/repair.rs +168 -0
  74. package/crates/naome-core/src/task_state/shape.rs +117 -0
  75. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  76. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  77. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  78. package/crates/naome-core/src/task_state/types.rs +87 -0
  79. package/crates/naome-core/src/task_state/util.rs +137 -0
  80. package/crates/naome-core/src/verification/render.rs +122 -0
  81. package/crates/naome-core/src/verification.rs +176 -58
  82. package/crates/naome-core/src/verification_contract.rs +49 -21
  83. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  84. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  85. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  86. package/crates/naome-core/src/workflow/mod.rs +18 -0
  87. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  88. package/crates/naome-core/src/workflow/output.rs +111 -0
  89. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  90. package/crates/naome-core/src/workflow/phases.rs +169 -0
  91. package/crates/naome-core/src/workflow/policy.rs +156 -0
  92. package/crates/naome-core/src/workflow/processes.rs +91 -0
  93. package/crates/naome-core/src/workflow/types.rs +42 -0
  94. package/crates/naome-core/tests/harness_health.rs +3 -0
  95. package/crates/naome-core/tests/intent.rs +97 -792
  96. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  97. package/crates/naome-core/tests/intent_v2.rs +90 -0
  98. package/crates/naome-core/tests/quality.rs +425 -0
  99. package/crates/naome-core/tests/route.rs +221 -4
  100. package/crates/naome-core/tests/task_state.rs +3 -0
  101. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  102. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  103. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  104. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  105. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  106. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  107. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  108. package/native/darwin-arm64/naome +0 -0
  109. package/native/linux-x64/naome +0 -0
  110. package/package.json +2 -2
  111. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  112. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  113. package/templates/naome-root/.naome/bin/naome.js +34 -63
  114. package/templates/naome-root/.naome/manifest.json +20 -18
  115. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  116. package/templates/naome-root/.naome/repository-quality.json +24 -0
  117. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  118. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  119. package/templates/naome-root/.naome/verification.json +37 -0
  120. package/templates/naome-root/AGENTS.md +3 -0
  121. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  122. package/templates/naome-root/docs/naome/execution.md +25 -21
  123. package/templates/naome-root/docs/naome/index.md +4 -3
  124. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  125. package/templates/naome-root/docs/naome/testing.md +12 -0
  126. 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
+ }