@lamentis/naome 1.4.0 → 1.4.2

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 (64) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +17 -122
  3. package/crates/naome-cli/Cargo.toml +1 -1
  4. package/crates/naome-cli/src/main.rs +13 -0
  5. package/crates/naome-cli/src/task_commands/can_edit.rs +116 -0
  6. package/crates/naome-cli/src/task_commands/check_run/output.rs +34 -0
  7. package/crates/naome-cli/src/task_commands/check_run/receipts.rs +155 -0
  8. package/crates/naome-cli/src/task_commands/check_run/verification.rs +165 -0
  9. package/crates/naome-cli/src/task_commands/check_run.rs +192 -0
  10. package/crates/naome-cli/src/task_commands/common.rs +70 -0
  11. package/crates/naome-cli/src/task_commands/complete.rs +43 -0
  12. package/crates/naome-cli/src/task_commands/loop_control.rs +55 -0
  13. package/crates/naome-cli/src/task_commands/readiness.rs +44 -0
  14. package/crates/naome-cli/src/task_commands/record.rs +236 -0
  15. package/crates/naome-cli/src/task_commands/repair.rs +77 -0
  16. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  17. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  18. package/crates/naome-cli/src/task_commands.rs +80 -1
  19. package/crates/naome-cli/tests/task_cli.rs +58 -0
  20. package/crates/naome-cli/tests/task_cli_agent_controls.rs +210 -0
  21. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  22. package/crates/naome-cli/tests/task_cli_loop.rs +383 -0
  23. package/crates/naome-cli/tests/task_cli_loop_edit.rs +144 -0
  24. package/crates/naome-cli/tests/task_cli_support/mod.rs +178 -0
  25. package/crates/naome-core/Cargo.toml +1 -1
  26. package/crates/naome-core/src/lib.rs +7 -2
  27. package/crates/naome-core/src/task_state/evidence_fingerprint.rs +47 -0
  28. package/crates/naome-core/src/task_state/mod.rs +12 -0
  29. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  30. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  31. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  32. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  33. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  34. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  35. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  36. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  37. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  38. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  39. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  40. package/crates/naome-core/src/task_state/status/model.rs +152 -0
  41. package/crates/naome-core/src/task_state/status/proof.rs +217 -0
  42. package/crates/naome-core/src/task_state/status/proof_read.rs +164 -0
  43. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  44. package/crates/naome-core/src/task_state/status/report_context.rs +148 -0
  45. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  46. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  47. package/crates/naome-core/src/task_state/status/transition.rs +101 -0
  48. package/crates/naome-core/src/task_state/status.rs +23 -0
  49. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  50. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  51. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  52. package/crates/naome-core/tests/task_status.rs +423 -0
  53. package/crates/naome-core/tests/task_status_git.rs +141 -0
  54. package/installer/context.js +1 -1
  55. package/installer/harness-verification.js +2 -6
  56. package/installer/manifest-state.js +2 -2
  57. package/installer/native.js +3 -31
  58. package/native/darwin-arm64/naome +0 -0
  59. package/native/linux-x64/naome +0 -0
  60. package/package.json +1 -1
  61. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  62. package/templates/naome-root/.naome/bin/check-task-state.js +4 -39
  63. package/templates/naome-root/.naome/bin/naome.js +2 -30
  64. package/templates/naome-root/.naome/manifest.json +2 -2
@@ -0,0 +1,133 @@
1
+ use std::path::Path;
2
+ use std::process::Command;
3
+
4
+ use crate::models::NaomeError;
5
+
6
+ use super::model::{finding, TaskGitStatus, TaskStatusFinding};
7
+ use crate::task_state::git_io::{command_output, git_commit_exists, read_git_head};
8
+
9
+ pub(super) fn git_status(
10
+ root: &Path,
11
+ admission_head: Option<String>,
12
+ findings: &mut Vec<TaskStatusFinding>,
13
+ ) -> Result<TaskGitStatus, NaomeError> {
14
+ let head = read_git_head(root)?;
15
+ let mut admission_head_reachable = false;
16
+ if let Some(admission_head) = admission_head.as_deref() {
17
+ if !git_commit_exists(root, admission_head)? {
18
+ findings.push(finding(
19
+ "task.git.admission_head_missing",
20
+ "error",
21
+ format!("Admission git head does not exist: {admission_head}."),
22
+ None,
23
+ "Recover the task-state from a reachable baseline or restart the task after sync.",
24
+ "Do not continue normal task work until the admission head is reachable.",
25
+ ));
26
+ } else if !is_ancestor(root, admission_head, "HEAD")? {
27
+ findings.push(finding(
28
+ "task.git.admission_head_not_reachable",
29
+ "error",
30
+ "Admission git head is not an ancestor of the current HEAD.",
31
+ None,
32
+ "Rebase or recreate the task-state against the current branch before continuing.",
33
+ "Do not commit task work against a branch that lost its admission baseline.",
34
+ ));
35
+ } else {
36
+ admission_head_reachable = true;
37
+ }
38
+ }
39
+
40
+ let (upstream, ahead, behind) = branch_divergence(root)?;
41
+ if upstream.is_some() && ahead > 0 && behind > 0 {
42
+ findings.push(finding(
43
+ "task.git.branch_diverged",
44
+ "warning",
45
+ format!("Current branch diverged from upstream ({ahead} ahead, {behind} behind)."),
46
+ None,
47
+ "Pull with rebase or coordinate the branch before recording final task proof.",
48
+ "Do not ignore branch divergence when interpreting task proof.",
49
+ ));
50
+ }
51
+
52
+ let operation_in_progress = operation_in_progress(root)?;
53
+ if let Some(operation) = &operation_in_progress {
54
+ findings.push(finding(
55
+ "task.git.operation_in_progress",
56
+ "error",
57
+ format!("Git operation in progress: {operation}."),
58
+ None,
59
+ "Finish or abort the git operation, then rerun naome task status.",
60
+ "Do not commit or mutate task-state while a git operation is unresolved.",
61
+ ));
62
+ }
63
+
64
+ Ok(TaskGitStatus {
65
+ head,
66
+ admission_head,
67
+ admission_head_reachable,
68
+ upstream,
69
+ ahead,
70
+ behind,
71
+ operation_in_progress,
72
+ })
73
+ }
74
+
75
+ fn is_ancestor(root: &Path, ancestor: &str, descendant: &str) -> Result<bool, NaomeError> {
76
+ Ok(Command::new("git")
77
+ .args(["merge-base", "--is-ancestor", ancestor, descendant])
78
+ .current_dir(root)
79
+ .status()?
80
+ .success())
81
+ }
82
+
83
+ fn branch_divergence(root: &Path) -> Result<(Option<String>, usize, usize), NaomeError> {
84
+ let upstream = Command::new("git")
85
+ .args(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
86
+ .current_dir(root)
87
+ .output()?;
88
+ if !upstream.status.success() {
89
+ return Ok((None, 0, 0));
90
+ }
91
+ let upstream_name = command_output(&upstream);
92
+ let counts = Command::new("git")
93
+ .args(["rev-list", "--left-right", "--count", "HEAD...@{u}"])
94
+ .current_dir(root)
95
+ .output()?;
96
+ if !counts.status.success() {
97
+ return Ok((Some(upstream_name), 0, 0));
98
+ }
99
+ let text = String::from_utf8_lossy(&counts.stdout);
100
+ let mut parts = text.split_whitespace();
101
+ let ahead = parts
102
+ .next()
103
+ .and_then(|value| value.parse::<usize>().ok())
104
+ .unwrap_or(0);
105
+ let behind = parts
106
+ .next()
107
+ .and_then(|value| value.parse::<usize>().ok())
108
+ .unwrap_or(0);
109
+ Ok((Some(upstream_name), ahead, behind))
110
+ }
111
+
112
+ fn operation_in_progress(root: &Path) -> Result<Option<String>, NaomeError> {
113
+ for (name, marker) in [
114
+ ("merge", "MERGE_HEAD"),
115
+ ("cherry-pick", "CHERRY_PICK_HEAD"),
116
+ ("revert", "REVERT_HEAD"),
117
+ ("rebase", "rebase-merge"),
118
+ ("rebase", "rebase-apply"),
119
+ ] {
120
+ let output = Command::new("git")
121
+ .args(["rev-parse", "--git-path", marker])
122
+ .current_dir(root)
123
+ .output()?;
124
+ if !output.status.success() {
125
+ continue;
126
+ }
127
+ let path = root.join(String::from_utf8_lossy(&output.stdout).trim());
128
+ if path.exists() {
129
+ return Ok(Some(name.to_string()));
130
+ }
131
+ }
132
+ Ok(None)
133
+ }
@@ -0,0 +1,152 @@
1
+ use serde::{Deserialize, Serialize};
2
+
3
+ use super::agent_model::{
4
+ AgentLoop, NextActionV2, PolicyHints, ProofRecording, RecoveryGuidance, RepairPlanItem,
5
+ };
6
+
7
+ pub(super) const STATUS_SCHEMA: &str = "naome.task.status.v1";
8
+ pub(super) const PROOF_PLAN_SCHEMA: &str = "naome.task.proof-plan.v1";
9
+ pub(super) const TASK_STATE_PATH: &str = ".naome/task-state.json";
10
+
11
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12
+ #[serde(rename_all = "camelCase")]
13
+ pub struct TaskStatusReportV1 {
14
+ pub schema: String,
15
+ pub state: String,
16
+ pub task_id: Option<String>,
17
+ pub request: Option<String>,
18
+ pub task_mode: TaskModeStatus,
19
+ pub git: TaskGitStatus,
20
+ pub scope: TaskScopeStatus,
21
+ pub proof: TaskProofStatus,
22
+ pub blocked: bool,
23
+ pub findings: Vec<TaskStatusFinding>,
24
+ pub next_action: String,
25
+ pub next_action_v2: NextActionV2,
26
+ pub agent_loop: AgentLoop,
27
+ pub repair_plan: Vec<RepairPlanItem>,
28
+ pub policy_hints: PolicyHints,
29
+ pub recovery_guidance: Vec<RecoveryGuidance>,
30
+ pub task_feedback: Vec<TaskFeedback>,
31
+ }
32
+
33
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34
+ #[serde(rename_all = "camelCase")]
35
+ pub struct TaskProofPlanReport {
36
+ pub schema: String,
37
+ pub state: String,
38
+ pub task_id: Option<String>,
39
+ pub task_mode: TaskModeStatus,
40
+ pub proof: TaskProofStatus,
41
+ pub recommended_commands: Vec<TaskRecommendedCommand>,
42
+ pub blocked: bool,
43
+ pub findings: Vec<TaskStatusFinding>,
44
+ pub next_action: String,
45
+ pub next_action_v2: NextActionV2,
46
+ pub agent_loop: AgentLoop,
47
+ pub repair_plan: Vec<RepairPlanItem>,
48
+ pub proof_recording: ProofRecording,
49
+ pub policy_hints: PolicyHints,
50
+ pub recovery_guidance: Vec<RecoveryGuidance>,
51
+ pub task_feedback: Vec<TaskFeedback>,
52
+ }
53
+
54
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55
+ #[serde(rename_all = "camelCase")]
56
+ pub struct TransitionReadinessReport {
57
+ pub schema: String,
58
+ pub target_state: String,
59
+ pub allowed: bool,
60
+ pub blocking_findings: Vec<TaskStatusFinding>,
61
+ pub required_before_transition: Vec<String>,
62
+ pub agent_loop: AgentLoop,
63
+ }
64
+
65
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66
+ #[serde(rename_all = "camelCase")]
67
+ pub struct TaskGitStatus {
68
+ pub head: Option<String>,
69
+ pub admission_head: Option<String>,
70
+ pub admission_head_reachable: bool,
71
+ pub upstream: Option<String>,
72
+ pub ahead: usize,
73
+ pub behind: usize,
74
+ pub operation_in_progress: Option<String>,
75
+ }
76
+
77
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78
+ #[serde(rename_all = "camelCase")]
79
+ pub struct TaskModeStatus {
80
+ pub kind: String,
81
+ pub review_fix: bool,
82
+ pub scope_policy: String,
83
+ }
84
+
85
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86
+ #[serde(rename_all = "camelCase")]
87
+ pub struct TaskScopeStatus {
88
+ pub allowed_paths: Vec<String>,
89
+ pub changed_paths: Vec<String>,
90
+ pub in_scope_changed_paths: Vec<String>,
91
+ pub out_of_scope_changed_paths: Vec<String>,
92
+ }
93
+
94
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95
+ #[serde(rename_all = "camelCase")]
96
+ pub struct TaskProofStatus {
97
+ pub required_checks: Vec<String>,
98
+ pub passed_checks: Vec<String>,
99
+ pub missing_checks: Vec<String>,
100
+ pub stale_checks: Vec<String>,
101
+ }
102
+
103
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104
+ #[serde(rename_all = "camelCase")]
105
+ pub struct TaskStatusFinding {
106
+ pub id: String,
107
+ pub severity: String,
108
+ pub message: String,
109
+ pub path: Option<String>,
110
+ pub suggested_fix: String,
111
+ pub agent_instruction: String,
112
+ }
113
+
114
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115
+ #[serde(rename_all = "camelCase")]
116
+ pub struct TaskFeedback {
117
+ pub problem: String,
118
+ pub repair: String,
119
+ pub files: Vec<String>,
120
+ pub must_not_do: Vec<String>,
121
+ }
122
+
123
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124
+ #[serde(rename_all = "camelCase")]
125
+ pub struct TaskRecommendedCommand {
126
+ pub check_id: String,
127
+ pub command: String,
128
+ pub cwd: String,
129
+ pub reason: String,
130
+ pub proof_reason: String,
131
+ pub selection_reason: String,
132
+ pub impacted_paths: Vec<String>,
133
+ pub safe_to_execute: bool,
134
+ }
135
+
136
+ pub(super) fn finding(
137
+ id: &str,
138
+ severity: &str,
139
+ message: impl Into<String>,
140
+ path: Option<String>,
141
+ suggested_fix: &str,
142
+ agent_instruction: &str,
143
+ ) -> TaskStatusFinding {
144
+ TaskStatusFinding {
145
+ id: id.to_string(),
146
+ severity: severity.to_string(),
147
+ message: message.into(),
148
+ path,
149
+ suggested_fix: suggested_fix.to_string(),
150
+ agent_instruction: agent_instruction.to_string(),
151
+ }
152
+ }
@@ -0,0 +1,217 @@
1
+ use std::collections::{BTreeMap, BTreeSet};
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+ use crate::task_state::evidence_fingerprint::task_evidence_fingerprint;
8
+ use crate::task_state::util::string_array;
9
+
10
+ use super::model::{finding, TaskProofStatus, TaskRecommendedCommand, TaskStatusFinding};
11
+ use super::proof_read::{ProofRecord, VerificationCheck};
12
+
13
+ const AUTONOMOUS_SAFE_CHECKS: &[&str] = &[
14
+ "git diff --check",
15
+ "node .naome/bin/check-harness-health.js",
16
+ "node .naome/bin/check-task-state.js",
17
+ "node .naome/bin/naome.js quality check --changed",
18
+ "node .naome/bin/naome.js semantic check --changed",
19
+ "node .naome/bin/naome.js arch validate --changed-only",
20
+ "npm run check:task-state",
21
+ "npm run test:task-state",
22
+ "npm run test:decision-engine",
23
+ ];
24
+
25
+ pub(super) fn proof_status(
26
+ root: &Path,
27
+ active_task: Option<&Value>,
28
+ proofs: &[ProofRecord],
29
+ current_task_paths: &[String],
30
+ ) -> Result<TaskProofStatus, NaomeError> {
31
+ let required_checks = active_task
32
+ .and_then(|task| string_array(task.get("requiredCheckIds")))
33
+ .unwrap_or_default();
34
+ let current_fingerprint = task_evidence_fingerprint(root, current_task_paths)?;
35
+ let mut passed_checks = Vec::new();
36
+ let mut missing_checks = Vec::new();
37
+ let mut stale_checks = Vec::new();
38
+
39
+ for check_id in &required_checks {
40
+ let successful = proofs
41
+ .iter()
42
+ .filter(|proof| proof.check_id == *check_id && proof.exit_code == 0)
43
+ .collect::<Vec<_>>();
44
+ if successful.is_empty() {
45
+ missing_checks.push(check_id.clone());
46
+ } else if successful
47
+ .iter()
48
+ .any(|proof| proof_is_fresh(proof, current_task_paths, &current_fingerprint))
49
+ {
50
+ passed_checks.push(check_id.clone());
51
+ } else {
52
+ stale_checks.push(check_id.clone());
53
+ }
54
+ }
55
+
56
+ Ok(TaskProofStatus {
57
+ required_checks,
58
+ passed_checks,
59
+ missing_checks,
60
+ stale_checks,
61
+ })
62
+ }
63
+
64
+ pub(super) fn add_proof_findings(
65
+ proof: &TaskProofStatus,
66
+ current_task_paths: &[String],
67
+ proofs: &[ProofRecord],
68
+ findings: &mut Vec<TaskStatusFinding>,
69
+ ) {
70
+ for check_id in &proof.missing_checks {
71
+ findings.push(finding(
72
+ "task.proof.missing_check",
73
+ "error",
74
+ format!("Required check has no passing proof: {check_id}."),
75
+ None,
76
+ "Run the required check and record passing proof before completing the task.",
77
+ "Do not mark a task complete while required checks are missing.",
78
+ ));
79
+ }
80
+
81
+ for check_id in &proof.stale_checks {
82
+ let files = stale_files_for_check(check_id, current_task_paths, proofs);
83
+ if files.is_empty() {
84
+ findings.push(finding(
85
+ "task.scope.missing_evidence",
86
+ "warning",
87
+ format!("Proof evidence is stale for required check: {check_id}."),
88
+ None,
89
+ "Rerun the check and record evidence for the current task-owned diff.",
90
+ "Do not reuse stale proof for new changed files.",
91
+ ));
92
+ }
93
+ for file in files {
94
+ findings.push(finding(
95
+ "task.scope.missing_evidence",
96
+ "warning",
97
+ format!("Proof evidence for {check_id} does not cover changed file: {file}."),
98
+ Some(file),
99
+ "Rerun the check and record evidence for the current task-owned diff.",
100
+ "Do not reuse stale proof for new changed files.",
101
+ ));
102
+ }
103
+ }
104
+ }
105
+
106
+ pub(super) fn add_unknown_proof_findings(
107
+ proofs: &[ProofRecord],
108
+ verification: &BTreeMap<String, VerificationCheck>,
109
+ findings: &mut Vec<TaskStatusFinding>,
110
+ ) {
111
+ let mut seen = BTreeSet::new();
112
+ for proof in proofs {
113
+ if verification.contains_key(&proof.check_id) || !seen.insert(proof.check_id.clone()) {
114
+ continue;
115
+ }
116
+ findings.push(finding(
117
+ "task.proof.unknown_check_metadata",
118
+ "info",
119
+ format!(
120
+ "Proof references a check not present in .naome/verification.json: {}.",
121
+ proof.check_id
122
+ ),
123
+ None,
124
+ "Keep the proof readable; add verification metadata if agents should recommend it.",
125
+ "Treat unknown check metadata as advisory, not as proof failure by itself.",
126
+ ));
127
+ }
128
+ }
129
+
130
+ pub(super) fn recommended_commands(
131
+ proof: &TaskProofStatus,
132
+ verification: &BTreeMap<String, VerificationCheck>,
133
+ current_task_paths: &[String],
134
+ ) -> Vec<TaskRecommendedCommand> {
135
+ let check_ids = proof
136
+ .missing_checks
137
+ .iter()
138
+ .chain(proof.stale_checks.iter())
139
+ .cloned()
140
+ .collect::<BTreeSet<_>>();
141
+ check_ids
142
+ .iter()
143
+ .filter_map(|check_id| {
144
+ let check = verification.get(check_id)?;
145
+ let reason = if proof.stale_checks.contains(check_id) {
146
+ "stale-proof".to_string()
147
+ } else {
148
+ "missing-proof".to_string()
149
+ };
150
+ Some(TaskRecommendedCommand {
151
+ check_id: check_id.clone(),
152
+ command: check.command.clone(),
153
+ cwd: check.cwd.clone(),
154
+ reason: reason.clone(),
155
+ proof_reason: reason,
156
+ selection_reason: "Required by active task proof state.".to_string(),
157
+ impacted_paths: current_task_paths.to_vec(),
158
+ safe_to_execute: is_safe_autonomous_command(
159
+ &check.command,
160
+ &check.cwd,
161
+ current_task_paths,
162
+ ),
163
+ })
164
+ })
165
+ .collect()
166
+ }
167
+
168
+ fn is_safe_autonomous_command(command: &str, cwd: &str, current_task_paths: &[String]) -> bool {
169
+ if cwd != "." || !AUTONOMOUS_SAFE_CHECKS.contains(&command) {
170
+ return false;
171
+ }
172
+ if command.starts_with("npm run ")
173
+ && current_task_paths
174
+ .iter()
175
+ .any(|path| path == "package.json" || path == "packages/naome/package.json")
176
+ {
177
+ return false;
178
+ }
179
+ true
180
+ }
181
+
182
+ fn evidence_covers(evidence_paths: &[String], current_paths: &[String]) -> bool {
183
+ current_paths
184
+ .iter()
185
+ .all(|path| evidence_paths.iter().any(|evidence| evidence == path))
186
+ }
187
+
188
+ fn proof_is_fresh(
189
+ proof: &ProofRecord,
190
+ current_paths: &[String],
191
+ current_fingerprint: &str,
192
+ ) -> bool {
193
+ evidence_covers(&proof.evidence_paths, current_paths)
194
+ && proof
195
+ .evidence_fingerprint
196
+ .as_deref()
197
+ .is_none_or(|fingerprint| fingerprint == current_fingerprint)
198
+ }
199
+
200
+ fn stale_files_for_check(
201
+ check_id: &str,
202
+ current_task_paths: &[String],
203
+ proofs: &[ProofRecord],
204
+ ) -> Vec<String> {
205
+ let mut covered = BTreeSet::new();
206
+ for proof in proofs
207
+ .iter()
208
+ .filter(|proof| proof.check_id == check_id && proof.exit_code == 0)
209
+ {
210
+ covered.extend(proof.evidence_paths.iter().cloned());
211
+ }
212
+ current_task_paths
213
+ .iter()
214
+ .filter(|path| !covered.contains(*path))
215
+ .cloned()
216
+ .collect()
217
+ }
@@ -0,0 +1,164 @@
1
+ use std::collections::BTreeMap;
2
+ use std::fs;
3
+ use std::path::Path;
4
+
5
+ use serde_json::Value;
6
+
7
+ use crate::models::NaomeError;
8
+ use crate::task_state::evidence::evidence_entry_path;
9
+ use crate::task_state::util::normalize_path;
10
+
11
+ use super::model::{finding, TaskStatusFinding};
12
+
13
+ #[derive(Debug, Clone)]
14
+ pub(super) struct ProofRecord {
15
+ pub(super) check_id: String,
16
+ pub(super) exit_code: i64,
17
+ pub(super) evidence_paths: Vec<String>,
18
+ pub(super) evidence_fingerprint: Option<String>,
19
+ }
20
+
21
+ #[derive(Debug, Clone)]
22
+ pub(super) struct VerificationCheck {
23
+ pub(super) command: String,
24
+ pub(super) cwd: String,
25
+ }
26
+
27
+ pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
28
+ let path_sets = proof_path_sets(active_task);
29
+ let mut proofs = Vec::new();
30
+ if let Some(legacy) = active_task.get("proofResults").and_then(Value::as_array) {
31
+ proofs.extend(legacy.iter().filter_map(read_legacy_proof));
32
+ }
33
+ if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
34
+ for batch in batches {
35
+ let batch_evidence = batch_evidence(batch, &path_sets);
36
+ let batch_fingerprint = batch
37
+ .get("evidenceFingerprint")
38
+ .and_then(Value::as_str)
39
+ .map(ToString::to_string);
40
+ let Some(batch_proofs) = batch.get("proofs").and_then(Value::as_array) else {
41
+ continue;
42
+ };
43
+ for proof in batch_proofs {
44
+ let Some(check_id) = proof.get("checkId").and_then(Value::as_str) else {
45
+ continue;
46
+ };
47
+ let Some(exit_code) = proof.get("exitCode").and_then(Value::as_i64) else {
48
+ continue;
49
+ };
50
+ proofs.push(ProofRecord {
51
+ check_id: check_id.to_string(),
52
+ exit_code,
53
+ evidence_paths: compact_evidence_paths(proof, &path_sets)
54
+ .unwrap_or_else(|| batch_evidence.clone()),
55
+ evidence_fingerprint: proof
56
+ .get("evidenceFingerprint")
57
+ .and_then(Value::as_str)
58
+ .map(ToString::to_string)
59
+ .or_else(|| batch_fingerprint.clone()),
60
+ });
61
+ }
62
+ }
63
+ }
64
+ proofs
65
+ }
66
+
67
+ pub(super) fn read_verification_checks(
68
+ root: &Path,
69
+ findings: &mut Vec<TaskStatusFinding>,
70
+ ) -> Result<BTreeMap<String, VerificationCheck>, NaomeError> {
71
+ let content = match fs::read_to_string(root.join(".naome/verification.json")) {
72
+ Ok(content) => content,
73
+ Err(_) => return Ok(BTreeMap::new()),
74
+ };
75
+ let verification: Value = match serde_json::from_str(&content) {
76
+ Ok(value) => value,
77
+ Err(error) => {
78
+ findings.push(finding(
79
+ "task.proof.verification_metadata_unreadable",
80
+ "warning",
81
+ format!(".naome/verification.json is not valid JSON: {error}."),
82
+ Some(".naome/verification.json".to_string()),
83
+ "Fix verification metadata so proof-plan can recommend commands.",
84
+ "Do not invent check commands when verification metadata is unreadable.",
85
+ ));
86
+ return Ok(BTreeMap::new());
87
+ }
88
+ };
89
+ Ok(verification
90
+ .get("checks")
91
+ .and_then(Value::as_array)
92
+ .into_iter()
93
+ .flatten()
94
+ .filter_map(|check| {
95
+ Some((
96
+ check.get("id")?.as_str()?.to_string(),
97
+ VerificationCheck {
98
+ command: check.get("command")?.as_str()?.to_string(),
99
+ cwd: check.get("cwd")?.as_str()?.to_string(),
100
+ },
101
+ ))
102
+ })
103
+ .collect())
104
+ }
105
+
106
+ fn batch_evidence(batch: &Value, path_sets: &BTreeMap<String, Vec<String>>) -> Vec<String> {
107
+ batch
108
+ .get("evidencePathSet")
109
+ .and_then(Value::as_str)
110
+ .and_then(|name| path_sets.get(name))
111
+ .cloned()
112
+ .unwrap_or_default()
113
+ }
114
+
115
+ fn compact_evidence_paths(
116
+ proof: &Value,
117
+ path_sets: &BTreeMap<String, Vec<String>>,
118
+ ) -> Option<Vec<String>> {
119
+ proof
120
+ .get("evidence")
121
+ .and_then(Value::as_array)
122
+ .map(|entries| evidence_paths(entries))
123
+ .or_else(|| {
124
+ proof
125
+ .get("evidencePathSet")
126
+ .and_then(Value::as_str)
127
+ .and_then(|name| path_sets.get(name))
128
+ .cloned()
129
+ })
130
+ }
131
+
132
+ fn read_legacy_proof(proof: &Value) -> Option<ProofRecord> {
133
+ Some(ProofRecord {
134
+ check_id: proof.get("checkId")?.as_str()?.to_string(),
135
+ exit_code: proof.get("exitCode")?.as_i64()?,
136
+ evidence_fingerprint: proof
137
+ .get("evidenceFingerprint")
138
+ .and_then(Value::as_str)
139
+ .map(ToString::to_string),
140
+ evidence_paths: proof
141
+ .get("evidence")
142
+ .and_then(Value::as_array)
143
+ .map(|entries| evidence_paths(entries))
144
+ .unwrap_or_default(),
145
+ })
146
+ }
147
+
148
+ fn proof_path_sets(active_task: &Value) -> BTreeMap<String, Vec<String>> {
149
+ active_task
150
+ .get("proofPathSets")
151
+ .and_then(Value::as_object)
152
+ .into_iter()
153
+ .flat_map(|sets| sets.iter())
154
+ .filter_map(|(name, value)| Some((name.clone(), evidence_paths(value.as_array()?))))
155
+ .collect()
156
+ }
157
+
158
+ fn evidence_paths(entries: &[Value]) -> Vec<String> {
159
+ entries
160
+ .iter()
161
+ .filter_map(evidence_entry_path)
162
+ .map(normalize_path)
163
+ .collect()
164
+ }