@lamentis/naome 1.1.2 → 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 (125) 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.js +54 -16
  6. package/crates/naome-cli/Cargo.toml +1 -1
  7. package/crates/naome-cli/src/check_commands.rs +135 -0
  8. package/crates/naome-cli/src/cli_args.rs +5 -0
  9. package/crates/naome-cli/src/dispatcher.rs +36 -0
  10. package/crates/naome-cli/src/install_bridge.rs +83 -0
  11. package/crates/naome-cli/src/main.rs +57 -341
  12. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  13. package/crates/naome-cli/src/quality_commands.rs +141 -0
  14. package/crates/naome-cli/src/simple_commands.rs +53 -0
  15. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  16. package/crates/naome-core/Cargo.toml +1 -1
  17. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  18. package/crates/naome-core/src/harness_health.rs +14 -126
  19. package/crates/naome-core/src/install_plan.rs +3 -0
  20. package/crates/naome-core/src/intent/classifier.rs +171 -0
  21. package/crates/naome-core/src/intent/envelope.rs +108 -0
  22. package/crates/naome-core/src/intent/legacy.rs +138 -0
  23. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  24. package/crates/naome-core/src/intent/model.rs +71 -0
  25. package/crates/naome-core/src/intent/patterns.rs +170 -0
  26. package/crates/naome-core/src/intent/resolver.rs +162 -0
  27. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  28. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  29. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  30. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  31. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  32. package/crates/naome-core/src/intent/risk.rs +40 -0
  33. package/crates/naome-core/src/intent/segment.rs +170 -0
  34. package/crates/naome-core/src/intent.rs +64 -879
  35. package/crates/naome-core/src/journal.rs +9 -20
  36. package/crates/naome-core/src/lib.rs +13 -0
  37. package/crates/naome-core/src/quality/adapters.rs +178 -0
  38. package/crates/naome-core/src/quality/baseline.rs +75 -0
  39. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  40. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  41. package/crates/naome-core/src/quality/checks.rs +228 -0
  42. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  43. package/crates/naome-core/src/quality/config.rs +109 -0
  44. package/crates/naome-core/src/quality/mod.rs +90 -0
  45. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  46. package/crates/naome-core/src/quality/scanner.rs +367 -0
  47. package/crates/naome-core/src/quality/types.rs +289 -0
  48. package/crates/naome-core/src/route.rs +62 -0
  49. package/crates/naome-core/src/task_state/admission.rs +63 -0
  50. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  51. package/crates/naome-core/src/task_state/api.rs +130 -0
  52. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  53. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  54. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  55. package/crates/naome-core/src/task_state/completion.rs +72 -0
  56. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  57. package/crates/naome-core/src/task_state/diff.rs +95 -0
  58. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  59. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  60. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  62. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  63. package/crates/naome-core/src/task_state/mod.rs +38 -0
  64. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  65. package/crates/naome-core/src/task_state/progress.rs +123 -0
  66. package/crates/naome-core/src/task_state/proof.rs +139 -0
  67. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  68. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  69. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  70. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  71. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  72. package/crates/naome-core/src/task_state/repair.rs +168 -0
  73. package/crates/naome-core/src/task_state/shape.rs +117 -0
  74. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  75. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  76. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  77. package/crates/naome-core/src/task_state/types.rs +87 -0
  78. package/crates/naome-core/src/task_state/util.rs +137 -0
  79. package/crates/naome-core/src/verification/render.rs +122 -0
  80. package/crates/naome-core/src/verification.rs +176 -58
  81. package/crates/naome-core/src/verification_contract.rs +49 -21
  82. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  83. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  84. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  85. package/crates/naome-core/src/workflow/mod.rs +18 -0
  86. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  87. package/crates/naome-core/src/workflow/output.rs +111 -0
  88. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  89. package/crates/naome-core/src/workflow/phases.rs +169 -0
  90. package/crates/naome-core/src/workflow/policy.rs +156 -0
  91. package/crates/naome-core/src/workflow/processes.rs +91 -0
  92. package/crates/naome-core/src/workflow/types.rs +42 -0
  93. package/crates/naome-core/tests/harness_health.rs +3 -0
  94. package/crates/naome-core/tests/intent.rs +97 -792
  95. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  96. package/crates/naome-core/tests/intent_v2.rs +90 -0
  97. package/crates/naome-core/tests/quality.rs +425 -0
  98. package/crates/naome-core/tests/route.rs +88 -188
  99. package/crates/naome-core/tests/task_state.rs +3 -0
  100. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  101. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  102. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  103. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  104. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  105. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  106. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  107. package/native/darwin-arm64/naome +0 -0
  108. package/native/linux-x64/naome +0 -0
  109. package/package.json +2 -2
  110. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  111. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  112. package/templates/naome-root/.naome/bin/naome.js +34 -63
  113. package/templates/naome-root/.naome/manifest.json +20 -18
  114. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  115. package/templates/naome-root/.naome/repository-quality.json +24 -0
  116. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  117. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  118. package/templates/naome-root/.naome/verification.json +37 -0
  119. package/templates/naome-root/AGENTS.md +3 -0
  120. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  121. package/templates/naome-root/docs/naome/execution.md +25 -21
  122. package/templates/naome-root/docs/naome/index.md +4 -3
  123. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  124. package/templates/naome-root/docs/naome/testing.md +12 -0
  125. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -0,0 +1,89 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::api::validate_task_state;
8
+ use super::git_io::read_git_changed_paths;
9
+ use super::repair::is_safe_harness_refresh_path;
10
+ use super::types::{
11
+ CompletedTaskHarnessRefreshDiff, TaskStateMode, TaskStateOptions, CONTROL_STATE_PATH,
12
+ };
13
+ use super::util::{matches_any_pattern, read_json, string_array};
14
+ pub(super) fn add_completed_task_diff_notice(
15
+ root: &Path,
16
+ notices: &mut Vec<String>,
17
+ ) -> Result<(), NaomeError> {
18
+ let changed_paths = read_git_changed_paths(root)?;
19
+ if changed_paths.is_empty() {
20
+ return Ok(());
21
+ }
22
+
23
+ notices.push(format!("Task is complete and verified. Changed paths: {}. NAOME intent can baseline it automatically before the next distinct task; only surface human choices when intent blocks or the user explicitly asks to review, revise, cancel, or commit.", changed_paths.join(", ")));
24
+ Ok(())
25
+ }
26
+
27
+ pub fn completed_task_harness_refresh_diff(
28
+ root: &Path,
29
+ ) -> Result<Option<CompletedTaskHarnessRefreshDiff>, NaomeError> {
30
+ let mut read_errors = Vec::new();
31
+ let Some(task_state) = read_json(root, ".naome/task-state.json", &mut read_errors)? else {
32
+ return Ok(None);
33
+ };
34
+ if !read_errors.is_empty() {
35
+ return Ok(None);
36
+ }
37
+ if task_state.get("status").and_then(Value::as_str) != Some("complete") {
38
+ return Ok(None);
39
+ }
40
+ let Some(active_task) = task_state.get("activeTask") else {
41
+ return Ok(None);
42
+ };
43
+
44
+ let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
45
+ let mut harness_paths = Vec::new();
46
+ let mut task_paths = Vec::new();
47
+ let mut other_paths = Vec::new();
48
+
49
+ for path in read_git_changed_paths(root)? {
50
+ if path == CONTROL_STATE_PATH {
51
+ continue;
52
+ }
53
+ if matches_any_pattern(&path, &allowed_paths) {
54
+ task_paths.push(path);
55
+ } else if is_safe_harness_refresh_path(&path) {
56
+ harness_paths.push(path);
57
+ } else {
58
+ other_paths.push(path);
59
+ }
60
+ }
61
+
62
+ if task_paths.is_empty() || harness_paths.is_empty() || !other_paths.is_empty() {
63
+ return Ok(None);
64
+ }
65
+
66
+ let report = validate_task_state(
67
+ root,
68
+ TaskStateOptions {
69
+ mode: TaskStateMode::State,
70
+ harness_health: None,
71
+ },
72
+ )?;
73
+ let allowed_scope_error = format!(
74
+ "Changed files outside allowedPaths: {}. Human options: request_scope_change, move_changes_to_new_task, revert_out_of_scope_changes.",
75
+ harness_paths.join(", ")
76
+ );
77
+ if report
78
+ .errors
79
+ .iter()
80
+ .all(|error| error == &allowed_scope_error)
81
+ {
82
+ Ok(Some(CompletedTaskHarnessRefreshDiff {
83
+ harness_paths,
84
+ task_paths,
85
+ }))
86
+ } else {
87
+ Ok(None)
88
+ }
89
+ }
@@ -0,0 +1,72 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ pub(super) use super::admission::validate_admission;
7
+ pub(super) use super::commit_gate::validate_commit_gate;
8
+ use super::diff::validate_changed_entries;
9
+ use super::git_io::read_git_changed_entries;
10
+ use super::process_guard::{validate_no_active_processes, ProcessGate};
11
+ pub(super) use super::progress::validate_progress;
12
+ use super::proof::{validate_proof_evidence_covers_changed_entries, validate_proof_results};
13
+ use super::reconcile::add_completed_task_diff_notice;
14
+ use super::shape::{read_verification_check_ids, validate_required_check_ids};
15
+ use super::types::ChangedEntry;
16
+ use crate::models::NaomeError;
17
+ pub(super) fn validate_complete_task(
18
+ active_task: Option<&Value>,
19
+ blocker: Option<&Value>,
20
+ root: &Path,
21
+ errors: &mut Vec<String>,
22
+ notices: &mut Vec<String>,
23
+ ) -> Result<(), NaomeError> {
24
+ let error_start = errors.len();
25
+
26
+ if !blocker.is_some_and(Value::is_null) {
27
+ errors.push("complete task state must have blocker set to null.".to_string());
28
+ }
29
+
30
+ let Some(active_task) = active_task else {
31
+ return Ok(());
32
+ };
33
+
34
+ if active_task
35
+ .get("humanReview")
36
+ .and_then(|review| review.get("required"))
37
+ .and_then(Value::as_bool)
38
+ == Some(true)
39
+ && active_task
40
+ .get("humanReview")
41
+ .and_then(|review| review.get("approved"))
42
+ .and_then(Value::as_bool)
43
+ != Some(true)
44
+ {
45
+ errors.push("complete task requires human review approval before completion.".to_string());
46
+ }
47
+
48
+ let check_ids = read_verification_check_ids(root, errors)?;
49
+ validate_required_check_ids(active_task, &check_ids, errors);
50
+ validate_no_active_processes(root, errors, ProcessGate::Completion)?;
51
+ let entries = read_git_changed_entries(root)?;
52
+ validate_complete_task_against_entries(active_task, root, &check_ids, &entries, errors)?;
53
+
54
+ if errors.len() == error_start {
55
+ add_completed_task_diff_notice(root, notices)?;
56
+ }
57
+
58
+ Ok(())
59
+ }
60
+
61
+ pub(super) fn validate_complete_task_against_entries(
62
+ active_task: &Value,
63
+ root: &Path,
64
+ check_ids: &HashSet<String>,
65
+ entries: &[ChangedEntry],
66
+ errors: &mut Vec<String>,
67
+ ) -> Result<(), NaomeError> {
68
+ validate_proof_results(active_task, check_ids, root, errors)?;
69
+ validate_changed_entries(active_task, entries, errors)?;
70
+ validate_proof_evidence_covers_changed_entries(active_task, root, entries, errors)?;
71
+ Ok(())
72
+ }
@@ -0,0 +1,47 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::git_io::{git_commit_exists, parse_name_status_output, read_git_head, run_git};
8
+
9
+ pub(super) fn read_historical_deleted_paths(
10
+ active_task: &Value,
11
+ root: &Path,
12
+ ) -> Result<Vec<String>, NaomeError> {
13
+ let Some(admission_head) = active_task
14
+ .get("admission")
15
+ .and_then(|admission| admission.get("gitHead"))
16
+ .and_then(Value::as_str)
17
+ .filter(|head| !head.trim().is_empty())
18
+ else {
19
+ return Ok(Vec::new());
20
+ };
21
+
22
+ if !git_commit_exists(root, admission_head)? {
23
+ return Ok(Vec::new());
24
+ }
25
+
26
+ let Some(current_head) = read_git_head(root)? else {
27
+ return Ok(Vec::new());
28
+ };
29
+
30
+ if current_head == admission_head {
31
+ return Ok(Vec::new());
32
+ }
33
+
34
+ let output = run_git(
35
+ root,
36
+ ["diff", "--name-status", "-z", admission_head, &current_head],
37
+ )?;
38
+ if !output.status.success() {
39
+ return Ok(Vec::new());
40
+ }
41
+
42
+ Ok(parse_name_status_output(&output.stdout)
43
+ .into_iter()
44
+ .filter(|entry| entry.status == "deleted")
45
+ .map(|entry| entry.path)
46
+ .collect())
47
+ }
@@ -0,0 +1,95 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::git_io::read_git_changed_entries;
9
+ use super::types::{ChangedEntry, TaskDiff, CONTROL_STATE_PATH};
10
+ use super::util::{matches_any_pattern, normalize_path, string_array};
11
+ pub(super) fn validate_changed_paths(
12
+ active_task: &Value,
13
+ root: &Path,
14
+ errors: &mut Vec<String>,
15
+ ) -> Result<(), NaomeError> {
16
+ let entries = read_git_changed_entries(root)?;
17
+ validate_changed_entries(active_task, &entries, errors)
18
+ }
19
+
20
+ pub(super) fn validate_changed_entries(
21
+ active_task: &Value,
22
+ entries: &[ChangedEntry],
23
+ errors: &mut Vec<String>,
24
+ ) -> Result<(), NaomeError> {
25
+ let diff = task_diff_from_entries(active_task, entries);
26
+ if !diff.outside_paths.is_empty() {
27
+ errors.push(format!(
28
+ "Changed files outside allowedPaths: {}. Human options: request_scope_change, move_changes_to_new_task, revert_out_of_scope_changes.",
29
+ diff.outside_paths.join(", ")
30
+ ));
31
+ }
32
+ Ok(())
33
+ }
34
+
35
+ pub(super) fn validate_human_review_blocker_paths(
36
+ active_task: Option<&Value>,
37
+ blocker: Option<&Value>,
38
+ root: &Path,
39
+ errors: &mut Vec<String>,
40
+ ) -> Result<(), NaomeError> {
41
+ let (Some(active_task), Some(blocker)) = (active_task, blocker) else {
42
+ return Ok(());
43
+ };
44
+ let diff = read_task_diff(active_task, root)?;
45
+ if diff.outside_paths.is_empty() {
46
+ return Ok(());
47
+ }
48
+
49
+ let blocker_paths: HashSet<String> = blocker
50
+ .get("paths")
51
+ .and_then(Value::as_array)
52
+ .into_iter()
53
+ .flatten()
54
+ .filter_map(Value::as_str)
55
+ .map(normalize_path)
56
+ .collect();
57
+ let missing_paths: Vec<String> = diff
58
+ .outside_paths
59
+ .into_iter()
60
+ .filter(|path| !blocker_paths.contains(path))
61
+ .collect();
62
+
63
+ if !missing_paths.is_empty() {
64
+ errors.push(format!(
65
+ "blocker.paths missing actual scope violations: {}",
66
+ missing_paths.join(", ")
67
+ ));
68
+ }
69
+
70
+ Ok(())
71
+ }
72
+
73
+ pub(super) fn read_task_diff(active_task: &Value, root: &Path) -> Result<TaskDiff, NaomeError> {
74
+ let entries = read_git_changed_entries(root)?;
75
+ Ok(task_diff_from_entries(active_task, &entries))
76
+ }
77
+
78
+ pub(super) fn task_diff_from_entries(active_task: &Value, entries: &[ChangedEntry]) -> TaskDiff {
79
+ let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
80
+ let diff_paths: Vec<String> = entries
81
+ .iter()
82
+ .map(|entry| entry.path.clone())
83
+ .filter(|path| path != CONTROL_STATE_PATH)
84
+ .collect();
85
+ let outside_paths = diff_paths
86
+ .iter()
87
+ .filter(|path| !matches_any_pattern(path, &allowed_paths))
88
+ .cloned()
89
+ .collect();
90
+
91
+ TaskDiff {
92
+ diff_paths,
93
+ outside_paths,
94
+ }
95
+ }
@@ -0,0 +1,154 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::deleted_paths::read_historical_deleted_paths;
9
+ use super::git_io::read_git_changed_entries;
10
+ use super::types::{ALLOWED_EVIDENCE_STATUS, CONTROL_STATE_PATH};
11
+ use super::util::{
12
+ is_non_empty_string, matches_any_pattern, normalize_path, require_string, string_array,
13
+ };
14
+ pub(super) fn validate_evidence_array(
15
+ evidence: Option<&Value>,
16
+ field_name: &str,
17
+ errors: &mut Vec<String>,
18
+ ) {
19
+ let Some(evidence) = evidence.and_then(Value::as_array) else {
20
+ errors.push(format!("{field_name} must be an evidence array."));
21
+ return;
22
+ };
23
+
24
+ for (index, entry) in evidence.iter().enumerate() {
25
+ let prefix = format!("{field_name}[{index}]");
26
+ if entry.as_str().is_some_and(is_non_empty_string) {
27
+ continue;
28
+ }
29
+
30
+ let Some(object) = entry.as_object() else {
31
+ errors.push(format!(
32
+ "{prefix} must be a non-empty string path or an evidence object."
33
+ ));
34
+ continue;
35
+ };
36
+
37
+ require_string(object.get("path"), &format!("{prefix}.path"), errors);
38
+
39
+ if let Some(status) = object.get("status").and_then(Value::as_str) {
40
+ if !ALLOWED_EVIDENCE_STATUS.contains(&status) {
41
+ errors.push(format!(
42
+ "{prefix}.status must be one of: {}.",
43
+ ALLOWED_EVIDENCE_STATUS.join(", ")
44
+ ));
45
+ }
46
+ }
47
+
48
+ if object.contains_key("fromPath")
49
+ && !object
50
+ .get("fromPath")
51
+ .and_then(Value::as_str)
52
+ .is_some_and(is_non_empty_string)
53
+ {
54
+ errors.push(format!(
55
+ "{prefix}.fromPath must be a non-empty string when present."
56
+ ));
57
+ }
58
+ }
59
+ }
60
+
61
+ pub(super) fn validate_control_state_patterns(
62
+ patterns: Option<&Value>,
63
+ field_name: &str,
64
+ errors: &mut Vec<String>,
65
+ ) {
66
+ let Some(patterns) = string_array(patterns) else {
67
+ return;
68
+ };
69
+
70
+ for pattern in patterns {
71
+ if matches_any_pattern(CONTROL_STATE_PATH, std::slice::from_ref(&pattern)) {
72
+ errors.push(format!(
73
+ "{field_name} cannot include NAOME control state: {pattern}"
74
+ ));
75
+ }
76
+ }
77
+ }
78
+
79
+ pub(super) fn validate_control_state_paths(
80
+ paths: Option<&Value>,
81
+ field_name: &str,
82
+ errors: &mut Vec<String>,
83
+ ) {
84
+ let Some(paths) = paths.and_then(Value::as_array) else {
85
+ return;
86
+ };
87
+
88
+ for entry in paths {
89
+ let Some(path) = evidence_entry_path(entry) else {
90
+ continue;
91
+ };
92
+ if normalize_path(&path) == CONTROL_STATE_PATH {
93
+ errors.push(format!(
94
+ "{field_name} cannot include NAOME control state: {path}"
95
+ ));
96
+ }
97
+ }
98
+ }
99
+
100
+ pub(super) fn validate_evidence_paths(
101
+ evidence: Option<&Value>,
102
+ field_name: &str,
103
+ root: &Path,
104
+ errors: &mut Vec<String>,
105
+ active_task: &Value,
106
+ ) -> Result<(), NaomeError> {
107
+ let Some(evidence) = evidence.and_then(Value::as_array) else {
108
+ return Ok(());
109
+ };
110
+
111
+ let mut deleted_paths: HashSet<String> = read_git_changed_entries(root)?
112
+ .into_iter()
113
+ .filter(|entry| entry.status == "deleted")
114
+ .map(|entry| entry.path)
115
+ .collect();
116
+ for path in read_historical_deleted_paths(active_task, root)? {
117
+ deleted_paths.insert(path);
118
+ }
119
+
120
+ for entry in evidence {
121
+ let Some(evidence_path) = evidence_entry_path(entry) else {
122
+ continue;
123
+ };
124
+ let normalized_path = normalize_path(&evidence_path);
125
+ if Path::new(&evidence_path).is_absolute()
126
+ || normalized_path.split('/').any(|part| part == "..")
127
+ {
128
+ errors.push(format!("{field_name} unsafe path: {evidence_path}"));
129
+ continue;
130
+ }
131
+
132
+ if !root.join(&normalized_path).exists() && !deleted_paths.contains(&normalized_path) {
133
+ errors.push(format!(
134
+ "{field_name} path does not exist or is not deleted in git diff: {evidence_path}"
135
+ ));
136
+ }
137
+ }
138
+
139
+ Ok(())
140
+ }
141
+
142
+ pub(super) fn evidence_entry_path(entry: &Value) -> Option<String> {
143
+ entry
144
+ .as_str()
145
+ .filter(|value| is_non_empty_string(value))
146
+ .map(ToString::to_string)
147
+ .or_else(|| {
148
+ entry
149
+ .get("path")
150
+ .and_then(Value::as_str)
151
+ .filter(|value| is_non_empty_string(value))
152
+ .map(ToString::to_string)
153
+ })
154
+ }
@@ -0,0 +1,86 @@
1
+ use std::collections::HashMap;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ pub(super) use super::git_parse::{parse_name_status_output, split_nul, upsert_changed_entry};
8
+ pub(super) use super::git_refs::{command_output, git_commit_exists, read_git_head, run_git};
9
+ use super::types::ChangedEntry;
10
+ use super::util::normalize_path;
11
+ pub(super) fn read_git_changed_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
12
+ Ok(read_git_changed_entries(root)?
13
+ .into_iter()
14
+ .map(|entry| entry.path)
15
+ .collect())
16
+ }
17
+
18
+ pub(super) fn read_git_staged_changed_entries(
19
+ root: &Path,
20
+ ) -> Result<Vec<ChangedEntry>, NaomeError> {
21
+ let output = Command::new("git")
22
+ .args(["diff", "--name-status", "--cached", "-z"])
23
+ .current_dir(root)
24
+ .output()?;
25
+ if !output.status.success() {
26
+ return Err(NaomeError::new(format!(
27
+ "git diff --name-status --cached -z failed: {}",
28
+ command_output(&output)
29
+ )));
30
+ }
31
+
32
+ Ok(parse_name_status_output(&output.stdout))
33
+ }
34
+
35
+ pub(super) fn read_git_changed_entries(root: &Path) -> Result<Vec<ChangedEntry>, NaomeError> {
36
+ let git_check = run_git(root, ["rev-parse", "--is-inside-work-tree"])?;
37
+ if !git_check.status.success() {
38
+ return Err(NaomeError::new(
39
+ "complete task validation requires a git work tree.",
40
+ ));
41
+ }
42
+
43
+ let mut entries: HashMap<String, ChangedEntry> = HashMap::new();
44
+ for args in [
45
+ vec!["diff", "--name-status", "-z"],
46
+ vec!["diff", "--name-status", "--cached", "-z"],
47
+ ] {
48
+ let output = Command::new("git").args(&args).current_dir(root).output()?;
49
+ if !output.status.success() {
50
+ return Err(NaomeError::new(format!(
51
+ "git {} failed: {}",
52
+ args.join(" "),
53
+ command_output(&output)
54
+ )));
55
+ }
56
+
57
+ for entry in parse_name_status_output(&output.stdout) {
58
+ upsert_changed_entry(&mut entries, entry);
59
+ }
60
+ }
61
+
62
+ let untracked = run_git(root, ["ls-files", "--others", "--exclude-standard", "-z"])?;
63
+ if !untracked.status.success() {
64
+ return Err(NaomeError::new(format!(
65
+ "git ls-files --others --exclude-standard -z failed: {}",
66
+ command_output(&untracked)
67
+ )));
68
+ }
69
+
70
+ for token in split_nul(&untracked.stdout) {
71
+ let path = normalize_path(token.trim());
72
+ if !path.is_empty() {
73
+ upsert_changed_entry(
74
+ &mut entries,
75
+ ChangedEntry {
76
+ path,
77
+ status: "added".to_string(),
78
+ },
79
+ );
80
+ }
81
+ }
82
+
83
+ let mut entries: Vec<ChangedEntry> = entries.into_values().collect();
84
+ entries.sort_by(|left, right| left.path.cmp(&right.path));
85
+ Ok(entries)
86
+ }
@@ -0,0 +1,86 @@
1
+ use std::collections::HashMap;
2
+
3
+ use super::types::ChangedEntry;
4
+ use super::util::normalize_path;
5
+ pub(super) fn parse_name_status_output(output: &[u8]) -> Vec<ChangedEntry> {
6
+ let tokens = split_nul(output);
7
+ let mut entries = Vec::new();
8
+ let mut index = 0;
9
+
10
+ while index < tokens.len() {
11
+ let raw_status = &tokens[index];
12
+ index += 1;
13
+ let status_code = raw_status.chars().next().unwrap_or('M');
14
+
15
+ if status_code == 'R' || status_code == 'C' {
16
+ let from_path = normalize_path(tokens.get(index).map(String::as_str).unwrap_or(""));
17
+ index += 1;
18
+ let to_path = normalize_path(tokens.get(index).map(String::as_str).unwrap_or(""));
19
+ index += 1;
20
+ if !from_path.is_empty() {
21
+ entries.push(ChangedEntry {
22
+ path: from_path,
23
+ status: "deleted".to_string(),
24
+ });
25
+ }
26
+ if !to_path.is_empty() {
27
+ entries.push(ChangedEntry {
28
+ path: to_path,
29
+ status: "renamed".to_string(),
30
+ });
31
+ }
32
+ continue;
33
+ }
34
+
35
+ let path = normalize_path(tokens.get(index).map(String::as_str).unwrap_or(""));
36
+ index += 1;
37
+ if path.is_empty() {
38
+ continue;
39
+ }
40
+
41
+ entries.push(ChangedEntry {
42
+ path,
43
+ status: git_status_code_to_evidence_status(status_code).to_string(),
44
+ });
45
+ }
46
+
47
+ entries
48
+ }
49
+
50
+ pub(super) fn split_nul(output: &[u8]) -> Vec<String> {
51
+ output
52
+ .split(|byte| *byte == 0)
53
+ .filter(|token| !token.is_empty())
54
+ .map(|token| String::from_utf8_lossy(token).to_string())
55
+ .collect()
56
+ }
57
+
58
+ pub(super) fn git_status_code_to_evidence_status(status_code: char) -> &'static str {
59
+ match status_code {
60
+ 'A' => "added",
61
+ 'D' => "deleted",
62
+ _ => "modified",
63
+ }
64
+ }
65
+
66
+ pub(super) fn upsert_changed_entry(
67
+ entries: &mut HashMap<String, ChangedEntry>,
68
+ entry: ChangedEntry,
69
+ ) {
70
+ let should_replace = entries
71
+ .get(&entry.path)
72
+ .map(|existing| status_rank(&entry.status) > status_rank(&existing.status))
73
+ .unwrap_or(true);
74
+ if should_replace {
75
+ entries.insert(entry.path.clone(), entry);
76
+ }
77
+ }
78
+
79
+ pub(super) fn status_rank(status: &str) -> u8 {
80
+ match status {
81
+ "deleted" => 4,
82
+ "renamed" => 3,
83
+ "added" => 2,
84
+ _ => 1,
85
+ }
86
+ }
@@ -0,0 +1,37 @@
1
+ use std::path::Path;
2
+ use std::process::Command;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ pub(super) fn read_git_head(root: &Path) -> Result<Option<String>, NaomeError> {
7
+ let output = run_git(root, ["rev-parse", "HEAD"])?;
8
+ if !output.status.success() {
9
+ return Ok(None);
10
+ }
11
+ Ok(Some(
12
+ String::from_utf8_lossy(&output.stdout).trim().to_string(),
13
+ ))
14
+ }
15
+
16
+ pub(super) fn git_commit_exists(root: &Path, commit: &str) -> Result<bool, NaomeError> {
17
+ Ok(
18
+ run_git(root, ["cat-file", "-e", &format!("{commit}^{{commit}}")])?
19
+ .status
20
+ .success(),
21
+ )
22
+ }
23
+
24
+ pub(super) fn run_git<const N: usize>(
25
+ root: &Path,
26
+ args: [&str; N],
27
+ ) -> Result<std::process::Output, NaomeError> {
28
+ Ok(Command::new("git").args(args).current_dir(root).output()?)
29
+ }
30
+
31
+ pub(super) fn command_output(output: &std::process::Output) -> String {
32
+ let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
33
+ if !stderr.is_empty() {
34
+ return stderr;
35
+ }
36
+ String::from_utf8_lossy(&output.stdout).trim().to_string()
37
+ }
@@ -0,0 +1,31 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::diff::validate_human_review_blocker_paths;
8
+ use super::proof::validate_proof_evidence_covers_changed_paths;
9
+ use super::shape::{validate_active_task, validate_active_task_references, validate_blocker};
10
+
11
+ pub(super) fn validate_human_review_state(
12
+ task_state: &Value,
13
+ root: &Path,
14
+ errors: &mut Vec<String>,
15
+ ) -> Result<(), NaomeError> {
16
+ validate_active_task(task_state.get("activeTask"), errors);
17
+ validate_active_task_references(
18
+ task_state.get("activeTask"),
19
+ root,
20
+ errors,
21
+ Some("needs_human_review"),
22
+ )?;
23
+ validate_blocker(task_state.get("blocker"), errors);
24
+ validate_human_review_blocker_paths(
25
+ task_state.get("activeTask"),
26
+ task_state.get("blocker"),
27
+ root,
28
+ errors,
29
+ )?;
30
+ validate_proof_evidence_covers_changed_paths(task_state.get("activeTask"), root, errors)
31
+ }