@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
@@ -14,6 +14,7 @@ use crate::intent::{evaluate_intent, IntentDecision};
14
14
  use crate::journal::{append_task_journal, TaskJournalEntry};
15
15
  use crate::models::{Decision, NaomeError};
16
16
  use crate::paths;
17
+ use crate::quality::{check_repository_quality, QualityMode};
17
18
  use crate::task_state::{
18
19
  completed_task_commit_paths, completed_task_harness_refresh_diff, harness_refresh_diff,
19
20
  harness_refresh_with_unrelated_diff, validate_task_state, TaskStateMode, TaskStateOptions,
@@ -881,12 +882,47 @@ fn run_quality_check(root: &Path, check_id: &str, check: &QualityCheck) -> Resul
881
882
  require_builtin_quality_check(check_id, check, "npm run check:context-budget")?;
882
883
  run_context_budget_check(root)
883
884
  }
885
+ "repository-quality-check" => {
886
+ require_builtin_quality_check_any(
887
+ check_id,
888
+ check,
889
+ &[
890
+ "naome quality check --changed",
891
+ "node .naome/bin/naome.js quality check --changed",
892
+ "npm run check:repository-quality",
893
+ ],
894
+ )?;
895
+ run_repository_quality_check(root)
896
+ }
884
897
  _ => Err(NaomeError::new(format!(
885
898
  "Quality check {check_id} is not a built-in safe check; NAOME will not execute repository-controlled verification commands."
886
899
  ))),
887
900
  }
888
901
  }
889
902
 
903
+ fn require_builtin_quality_check_any(
904
+ check_id: &str,
905
+ check: &QualityCheck,
906
+ expected_commands: &[&str],
907
+ ) -> Result<(), NaomeError> {
908
+ if check.cwd == "."
909
+ && expected_commands
910
+ .iter()
911
+ .any(|expected_command| check.command == *expected_command)
912
+ {
913
+ return Ok(());
914
+ }
915
+
916
+ Err(NaomeError::new(format!(
917
+ "Quality check {check_id} has an unsafe command or cwd; expected one of [{}] with cwd `.`.",
918
+ expected_commands
919
+ .iter()
920
+ .map(|command| format!("`{command}`"))
921
+ .collect::<Vec<_>>()
922
+ .join(", ")
923
+ )))
924
+ }
925
+
890
926
  fn require_builtin_quality_check(
891
927
  check_id: &str,
892
928
  check: &QualityCheck,
@@ -901,6 +937,32 @@ fn require_builtin_quality_check(
901
937
  )))
902
938
  }
903
939
 
940
+ fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
941
+ let report = check_repository_quality(root, QualityMode::Changed)?;
942
+ if report.ok {
943
+ return Ok(());
944
+ }
945
+
946
+ let details = report
947
+ .violations
948
+ .iter()
949
+ .take(20)
950
+ .map(|violation| {
951
+ let location = violation
952
+ .line
953
+ .map(|line| format!("{}:{line}", violation.path))
954
+ .unwrap_or_else(|| violation.path.clone());
955
+ format!("{location} {}: {}", violation.check_id, violation.message)
956
+ })
957
+ .collect::<Vec<_>>()
958
+ .join("\n");
959
+ Err(NaomeError::new(format!(
960
+ "repository-quality-check failed with {} violation(s).\n{}",
961
+ report.violations.len(),
962
+ details
963
+ )))
964
+ }
965
+
904
966
  fn run_harness_health_check(root: &Path) -> Result<(), NaomeError> {
905
967
  let errors = validate_harness_health(
906
968
  root,
@@ -0,0 +1,63 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::completion::validate_complete_task;
8
+ use super::human_review_state::validate_human_review_state;
9
+ use super::progress::{checked_status, validate_clean_git_diff};
10
+ use super::shape::{
11
+ format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
12
+ validate_idle_state,
13
+ };
14
+ pub(super) fn validate_admission(
15
+ task_state: &Value,
16
+ root: &Path,
17
+ errors: &mut Vec<String>,
18
+ ) -> Result<(), NaomeError> {
19
+ let status = checked_status(task_state, root, errors)?;
20
+ match status {
21
+ "idle" => validate_idle_state(task_state, errors),
22
+ "complete" => {
23
+ validate_active_task(task_state.get("activeTask"), errors);
24
+ validate_active_task_references(
25
+ task_state.get("activeTask"),
26
+ root,
27
+ errors,
28
+ Some(status),
29
+ )?;
30
+ validate_complete_task(
31
+ task_state.get("activeTask"),
32
+ task_state.get("blocker"),
33
+ root,
34
+ errors,
35
+ &mut Vec::new(),
36
+ )?;
37
+ }
38
+ "needs_human_review" => {
39
+ let start = errors.len();
40
+ validate_human_review_state(task_state, root, errors)?;
41
+ if errors.len() > start {
42
+ errors.push("Task admission is blocked because needs_human_review state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
43
+ } else {
44
+ errors.push(format_blocker(
45
+ "Task admission is blocked",
46
+ task_state.get("blocker"),
47
+ ));
48
+ }
49
+ }
50
+ "blocked" => {
51
+ validate_blocker(task_state.get("blocker"), errors);
52
+ errors.push(format_blocker(
53
+ "Task admission is blocked",
54
+ task_state.get("blocker"),
55
+ ));
56
+ }
57
+ other => errors.push(format!(
58
+ "Task admission is blocked because task state is {other}."
59
+ )),
60
+ }
61
+
62
+ validate_clean_git_diff(task_state, root, errors)
63
+ }
@@ -0,0 +1,72 @@
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;
8
+ use super::util::{is_iso_datetime, require_string, require_string_array_allow_empty};
9
+
10
+ pub(super) fn validate_admission_proof(
11
+ admission: Option<&Value>,
12
+ root: &Path,
13
+ errors: &mut Vec<String>,
14
+ ) -> Result<(), NaomeError> {
15
+ let Some(object) = admission.and_then(Value::as_object) else {
16
+ errors.push(
17
+ "activeTask.admission must be an object recorded from a passed admission check."
18
+ .to_string(),
19
+ );
20
+ return Ok(());
21
+ };
22
+ let prefix = "activeTask.admission";
23
+
24
+ require_string(object.get("command"), &format!("{prefix}.command"), errors);
25
+ require_string(object.get("cwd"), &format!("{prefix}.cwd"), errors);
26
+ require_string_array_allow_empty(
27
+ object.get("changedPaths"),
28
+ &format!("{prefix}.changedPaths"),
29
+ errors,
30
+ );
31
+ require_string(object.get("gitHead"), &format!("{prefix}.gitHead"), errors);
32
+
33
+ if object.get("command").and_then(Value::as_str)
34
+ != Some("node .naome/bin/check-task-state.js --admission")
35
+ {
36
+ errors.push(format!(
37
+ "{prefix}.command must be node .naome/bin/check-task-state.js --admission."
38
+ ));
39
+ }
40
+
41
+ if object.get("cwd").and_then(Value::as_str) != Some(".") {
42
+ errors.push(format!("{prefix}.cwd must be \".\"."));
43
+ }
44
+
45
+ match object.get("exitCode").and_then(Value::as_i64) {
46
+ Some(0) => {}
47
+ Some(_) => errors.push(format!("{prefix}.exitCode must be 0.")),
48
+ None => errors.push(format!("{prefix}.exitCode must be an integer.")),
49
+ }
50
+
51
+ if !object
52
+ .get("checkedAt")
53
+ .and_then(Value::as_str)
54
+ .is_some_and(is_iso_datetime)
55
+ {
56
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
57
+ }
58
+
59
+ if let Some(changed_paths) = object.get("changedPaths").and_then(Value::as_array) {
60
+ if !changed_paths.is_empty() {
61
+ errors.push(format!("{prefix}.changedPaths must be empty because task admission requires a clean git diff."));
62
+ }
63
+ }
64
+
65
+ if let Some(git_head) = object.get("gitHead").and_then(Value::as_str) {
66
+ if !git_head.trim().is_empty() && !git_commit_exists(root, git_head)? {
67
+ errors.push(format!("{prefix}.gitHead must be an existing git commit."));
68
+ }
69
+ }
70
+
71
+ Ok(())
72
+ }
@@ -0,0 +1,130 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::completion::{
8
+ validate_admission, validate_commit_gate, validate_complete_task, validate_progress,
9
+ };
10
+ use super::diff::validate_human_review_blocker_paths;
11
+ use super::proof::validate_proof_evidence_covers_changed_paths;
12
+ use super::reconcile::validate_push_gate;
13
+ use super::shape::{
14
+ format_blocker, validate_active_task, validate_active_task_references, validate_blocker,
15
+ validate_idle_state, validate_pending_upgrade, validate_task_state_shape,
16
+ };
17
+ use super::task_diff_api::validate_harness_health_gate;
18
+ use super::types::*;
19
+ use super::util::read_json;
20
+ pub fn validate_task_state(
21
+ root: &Path,
22
+ options: TaskStateOptions,
23
+ ) -> Result<TaskStateReport, NaomeError> {
24
+ let mut report = TaskStateReport {
25
+ errors: Vec::new(),
26
+ notices: Vec::new(),
27
+ };
28
+ let Some(task_state) = read_json(root, ".naome/task-state.json", &mut report.errors)? else {
29
+ return Ok(report);
30
+ };
31
+
32
+ validate_task_state_shape(&task_state, &mut report.errors);
33
+ let status = task_state
34
+ .get("status")
35
+ .and_then(Value::as_str)
36
+ .unwrap_or("invalid");
37
+ if !ALLOWED_STATUS.contains(&status) {
38
+ return Ok(report);
39
+ }
40
+
41
+ validate_harness_health_gate(root, &options, &mut report.errors)?;
42
+ if !report.errors.is_empty() {
43
+ return Ok(report);
44
+ }
45
+
46
+ if validate_requested_mode(&task_state, root, options.mode, &mut report)? {
47
+ return Ok(report);
48
+ }
49
+
50
+ if status == "idle" {
51
+ validate_idle_state(&task_state, &mut report.errors);
52
+ return Ok(report);
53
+ }
54
+
55
+ let active_error_start = report.errors.len();
56
+ validate_active_task(task_state.get("activeTask"), &mut report.errors);
57
+ validate_pending_upgrade(&task_state, root, &mut report.errors)?;
58
+ validate_active_task_references(
59
+ task_state.get("activeTask"),
60
+ root,
61
+ &mut report.errors,
62
+ Some(status),
63
+ )?;
64
+
65
+ if status == "needs_human_review" {
66
+ validate_blocker(task_state.get("blocker"), &mut report.errors);
67
+ validate_human_review_blocker_paths(
68
+ task_state.get("activeTask"),
69
+ task_state.get("blocker"),
70
+ root,
71
+ &mut report.errors,
72
+ )?;
73
+ validate_proof_evidence_covers_changed_paths(
74
+ task_state.get("activeTask"),
75
+ root,
76
+ &mut report.errors,
77
+ )?;
78
+ if report.errors.len() > active_error_start {
79
+ report.errors.push("needs_human_review task state is invalid; fix blocker paths and proof evidence before asking for a human decision.".to_string());
80
+ } else {
81
+ report.errors.push(format_blocker(
82
+ "Human review required",
83
+ task_state.get("blocker"),
84
+ ));
85
+ }
86
+ return Ok(report);
87
+ }
88
+
89
+ if status == "blocked" {
90
+ validate_blocker(task_state.get("blocker"), &mut report.errors);
91
+ report
92
+ .errors
93
+ .push(format_blocker("Task is blocked", task_state.get("blocker")));
94
+ return Ok(report);
95
+ }
96
+
97
+ if BLOCKING_STATUS.contains(&status) {
98
+ report.errors.push(format!(
99
+ "Task is still {status}; new work must wait until the active task is complete or resolved."
100
+ ));
101
+ return Ok(report);
102
+ }
103
+
104
+ validate_complete_task(
105
+ task_state.get("activeTask"),
106
+ task_state.get("blocker"),
107
+ root,
108
+ &mut report.errors,
109
+ &mut report.notices,
110
+ )?;
111
+ Ok(report)
112
+ }
113
+
114
+ fn validate_requested_mode(
115
+ task_state: &Value,
116
+ root: &Path,
117
+ mode: TaskStateMode,
118
+ report: &mut TaskStateReport,
119
+ ) -> Result<bool, NaomeError> {
120
+ match mode {
121
+ TaskStateMode::Admission => validate_admission(task_state, root, &mut report.errors)?,
122
+ TaskStateMode::Progress => validate_progress(task_state, root, &mut report.errors)?,
123
+ TaskStateMode::CommitGate => {
124
+ validate_commit_gate(task_state, root, &mut report.errors, &mut report.notices)?;
125
+ }
126
+ TaskStateMode::PushGate => validate_push_gate(task_state, &mut report.errors),
127
+ TaskStateMode::State => return Ok(false),
128
+ }
129
+ Ok(true)
130
+ }
@@ -0,0 +1,138 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use super::completion::validate_complete_task_against_entries;
6
+ use super::diff::task_diff_from_entries;
7
+ use super::git_io::read_git_staged_changed_entries;
8
+ use super::process_guard::{validate_no_active_processes, ProcessGate};
9
+ use super::progress::{validate_init_complete, validate_upgrade_complete};
10
+ use super::reconcile::{
11
+ is_deterministic_harness_refresh_diff, is_harness_repair_diff,
12
+ is_install_or_upgrade_baseline_diff,
13
+ };
14
+ use super::shape::{
15
+ read_verification_check_ids, validate_active_task, validate_active_task_references,
16
+ validate_blocker, validate_pending_upgrade, validate_required_check_ids,
17
+ };
18
+ use super::types::ChangedEntry;
19
+ use crate::models::NaomeError;
20
+ pub(super) fn validate_commit_gate(
21
+ task_state: &Value,
22
+ root: &Path,
23
+ errors: &mut Vec<String>,
24
+ notices: &mut Vec<String>,
25
+ ) -> Result<(), NaomeError> {
26
+ let staged_entries = read_git_staged_changed_entries(root)?;
27
+ let changed_paths: Vec<String> = staged_entries
28
+ .iter()
29
+ .map(|entry| entry.path.clone())
30
+ .collect();
31
+ if changed_paths.is_empty() {
32
+ return Ok(());
33
+ }
34
+
35
+ let status = task_state
36
+ .get("status")
37
+ .and_then(Value::as_str)
38
+ .unwrap_or("invalid");
39
+ if status == "complete" && is_deterministic_harness_refresh_diff(&changed_paths) {
40
+ validate_pending_upgrade(task_state, root, errors)?;
41
+ validate_completed_task_for_harness_refresh(task_state, root, &staged_entries, errors)?;
42
+ return Ok(());
43
+ }
44
+
45
+ if status == "complete" {
46
+ validate_active_task(task_state.get("activeTask"), errors);
47
+ validate_active_task_references(task_state.get("activeTask"), root, errors, Some(status))?;
48
+ if !task_state.get("blocker").is_some_and(Value::is_null) {
49
+ errors.push("complete task state must have blocker set to null.".to_string());
50
+ }
51
+ if let Some(active_task) = task_state.get("activeTask") {
52
+ let check_ids = read_verification_check_ids(root, errors)?;
53
+ validate_required_check_ids(active_task, &check_ids, errors);
54
+ validate_no_active_processes(root, errors, ProcessGate::Commit)?;
55
+ validate_complete_task_against_entries(
56
+ active_task,
57
+ root,
58
+ &check_ids,
59
+ &staged_entries,
60
+ errors,
61
+ )?;
62
+ if errors.is_empty() {
63
+ notices.push(format!(
64
+ "Commit gate accepted task-owned staged paths: {}.",
65
+ changed_paths.join(", ")
66
+ ));
67
+ }
68
+ }
69
+ return Ok(());
70
+ }
71
+
72
+ if status == "idle" && is_install_or_upgrade_baseline_diff(root, &changed_paths)? {
73
+ return Ok(());
74
+ }
75
+
76
+ if status == "idle" && is_harness_repair_diff(root, &changed_paths)? {
77
+ return Ok(());
78
+ }
79
+
80
+ validate_init_complete(root, errors)?;
81
+ validate_upgrade_complete(root, errors)?;
82
+
83
+ if status == "idle" {
84
+ errors.push(format!("NAOME commit gate blocked: changed paths are not owned by a completed task state. Changed paths: {}. Finish a NAOME task and use naome commit, or reconcile the diff before committing.", changed_paths.join(", ")));
85
+ return Ok(());
86
+ }
87
+
88
+ if status == "blocked" || status == "needs_human_review" {
89
+ validate_blocker(task_state.get("blocker"), errors);
90
+ }
91
+
92
+ errors.push(format!("NAOME commit gate blocked because task state is {status}. Finish or revise the active task, set it to complete with fresh proof, then use naome commit. Human options: continue_current_task, request_task_changes, mark_task_blocked, cancel_task_state."));
93
+ Ok(())
94
+ }
95
+
96
+ pub(super) fn validate_completed_task_for_harness_refresh(
97
+ task_state: &Value,
98
+ root: &Path,
99
+ staged_entries: &[ChangedEntry],
100
+ errors: &mut Vec<String>,
101
+ ) -> Result<(), NaomeError> {
102
+ validate_active_task(task_state.get("activeTask"), errors);
103
+ validate_active_task_references(task_state.get("activeTask"), root, errors, Some("complete"))?;
104
+ if !task_state.get("blocker").is_some_and(Value::is_null) {
105
+ errors.push("complete task state must have blocker set to null.".to_string());
106
+ }
107
+
108
+ let Some(active_task) = task_state.get("activeTask") else {
109
+ return Ok(());
110
+ };
111
+
112
+ let check_ids = read_verification_check_ids(root, errors)?;
113
+ validate_required_check_ids(active_task, &check_ids, errors);
114
+
115
+ let mut validation_errors = Vec::new();
116
+ validate_no_active_processes(root, &mut validation_errors, ProcessGate::Commit)?;
117
+ validate_complete_task_against_entries(
118
+ active_task,
119
+ root,
120
+ &check_ids,
121
+ staged_entries,
122
+ &mut validation_errors,
123
+ )?;
124
+
125
+ let staged_harness_paths = task_diff_from_entries(active_task, staged_entries).outside_paths;
126
+ let allowed_scope_error = format!(
127
+ "Changed files outside allowedPaths: {}. Human options: request_scope_change, move_changes_to_new_task, revert_out_of_scope_changes.",
128
+ staged_harness_paths.join(", ")
129
+ );
130
+
131
+ errors.extend(
132
+ validation_errors
133
+ .into_iter()
134
+ .filter(|error| error != &allowed_scope_error),
135
+ );
136
+
137
+ Ok(())
138
+ }
@@ -0,0 +1,160 @@
1
+ use std::collections::HashMap;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ use super::proof::{
9
+ validate_control_state_paths, validate_evidence_array, validate_evidence_paths,
10
+ };
11
+ use super::proof_model::{CanonicalProof, VerificationDefaults};
12
+ use super::proof_sources::read_path_sets;
13
+ use super::util::{is_iso_datetime, require_string};
14
+
15
+ pub(super) fn compact_proofs(
16
+ active_task: &Value,
17
+ root: &Path,
18
+ errors: &mut Vec<String>,
19
+ defaults: &HashMap<String, VerificationDefaults>,
20
+ ) -> Result<Vec<CanonicalProof>, NaomeError> {
21
+ let path_sets = read_path_sets(active_task, root, errors)?;
22
+ let Some(batches) = active_task.get("proofBatches") else {
23
+ return Ok(Vec::new());
24
+ };
25
+ let Some(batches) = batches.as_array() else {
26
+ errors.push("activeTask.proofBatches must be an array when present.".to_string());
27
+ return Ok(Vec::new());
28
+ };
29
+
30
+ let mut proofs = Vec::new();
31
+ for (batch_index, batch) in batches.iter().enumerate() {
32
+ let prefix = format!("activeTask.proofBatches[{batch_index}]");
33
+ let Some(batch_object) = batch.as_object() else {
34
+ errors.push(format!("{prefix} must be an object."));
35
+ continue;
36
+ };
37
+ let batch_checked_at = batch_object.get("checkedAt").and_then(Value::as_str);
38
+ if batch_checked_at.is_some_and(|value| !is_iso_datetime(value)) {
39
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
40
+ }
41
+ let batch_ref = batch_object.get("evidencePathSet").and_then(Value::as_str);
42
+ let Some(batch_proofs) = batch_object.get("proofs").and_then(Value::as_array) else {
43
+ errors.push(format!("{prefix}.proofs must be an array."));
44
+ continue;
45
+ };
46
+
47
+ for (proof_index, proof) in batch_proofs.iter().enumerate() {
48
+ let proof_prefix = format!("{prefix}.proofs[{proof_index}]");
49
+ if let Some(proof) = compact_proof(
50
+ proof,
51
+ &proof_prefix,
52
+ batch,
53
+ batch_checked_at,
54
+ batch_ref,
55
+ &path_sets,
56
+ defaults,
57
+ errors,
58
+ ) {
59
+ validate_evidence_paths(
60
+ Some(&Value::Array(proof.evidence.clone())),
61
+ &format!("{proof_prefix}.evidence"),
62
+ root,
63
+ errors,
64
+ active_task,
65
+ )?;
66
+ proofs.push(proof);
67
+ }
68
+ }
69
+ }
70
+ Ok(proofs)
71
+ }
72
+
73
+ fn compact_proof(
74
+ proof: &Value,
75
+ prefix: &str,
76
+ batch: &Value,
77
+ batch_checked_at: Option<&str>,
78
+ batch_ref: Option<&str>,
79
+ path_sets: &HashMap<String, Vec<Value>>,
80
+ defaults: &HashMap<String, VerificationDefaults>,
81
+ errors: &mut Vec<String>,
82
+ ) -> Option<CanonicalProof> {
83
+ let object = proof.as_object()?;
84
+ let check_id = object.get("checkId").and_then(Value::as_str)?;
85
+ let defaults = defaults.get(check_id);
86
+ let command = resolved_text(proof, batch, "command")
87
+ .or_else(|| defaults.map(|check| check.command.as_str()));
88
+ let cwd =
89
+ resolved_text(proof, batch, "cwd").or_else(|| defaults.map(|check| check.cwd.as_str()));
90
+ let checked_at = object
91
+ .get("checkedAt")
92
+ .and_then(Value::as_str)
93
+ .or(batch_checked_at);
94
+ let evidence = resolved_evidence(proof, batch_ref, path_sets, prefix, errors);
95
+
96
+ require_string(object.get("checkId"), &format!("{prefix}.checkId"), errors);
97
+ if command.is_none() {
98
+ errors.push(format!(
99
+ "{prefix}.command must be explicit or resolvable from .naome/verification.json."
100
+ ));
101
+ }
102
+ if cwd.is_none() {
103
+ errors.push(format!(
104
+ "{prefix}.cwd must be explicit or resolvable from .naome/verification.json."
105
+ ));
106
+ }
107
+ let Some(exit_code) = object.get("exitCode").and_then(Value::as_i64) else {
108
+ errors.push(format!("{prefix}.exitCode must be an integer."));
109
+ return None;
110
+ };
111
+ let Some(checked_at) = checked_at else {
112
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
113
+ return None;
114
+ };
115
+ if !is_iso_datetime(checked_at) {
116
+ errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
117
+ }
118
+ Some(CanonicalProof {
119
+ check_id: check_id.to_string(),
120
+ command: command?.to_string(),
121
+ cwd: cwd?.to_string(),
122
+ exit_code,
123
+ checked_at: checked_at.to_string(),
124
+ evidence: evidence?,
125
+ })
126
+ }
127
+
128
+ fn resolved_text<'a>(proof: &'a Value, batch: &'a Value, field: &str) -> Option<&'a str> {
129
+ proof
130
+ .get(field)
131
+ .and_then(Value::as_str)
132
+ .or_else(|| batch.get(field).and_then(Value::as_str))
133
+ }
134
+
135
+ fn resolved_evidence(
136
+ proof: &Value,
137
+ batch_ref: Option<&str>,
138
+ path_sets: &HashMap<String, Vec<Value>>,
139
+ prefix: &str,
140
+ errors: &mut Vec<String>,
141
+ ) -> Option<Vec<Value>> {
142
+ if let Some(evidence) = proof.get("evidence") {
143
+ validate_evidence_array(Some(evidence), &format!("{prefix}.evidence"), errors);
144
+ validate_control_state_paths(Some(evidence), &format!("{prefix}.evidence"), errors);
145
+ return evidence.as_array().cloned();
146
+ }
147
+ let path_set = proof
148
+ .get("evidencePathSet")
149
+ .and_then(Value::as_str)
150
+ .or(batch_ref);
151
+ match path_set.and_then(|name| path_sets.get(name)) {
152
+ Some(paths) => Some(paths.clone()),
153
+ None => {
154
+ errors.push(format!(
155
+ "{prefix}.evidence must be an evidence array or path-set reference."
156
+ ));
157
+ None
158
+ }
159
+ }
160
+ }