@lamentis/naome 1.1.2 → 1.2.1

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 (204) 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 +2 -1579
  6. package/bin/naome.js +68 -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 +37 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +60 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +229 -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/decision/checks.rs +64 -0
  19. package/crates/naome-core/src/decision/idle.rs +67 -0
  20. package/crates/naome-core/src/decision/json.rs +36 -0
  21. package/crates/naome-core/src/decision/states.rs +165 -0
  22. package/crates/naome-core/src/decision.rs +131 -353
  23. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  24. package/crates/naome-core/src/harness_health.rs +14 -126
  25. package/crates/naome-core/src/install_plan.rs +5 -0
  26. package/crates/naome-core/src/intent/classifier.rs +171 -0
  27. package/crates/naome-core/src/intent/envelope.rs +108 -0
  28. package/crates/naome-core/src/intent/legacy.rs +138 -0
  29. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  30. package/crates/naome-core/src/intent/model.rs +71 -0
  31. package/crates/naome-core/src/intent/patterns.rs +170 -0
  32. package/crates/naome-core/src/intent/resolver.rs +162 -0
  33. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  34. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  35. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  36. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  37. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  38. package/crates/naome-core/src/intent/risk.rs +40 -0
  39. package/crates/naome-core/src/intent/segment.rs +170 -0
  40. package/crates/naome-core/src/intent.rs +64 -879
  41. package/crates/naome-core/src/journal.rs +9 -20
  42. package/crates/naome-core/src/lib.rs +15 -0
  43. package/crates/naome-core/src/paths.rs +3 -1
  44. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  45. package/crates/naome-core/src/quality/adapters.rs +131 -0
  46. package/crates/naome-core/src/quality/baseline.rs +75 -0
  47. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  48. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  49. package/crates/naome-core/src/quality/checks.rs +228 -0
  50. package/crates/naome-core/src/quality/cleanup.rs +84 -0
  51. package/crates/naome-core/src/quality/config.rs +102 -0
  52. package/crates/naome-core/src/quality/config_support.rs +24 -0
  53. package/crates/naome-core/src/quality/mod.rs +108 -0
  54. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  55. package/crates/naome-core/src/quality/scanner.rs +379 -0
  56. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  57. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  58. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  59. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  60. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  61. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  62. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  63. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  64. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  65. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  66. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  67. package/crates/naome-core/src/quality/types.rs +292 -0
  68. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  69. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  70. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  71. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  72. package/crates/naome-core/src/route/context.rs +180 -0
  73. package/crates/naome-core/src/route/execution.rs +96 -0
  74. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  75. package/crates/naome-core/src/route/execution_support.rs +57 -0
  76. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  77. package/crates/naome-core/src/route/git_ops.rs +72 -0
  78. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  79. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  80. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  81. package/crates/naome-core/src/route/worktree.rs +75 -0
  82. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  83. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  84. package/crates/naome-core/src/route.rs +44 -1155
  85. package/crates/naome-core/src/task_state/admission.rs +63 -0
  86. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  87. package/crates/naome-core/src/task_state/api.rs +130 -0
  88. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  89. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  90. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  91. package/crates/naome-core/src/task_state/completion.rs +72 -0
  92. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  93. package/crates/naome-core/src/task_state/diff.rs +95 -0
  94. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  95. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  96. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  97. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  98. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  99. package/crates/naome-core/src/task_state/mod.rs +38 -0
  100. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  101. package/crates/naome-core/src/task_state/progress.rs +123 -0
  102. package/crates/naome-core/src/task_state/proof.rs +139 -0
  103. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  104. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  105. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  106. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  107. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  108. package/crates/naome-core/src/task_state/repair.rs +168 -0
  109. package/crates/naome-core/src/task_state/shape.rs +117 -0
  110. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  111. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  112. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  113. package/crates/naome-core/src/task_state/types.rs +87 -0
  114. package/crates/naome-core/src/task_state/util.rs +137 -0
  115. package/crates/naome-core/src/verification/render.rs +122 -0
  116. package/crates/naome-core/src/verification.rs +177 -58
  117. package/crates/naome-core/src/verification_contract.rs +49 -21
  118. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  119. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  120. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  121. package/crates/naome-core/src/workflow/mod.rs +18 -0
  122. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  123. package/crates/naome-core/src/workflow/output.rs +111 -0
  124. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  125. package/crates/naome-core/src/workflow/phases.rs +169 -0
  126. package/crates/naome-core/src/workflow/policy.rs +156 -0
  127. package/crates/naome-core/src/workflow/processes.rs +91 -0
  128. package/crates/naome-core/src/workflow/types.rs +42 -0
  129. package/crates/naome-core/tests/decision.rs +24 -118
  130. package/crates/naome-core/tests/harness_health.rs +5 -0
  131. package/crates/naome-core/tests/intent.rs +97 -792
  132. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  133. package/crates/naome-core/tests/intent_v2.rs +90 -0
  134. package/crates/naome-core/tests/quality.rs +319 -0
  135. package/crates/naome-core/tests/quality_structure.rs +116 -0
  136. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  137. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  138. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  139. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  140. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  141. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  142. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  143. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  144. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  145. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  146. package/crates/naome-core/tests/route.rs +1 -1476
  147. package/crates/naome-core/tests/route_baseline.rs +86 -0
  148. package/crates/naome-core/tests/route_completion.rs +141 -0
  149. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  150. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  151. package/crates/naome-core/tests/route_worktree.rs +54 -0
  152. package/crates/naome-core/tests/task_state.rs +60 -429
  153. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  154. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  155. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  156. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  157. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  158. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  159. package/crates/naome-core/tests/verification.rs +4 -45
  160. package/crates/naome-core/tests/verification_contract.rs +22 -78
  161. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  162. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  163. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  164. package/installer/agents.js +90 -0
  165. package/installer/context.js +67 -0
  166. package/installer/filesystem.js +166 -0
  167. package/installer/flows.js +84 -0
  168. package/installer/git-boundary.js +170 -0
  169. package/installer/git-hook-content.js +36 -0
  170. package/installer/git-hooks.js +134 -0
  171. package/installer/git-local.js +2 -0
  172. package/installer/git-shared.js +35 -0
  173. package/installer/harness-file-ops.js +140 -0
  174. package/installer/harness-files.js +56 -0
  175. package/installer/harness-verification.js +123 -0
  176. package/installer/install-plan.js +66 -0
  177. package/installer/main.js +25 -0
  178. package/installer/manifest-state.js +167 -0
  179. package/installer/native-build.js +24 -0
  180. package/installer/native-format.js +6 -0
  181. package/installer/native.js +162 -0
  182. package/installer/output.js +131 -0
  183. package/installer/version.js +32 -0
  184. package/native/darwin-arm64/naome +0 -0
  185. package/native/linux-x64/naome +0 -0
  186. package/package.json +3 -2
  187. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  188. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  189. package/templates/naome-root/.naome/bin/naome.js +51 -76
  190. package/templates/naome-root/.naome/manifest.json +22 -18
  191. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  192. package/templates/naome-root/.naome/repository-quality.json +24 -0
  193. package/templates/naome-root/.naome/repository-structure.json +90 -0
  194. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  195. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  196. package/templates/naome-root/.naome/verification.json +38 -0
  197. package/templates/naome-root/AGENTS.md +3 -0
  198. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  199. package/templates/naome-root/docs/naome/execution.md +25 -21
  200. package/templates/naome-root/docs/naome/index.md +5 -3
  201. package/templates/naome-root/docs/naome/repository-quality.md +46 -0
  202. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  203. package/templates/naome-root/docs/naome/testing.md +13 -0
  204. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -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
+ }
@@ -0,0 +1,91 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde::{Deserialize, Serialize};
5
+ use serde_json::Value;
6
+
7
+ use crate::models::NaomeError;
8
+
9
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10
+ #[serde(rename_all = "camelCase")]
11
+ pub struct ProcessReport {
12
+ pub schema: String,
13
+ pub active: Vec<TrackedProcess>,
14
+ }
15
+
16
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17
+ #[serde(rename_all = "camelCase")]
18
+ pub struct TrackedProcess {
19
+ pub pid: Option<u32>,
20
+ pub command: String,
21
+ pub cwd: String,
22
+ pub started_at: Option<String>,
23
+ pub status: String,
24
+ pub allow_after_completion: bool,
25
+ }
26
+
27
+ pub fn tracked_process_report(root: &Path) -> Result<ProcessReport, NaomeError> {
28
+ let path = root.join(".naome/processes.json");
29
+ if !path.exists() {
30
+ return Ok(ProcessReport {
31
+ schema: "naome.process-report.v1".to_string(),
32
+ active: Vec::new(),
33
+ });
34
+ }
35
+
36
+ let value: Value = serde_json::from_str(&fs::read_to_string(path)?)?;
37
+ let active = value
38
+ .get("processes")
39
+ .and_then(Value::as_array)
40
+ .into_iter()
41
+ .flatten()
42
+ .filter_map(parse_process)
43
+ .filter(is_active_blocker)
44
+ .collect();
45
+
46
+ Ok(ProcessReport {
47
+ schema: "naome.process-report.v1".to_string(),
48
+ active,
49
+ })
50
+ }
51
+
52
+ fn parse_process(value: &Value) -> Option<TrackedProcess> {
53
+ Some(TrackedProcess {
54
+ pid: value
55
+ .get("pid")
56
+ .and_then(Value::as_u64)
57
+ .map(|pid| pid as u32),
58
+ command: value.get("command")?.as_str()?.to_string(),
59
+ cwd: value
60
+ .get("cwd")
61
+ .and_then(Value::as_str)
62
+ .unwrap_or(".")
63
+ .to_string(),
64
+ started_at: value
65
+ .get("startedAt")
66
+ .and_then(Value::as_str)
67
+ .map(ToString::to_string),
68
+ status: value
69
+ .get("status")
70
+ .and_then(Value::as_str)
71
+ .unwrap_or("unknown")
72
+ .to_string(),
73
+ allow_after_completion: value
74
+ .get("allowAfterCompletion")
75
+ .and_then(Value::as_bool)
76
+ .unwrap_or(false),
77
+ })
78
+ }
79
+
80
+ fn is_active_blocker(process: &TrackedProcess) -> bool {
81
+ process.status == "running"
82
+ && !process.allow_after_completion
83
+ && match process.pid {
84
+ Some(pid) => process_may_be_running(pid),
85
+ None => true,
86
+ }
87
+ }
88
+
89
+ fn process_may_be_running(pid: u32) -> bool {
90
+ pid == std::process::id() || pid > 0
91
+ }