@lamentis/naome 1.3.17 → 1.4.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 (55) hide show
  1. package/Cargo.lock +2 -2
  2. package/crates/naome-cli/Cargo.toml +1 -1
  3. package/crates/naome-cli/src/architecture_commands.rs +16 -3
  4. package/crates/naome-cli/src/main.rs +10 -1
  5. package/crates/naome-cli/src/task_commands/common.rs +32 -0
  6. package/crates/naome-cli/src/task_commands/readiness.rs +40 -0
  7. package/crates/naome-cli/src/task_commands/record.rs +134 -0
  8. package/crates/naome-cli/src/task_commands/repair.rs +30 -0
  9. package/crates/naome-cli/src/task_commands/scope_request.rs +24 -0
  10. package/crates/naome-cli/src/task_commands/timeline.rs +71 -0
  11. package/crates/naome-cli/src/task_commands.rs +69 -1
  12. package/crates/naome-cli/tests/task_cli.rs +58 -0
  13. package/crates/naome-cli/tests/task_cli_agent_controls.rs +217 -0
  14. package/crates/naome-cli/tests/task_cli_control.rs +126 -0
  15. package/crates/naome-cli/tests/task_cli_support/mod.rs +150 -0
  16. package/crates/naome-core/Cargo.toml +1 -1
  17. package/crates/naome-core/src/architecture/output.rs +196 -0
  18. package/crates/naome-core/src/architecture/scan/cache.rs +1 -1
  19. package/crates/naome-core/src/architecture.rs +1 -0
  20. package/crates/naome-core/src/lib.rs +15 -9
  21. package/crates/naome-core/src/task_state/mod.rs +10 -0
  22. package/crates/naome-core/src/task_state/status/agent_model.rs +76 -0
  23. package/crates/naome-core/src/task_state/status/control/action.rs +87 -0
  24. package/crates/naome-core/src/task_state/status/control/exit_code.rs +32 -0
  25. package/crates/naome-core/src/task_state/status/control/loop_state.rs +70 -0
  26. package/crates/naome-core/src/task_state/status/control/policy.rs +31 -0
  27. package/crates/naome-core/src/task_state/status/control/proof_recording.rs +25 -0
  28. package/crates/naome-core/src/task_state/status/control/recovery.rs +19 -0
  29. package/crates/naome-core/src/task_state/status/control/repair.rs +125 -0
  30. package/crates/naome-core/src/task_state/status/control/shared.rs +25 -0
  31. package/crates/naome-core/src/task_state/status/control.rs +16 -0
  32. package/crates/naome-core/src/task_state/status/git.rs +133 -0
  33. package/crates/naome-core/src/task_state/status/model.rs +150 -0
  34. package/crates/naome-core/src/task_state/status/proof.rs +167 -0
  35. package/crates/naome-core/src/task_state/status/proof_read.rs +150 -0
  36. package/crates/naome-core/src/task_state/status/report.rs +148 -0
  37. package/crates/naome-core/src/task_state/status/report_context.rs +126 -0
  38. package/crates/naome-core/src/task_state/status/report_support.rs +117 -0
  39. package/crates/naome-core/src/task_state/status/scope.rs +111 -0
  40. package/crates/naome-core/src/task_state/status/transition.rs +73 -0
  41. package/crates/naome-core/src/task_state/status.rs +23 -0
  42. package/crates/naome-core/src/task_state/status_output.rs +103 -0
  43. package/crates/naome-core/tests/architecture_cache.rs +1 -1
  44. package/crates/naome-core/tests/architecture_config.rs +68 -1
  45. package/crates/naome-core/tests/task_state_support/mod.rs +15 -1
  46. package/crates/naome-core/tests/task_state_support/states.rs +4 -0
  47. package/crates/naome-core/tests/task_status.rs +301 -0
  48. package/crates/naome-core/tests/task_status_git.rs +141 -0
  49. package/native/darwin-arm64/naome +0 -0
  50. package/native/linux-x64/naome +0 -0
  51. package/package.json +1 -1
  52. package/templates/naome-root/.naome/bin/check-harness-health.js +1 -1
  53. package/templates/naome-root/.naome/bin/check-task-state.js +1 -1
  54. package/templates/naome-root/.naome/manifest.json +2 -2
  55. package/templates/naome-root/docs/naome/architecture-fitness.md +23 -23
@@ -0,0 +1,150 @@
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
+ }
19
+
20
+ #[derive(Debug, Clone)]
21
+ pub(super) struct VerificationCheck {
22
+ pub(super) command: String,
23
+ pub(super) cwd: String,
24
+ }
25
+
26
+ pub(super) fn read_proofs(active_task: &Value) -> Vec<ProofRecord> {
27
+ let path_sets = proof_path_sets(active_task);
28
+ let mut proofs = Vec::new();
29
+ if let Some(legacy) = active_task.get("proofResults").and_then(Value::as_array) {
30
+ proofs.extend(legacy.iter().filter_map(read_legacy_proof));
31
+ }
32
+ if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
33
+ for batch in batches {
34
+ let batch_evidence = batch_evidence(batch, &path_sets);
35
+ let Some(batch_proofs) = batch.get("proofs").and_then(Value::as_array) else {
36
+ continue;
37
+ };
38
+ for proof in batch_proofs {
39
+ let Some(check_id) = proof.get("checkId").and_then(Value::as_str) else {
40
+ continue;
41
+ };
42
+ let Some(exit_code) = proof.get("exitCode").and_then(Value::as_i64) else {
43
+ continue;
44
+ };
45
+ proofs.push(ProofRecord {
46
+ check_id: check_id.to_string(),
47
+ exit_code,
48
+ evidence_paths: compact_evidence_paths(proof, &path_sets)
49
+ .unwrap_or_else(|| batch_evidence.clone()),
50
+ });
51
+ }
52
+ }
53
+ }
54
+ proofs
55
+ }
56
+
57
+ pub(super) fn read_verification_checks(
58
+ root: &Path,
59
+ findings: &mut Vec<TaskStatusFinding>,
60
+ ) -> Result<BTreeMap<String, VerificationCheck>, NaomeError> {
61
+ let content = match fs::read_to_string(root.join(".naome/verification.json")) {
62
+ Ok(content) => content,
63
+ Err(_) => return Ok(BTreeMap::new()),
64
+ };
65
+ let verification: Value = match serde_json::from_str(&content) {
66
+ Ok(value) => value,
67
+ Err(error) => {
68
+ findings.push(finding(
69
+ "task.proof.verification_metadata_unreadable",
70
+ "warning",
71
+ format!(".naome/verification.json is not valid JSON: {error}."),
72
+ Some(".naome/verification.json".to_string()),
73
+ "Fix verification metadata so proof-plan can recommend commands.",
74
+ "Do not invent check commands when verification metadata is unreadable.",
75
+ ));
76
+ return Ok(BTreeMap::new());
77
+ }
78
+ };
79
+ Ok(verification
80
+ .get("checks")
81
+ .and_then(Value::as_array)
82
+ .into_iter()
83
+ .flatten()
84
+ .filter_map(|check| {
85
+ Some((
86
+ check.get("id")?.as_str()?.to_string(),
87
+ VerificationCheck {
88
+ command: check.get("command")?.as_str()?.to_string(),
89
+ cwd: check.get("cwd")?.as_str()?.to_string(),
90
+ },
91
+ ))
92
+ })
93
+ .collect())
94
+ }
95
+
96
+ fn batch_evidence(batch: &Value, path_sets: &BTreeMap<String, Vec<String>>) -> Vec<String> {
97
+ batch
98
+ .get("evidencePathSet")
99
+ .and_then(Value::as_str)
100
+ .and_then(|name| path_sets.get(name))
101
+ .cloned()
102
+ .unwrap_or_default()
103
+ }
104
+
105
+ fn compact_evidence_paths(
106
+ proof: &Value,
107
+ path_sets: &BTreeMap<String, Vec<String>>,
108
+ ) -> Option<Vec<String>> {
109
+ proof
110
+ .get("evidence")
111
+ .and_then(Value::as_array)
112
+ .map(|entries| evidence_paths(entries))
113
+ .or_else(|| {
114
+ proof
115
+ .get("evidencePathSet")
116
+ .and_then(Value::as_str)
117
+ .and_then(|name| path_sets.get(name))
118
+ .cloned()
119
+ })
120
+ }
121
+
122
+ fn read_legacy_proof(proof: &Value) -> Option<ProofRecord> {
123
+ Some(ProofRecord {
124
+ check_id: proof.get("checkId")?.as_str()?.to_string(),
125
+ exit_code: proof.get("exitCode")?.as_i64()?,
126
+ evidence_paths: proof
127
+ .get("evidence")
128
+ .and_then(Value::as_array)
129
+ .map(|entries| evidence_paths(entries))
130
+ .unwrap_or_default(),
131
+ })
132
+ }
133
+
134
+ fn proof_path_sets(active_task: &Value) -> BTreeMap<String, Vec<String>> {
135
+ active_task
136
+ .get("proofPathSets")
137
+ .and_then(Value::as_object)
138
+ .into_iter()
139
+ .flat_map(|sets| sets.iter())
140
+ .filter_map(|(name, value)| Some((name.clone(), evidence_paths(value.as_array()?))))
141
+ .collect()
142
+ }
143
+
144
+ fn evidence_paths(entries: &[Value]) -> Vec<String> {
145
+ entries
146
+ .iter()
147
+ .filter_map(evidence_entry_path)
148
+ .map(normalize_path)
149
+ .collect()
150
+ }
@@ -0,0 +1,148 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ use super::agent_model::{AgentLoop, NextActionV2, RepairPlanItem};
6
+ use super::control::{
7
+ agent_loop, next_action_v2, policy_hints, proof_recording, recovery_guidance, repair_plan,
8
+ };
9
+ use super::model::{
10
+ TaskProofPlanReport, TaskStatusReportV1, TransitionReadinessReport, PROOF_PLAN_SCHEMA,
11
+ STATUS_SCHEMA,
12
+ };
13
+ use super::report_context::TaskStatusContext;
14
+ use super::report_support::{next_action, read_task_state_for_status, task_feedback};
15
+ use super::transition::transition_report;
16
+
17
+ pub fn task_status_report(root: &Path) -> Result<TaskStatusReportV1, NaomeError> {
18
+ let mut findings = Vec::new();
19
+ let task_state = read_task_state_for_status(root, &mut findings)?;
20
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
21
+ Ok(status_report(context))
22
+ }
23
+
24
+ pub fn task_proof_plan(root: &Path) -> Result<TaskProofPlanReport, NaomeError> {
25
+ let mut findings = Vec::new();
26
+ let task_state = read_task_state_for_status(root, &mut findings)?;
27
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
28
+ Ok(proof_plan_report(context))
29
+ }
30
+
31
+ pub fn task_transition_readiness(
32
+ root: &Path,
33
+ target_state: &str,
34
+ ) -> Result<TransitionReadinessReport, NaomeError> {
35
+ if target_state != "complete" {
36
+ return Err(NaomeError::new(format!(
37
+ "unsupported task transition target: {target_state}; v1.4.1 supports only complete"
38
+ )));
39
+ }
40
+ let mut findings = Vec::new();
41
+ let task_state = read_task_state_for_status(root, &mut findings)?;
42
+ let context = TaskStatusContext::new(root, task_state.as_ref(), findings)?;
43
+ Ok(transition_report(context, target_state))
44
+ }
45
+
46
+ fn status_report(context: TaskStatusContext) -> TaskStatusReportV1 {
47
+ let blocked = context
48
+ .findings
49
+ .iter()
50
+ .any(|finding| finding.severity == "error");
51
+ let next_action = next_action(&context.state, &context.proof, &context.findings);
52
+ let agent = agent_control(&context);
53
+ let policy_hints = policy_hints(&context.scope, &context.proof, &context.findings);
54
+ let recovery_guidance = recovery_guidance(&context.findings);
55
+ TaskStatusReportV1 {
56
+ schema: STATUS_SCHEMA.to_string(),
57
+ state: context.state,
58
+ task_id: context.task_id,
59
+ request: context.request,
60
+ task_mode: context.task_mode,
61
+ git: context.git,
62
+ scope: context.scope,
63
+ proof: context.proof,
64
+ blocked,
65
+ task_feedback: task_feedback(&context.findings),
66
+ findings: context.findings,
67
+ next_action,
68
+ next_action_v2: agent.next_action,
69
+ agent_loop: agent.loop_state,
70
+ repair_plan: agent.repair_plan,
71
+ policy_hints,
72
+ recovery_guidance,
73
+ }
74
+ }
75
+
76
+ fn proof_plan_report(context: TaskStatusContext) -> TaskProofPlanReport {
77
+ let next_action = proof_plan_next_action(&context);
78
+ let agent = agent_control(&context);
79
+ let proof_recording =
80
+ proof_recording(context.task_id.as_deref(), &context.scope, &context.proof);
81
+ let policy_hints = policy_hints(&context.scope, &context.proof, &context.findings);
82
+ let recovery_guidance = recovery_guidance(&context.findings);
83
+ let blocked = context
84
+ .findings
85
+ .iter()
86
+ .any(|finding| finding.id == "task.scope.out_of_scope_change");
87
+ TaskProofPlanReport {
88
+ schema: PROOF_PLAN_SCHEMA.to_string(),
89
+ state: context.state,
90
+ task_id: context.task_id,
91
+ task_mode: context.task_mode,
92
+ proof: context.proof,
93
+ recommended_commands: context.recommended_commands,
94
+ blocked,
95
+ task_feedback: task_feedback(&context.findings),
96
+ findings: context.findings,
97
+ next_action,
98
+ next_action_v2: agent.next_action,
99
+ agent_loop: agent.loop_state,
100
+ repair_plan: agent.repair_plan,
101
+ proof_recording,
102
+ policy_hints,
103
+ recovery_guidance,
104
+ }
105
+ }
106
+
107
+ struct AgentControl {
108
+ next_action: NextActionV2,
109
+ loop_state: AgentLoop,
110
+ repair_plan: Vec<RepairPlanItem>,
111
+ }
112
+
113
+ fn agent_control(context: &TaskStatusContext) -> AgentControl {
114
+ AgentControl {
115
+ next_action: next_action_v2(
116
+ &context.state,
117
+ &context.proof,
118
+ &context.findings,
119
+ &context.recommended_commands,
120
+ &context.scope,
121
+ ),
122
+ loop_state: agent_loop(
123
+ &context.state,
124
+ &context.proof,
125
+ &context.findings,
126
+ &context.scope,
127
+ ),
128
+ repair_plan: repair_plan(
129
+ &context.proof,
130
+ &context.findings,
131
+ &context.recommended_commands,
132
+ &context.scope,
133
+ ),
134
+ }
135
+ }
136
+
137
+ fn proof_plan_next_action(context: &TaskStatusContext) -> String {
138
+ if !context.proof.missing_checks.is_empty() || !context.proof.stale_checks.is_empty() {
139
+ if context.recommended_commands.is_empty() {
140
+ "Recover verification metadata or run the missing/stale checks manually, then record the proof in task-state.".to_string()
141
+ } else {
142
+ "Run the recommended missing or stale checks, then record the proof in task-state."
143
+ .to_string()
144
+ }
145
+ } else {
146
+ "Proof evidence is current for the active task.".to_string()
147
+ }
148
+ }
@@ -0,0 +1,126 @@
1
+ use std::path::Path;
2
+
3
+ use serde_json::Value;
4
+
5
+ use crate::models::NaomeError;
6
+ use crate::task_state::util::string_array;
7
+
8
+ use super::git::git_status;
9
+ use super::model::{
10
+ TaskGitStatus, TaskModeStatus, TaskProofStatus, TaskRecommendedCommand, TaskScopeStatus,
11
+ TaskStatusFinding,
12
+ };
13
+ use super::proof::{
14
+ add_proof_findings, add_unknown_proof_findings, proof_status, recommended_commands,
15
+ };
16
+ use super::proof_read::{read_proofs, read_verification_checks};
17
+ use super::report_support::{changed_entries, state_name, task_text};
18
+ use super::scope::{add_scope_findings, scope_status};
19
+
20
+ pub(super) struct TaskStatusContext {
21
+ pub(super) state: String,
22
+ pub(super) task_id: Option<String>,
23
+ pub(super) request: Option<String>,
24
+ pub(super) task_mode: TaskModeStatus,
25
+ pub(super) git: TaskGitStatus,
26
+ pub(super) scope: TaskScopeStatus,
27
+ pub(super) proof: TaskProofStatus,
28
+ pub(super) recommended_commands: Vec<TaskRecommendedCommand>,
29
+ pub(super) findings: Vec<TaskStatusFinding>,
30
+ }
31
+
32
+ impl TaskStatusContext {
33
+ pub(super) fn new(
34
+ root: &Path,
35
+ task_state: Option<&Value>,
36
+ mut findings: Vec<TaskStatusFinding>,
37
+ ) -> Result<Self, NaomeError> {
38
+ let active_task = task_state
39
+ .and_then(|state| state.get("activeTask"))
40
+ .filter(|task| task.is_object());
41
+ let state = state_name(task_state);
42
+ add_active_task_shape_finding(&state, task_state, active_task, &mut findings);
43
+ let task_id = task_text(active_task, "id");
44
+ let request = task_text(active_task, "request");
45
+ let task_mode = task_mode(active_task);
46
+ let admission_head = active_task
47
+ .and_then(|task| task.get("admission"))
48
+ .and_then(|admission| admission.get("gitHead"))
49
+ .and_then(Value::as_str)
50
+ .map(ToString::to_string);
51
+
52
+ let changed_entries = changed_entries(root, &mut findings);
53
+ let git = git_status(root, admission_head, &mut findings)?;
54
+ let scope = scope_status(active_task, &changed_entries);
55
+ add_scope_findings(&state, active_task, &scope, &mut findings);
56
+
57
+ let verification = read_verification_checks(root, &mut findings)?;
58
+ let proofs = active_task.map(read_proofs).unwrap_or_default();
59
+ add_unknown_proof_findings(&proofs, &verification, &mut findings);
60
+ let proof = proof_status(active_task, &proofs, &scope.in_scope_changed_paths);
61
+ add_proof_findings(
62
+ &proof,
63
+ &scope.in_scope_changed_paths,
64
+ &proofs,
65
+ &mut findings,
66
+ );
67
+ let recommended_commands =
68
+ recommended_commands(&proof, &verification, &scope.in_scope_changed_paths);
69
+
70
+ Ok(Self {
71
+ state,
72
+ task_id,
73
+ request,
74
+ task_mode,
75
+ git,
76
+ scope,
77
+ proof,
78
+ recommended_commands,
79
+ findings,
80
+ })
81
+ }
82
+ }
83
+
84
+ fn add_active_task_shape_finding(
85
+ state: &str,
86
+ task_state: Option<&Value>,
87
+ active_task: Option<&Value>,
88
+ findings: &mut Vec<TaskStatusFinding>,
89
+ ) {
90
+ if !matches!(
91
+ state,
92
+ "implementing" | "blocked" | "needs_human_review" | "revising"
93
+ ) {
94
+ return;
95
+ }
96
+ if task_state.is_some() && active_task.is_none() {
97
+ findings.push(super::model::finding(
98
+ "task.state.active_task_missing",
99
+ "error",
100
+ format!("Task state is {state} but activeTask is missing or not an object."),
101
+ Some(".naome/task-state.json".to_string()),
102
+ "Restore a valid activeTask object or reset the task state before continuing.",
103
+ "Do not complete or commit a task while activeTask is missing.",
104
+ ));
105
+ }
106
+ }
107
+
108
+ fn task_mode(active_task: Option<&Value>) -> TaskModeStatus {
109
+ let kind = task_text(active_task, "kind").unwrap_or_else(|| "standard".to_string());
110
+ let declared_review_fix = active_task
111
+ .and_then(|task| string_array(task.get("declaredChangeTypes")))
112
+ .unwrap_or_default()
113
+ .iter()
114
+ .any(|value| value == "review-fix");
115
+ let review_fix = kind == "review_fix" || declared_review_fix;
116
+ TaskModeStatus {
117
+ kind,
118
+ review_fix,
119
+ scope_policy: if review_fix {
120
+ "Review-fix tasks must stay inside explicit allowedPaths."
121
+ } else {
122
+ "Task changes must stay inside explicit allowedPaths."
123
+ }
124
+ .to_string(),
125
+ }
126
+ }
@@ -0,0 +1,117 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+ use crate::task_ledger::read_task_state_projection;
8
+ use crate::task_state::git_io::read_git_changed_entries;
9
+ use crate::task_state::types::ChangedEntry;
10
+
11
+ use super::model::{finding, TaskFeedback, TaskProofStatus, TaskStatusFinding, TASK_STATE_PATH};
12
+
13
+ pub(super) fn read_task_state_for_status(
14
+ root: &Path,
15
+ findings: &mut Vec<TaskStatusFinding>,
16
+ ) -> Result<Option<Value>, NaomeError> {
17
+ let path = root.join(TASK_STATE_PATH);
18
+ if let Ok(content) = fs::read_to_string(&path) {
19
+ if has_conflict_markers(&content) {
20
+ findings.push(finding(
21
+ "task.state.conflict_markers",
22
+ "error",
23
+ ".naome/task-state.json contains unresolved conflict markers.",
24
+ Some(TASK_STATE_PATH.to_string()),
25
+ "Resolve the conflict markers and rerun naome task status.",
26
+ "Do not edit or commit task-state conflict markers.",
27
+ ));
28
+ return Ok(None);
29
+ }
30
+ if let Err(error) = serde_json::from_str::<Value>(&content) {
31
+ findings.push(finding(
32
+ "task.state.invalid_json",
33
+ "error",
34
+ format!(".naome/task-state.json is not valid JSON: {error}."),
35
+ Some(TASK_STATE_PATH.to_string()),
36
+ "Repair .naome/task-state.json to valid JSON or restore it from a clean task baseline.",
37
+ "Do not continue task work while task-state JSON is malformed.",
38
+ ));
39
+ return Ok(None);
40
+ }
41
+ }
42
+ read_task_state_projection(root)
43
+ }
44
+
45
+ pub(super) fn changed_entries(
46
+ root: &Path,
47
+ findings: &mut Vec<TaskStatusFinding>,
48
+ ) -> Vec<ChangedEntry> {
49
+ match read_git_changed_entries(root) {
50
+ Ok(entries) => entries,
51
+ Err(error) => {
52
+ findings.push(finding(
53
+ "task.git.status_unavailable",
54
+ "warning",
55
+ format!("Git changed-file status is unavailable: {error}"),
56
+ None,
57
+ "Run task status from a valid git work tree.",
58
+ "Do not infer task scope while git status is unavailable.",
59
+ ));
60
+ Vec::new()
61
+ }
62
+ }
63
+ }
64
+
65
+ pub(super) fn state_name(task_state: Option<&Value>) -> String {
66
+ task_state
67
+ .and_then(|state| state.get("status"))
68
+ .and_then(Value::as_str)
69
+ .unwrap_or("missing")
70
+ .to_string()
71
+ }
72
+
73
+ pub(super) fn task_text(active_task: Option<&Value>, key: &str) -> Option<String> {
74
+ active_task
75
+ .and_then(|task| task.get(key))
76
+ .and_then(Value::as_str)
77
+ .map(ToString::to_string)
78
+ }
79
+
80
+ pub(super) fn task_feedback(findings: &[TaskStatusFinding]) -> Vec<TaskFeedback> {
81
+ findings
82
+ .iter()
83
+ .filter(|finding| finding.severity != "info")
84
+ .map(|finding| TaskFeedback {
85
+ problem: finding.message.clone(),
86
+ repair: finding.suggested_fix.clone(),
87
+ files: finding.path.iter().cloned().collect(),
88
+ must_not_do: vec![
89
+ "Do not commit out-of-scope changes.".to_string(),
90
+ "Do not bypass the NAOME commit gate.".to_string(),
91
+ ],
92
+ })
93
+ .collect()
94
+ }
95
+
96
+ pub(super) fn next_action(
97
+ state: &str,
98
+ proof: &TaskProofStatus,
99
+ findings: &[TaskStatusFinding],
100
+ ) -> String {
101
+ if let Some(finding) = findings.iter().find(|finding| finding.severity == "error") {
102
+ return finding.suggested_fix.clone();
103
+ }
104
+ if !proof.missing_checks.is_empty() || !proof.stale_checks.is_empty() {
105
+ return "Run missing or stale checks from naome task proof-plan before completion."
106
+ .to_string();
107
+ }
108
+ match state {
109
+ "idle" | "missing" => "Create a NAOME task before feature work.".to_string(),
110
+ "complete" => "Task is complete; baseline it before starting unrelated work.".to_string(),
111
+ _ => "Continue the active task and keep proof current.".to_string(),
112
+ }
113
+ }
114
+
115
+ fn has_conflict_markers(content: &str) -> bool {
116
+ content.contains("<<<<<<<") && content.contains("=======") && content.contains(">>>>>>>")
117
+ }
@@ -0,0 +1,111 @@
1
+ use serde_json::Value;
2
+
3
+ use super::model::{finding, TaskScopeStatus, TaskStatusFinding, TASK_STATE_PATH};
4
+ use crate::task_state::types::{is_control_state_path, ChangedEntry};
5
+ use crate::task_state::util::{matches_any_pattern, string_array};
6
+
7
+ pub(super) fn scope_status(
8
+ active_task: Option<&Value>,
9
+ changed_entries: &[ChangedEntry],
10
+ ) -> TaskScopeStatus {
11
+ let allowed_paths = active_task
12
+ .and_then(|task| string_array(task.get("allowedPaths")))
13
+ .unwrap_or_default();
14
+ let changed_paths = changed_entries
15
+ .iter()
16
+ .map(|entry| entry.path.clone())
17
+ .collect::<Vec<_>>();
18
+ let task_paths = changed_paths
19
+ .iter()
20
+ .filter(|path| !is_control_state_path(path))
21
+ .cloned()
22
+ .collect::<Vec<_>>();
23
+ let in_scope_changed_paths = task_paths
24
+ .iter()
25
+ .filter(|path| matches_any_pattern(path, &allowed_paths))
26
+ .cloned()
27
+ .collect::<Vec<_>>();
28
+ let out_of_scope_changed_paths = if active_task.is_some() {
29
+ task_paths
30
+ .iter()
31
+ .filter(|path| !matches_any_pattern(path, &allowed_paths))
32
+ .cloned()
33
+ .collect::<Vec<_>>()
34
+ } else {
35
+ Vec::new()
36
+ };
37
+
38
+ TaskScopeStatus {
39
+ allowed_paths,
40
+ changed_paths,
41
+ in_scope_changed_paths,
42
+ out_of_scope_changed_paths,
43
+ }
44
+ }
45
+
46
+ pub(super) fn add_scope_findings(
47
+ state: &str,
48
+ active_task: Option<&Value>,
49
+ scope: &TaskScopeStatus,
50
+ findings: &mut Vec<TaskStatusFinding>,
51
+ ) {
52
+ for path in &scope.out_of_scope_changed_paths {
53
+ findings.push(finding(
54
+ "task.scope.out_of_scope_change",
55
+ "error",
56
+ format!("Changed file is outside the active task scope: {path}."),
57
+ Some(path.clone()),
58
+ "Either add the file through an explicit task scope revision or remove the unrelated change.",
59
+ "Do not commit out-of-scope changes or bypass the NAOME commit gate.",
60
+ ));
61
+ }
62
+
63
+ if !scope.changed_paths.is_empty()
64
+ && scope
65
+ .changed_paths
66
+ .iter()
67
+ .all(|path| is_control_state_path(path))
68
+ {
69
+ findings.push(finding(
70
+ "task.scope.control_state_only_change",
71
+ "warning",
72
+ "Only NAOME control-state files are changed.",
73
+ Some(TASK_STATE_PATH.to_string()),
74
+ "Continue only if the task-state transition is intentional and supported by proof.",
75
+ "Do not use control-state-only edits to bypass normal task admission.",
76
+ ));
77
+ }
78
+
79
+ if active_task.is_none()
80
+ && scope
81
+ .changed_paths
82
+ .iter()
83
+ .any(|path| !is_control_state_path(path))
84
+ {
85
+ findings.push(finding(
86
+ "task.state.no_active_task_with_diff",
87
+ "error",
88
+ "Changed files exist but no active task is available.",
89
+ None,
90
+ "Create or recover the active task before continuing feature work.",
91
+ "Do not commit repository changes without an admitted NAOME task.",
92
+ ));
93
+ }
94
+
95
+ if state == "complete" && !scope.changed_paths.is_empty() {
96
+ for path in scope
97
+ .changed_paths
98
+ .iter()
99
+ .filter(|path| !is_control_state_path(path))
100
+ {
101
+ findings.push(finding(
102
+ "task.state.completed_task_has_diff",
103
+ "warning",
104
+ format!("Completed task has new unbaselined diff: {path}."),
105
+ Some(path.clone()),
106
+ "Baseline the completed task or create a new task for the new diff.",
107
+ "Do not mix new work into a completed task without an explicit baseline.",
108
+ ));
109
+ }
110
+ }
111
+ }