@lamentis/naome 1.1.1 → 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.
- package/Cargo.lock +2 -2
- package/Cargo.toml +1 -1
- package/LICENSE +180 -21
- package/README.md +49 -6
- package/bin/naome-node.js +44 -4
- package/bin/naome.js +54 -16
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/check_commands.rs +135 -0
- package/crates/naome-cli/src/cli_args.rs +5 -0
- package/crates/naome-cli/src/dispatcher.rs +36 -0
- package/crates/naome-cli/src/install_bridge.rs +83 -0
- package/crates/naome-cli/src/main.rs +57 -341
- package/crates/naome-cli/src/prompt_commands.rs +68 -0
- package/crates/naome-cli/src/quality_commands.rs +141 -0
- package/crates/naome-cli/src/simple_commands.rs +53 -0
- package/crates/naome-cli/src/workflow_commands.rs +153 -0
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/harness_health/integrity.rs +96 -0
- package/crates/naome-core/src/harness_health.rs +14 -126
- package/crates/naome-core/src/install_plan.rs +3 -0
- package/crates/naome-core/src/intent/classifier.rs +171 -0
- package/crates/naome-core/src/intent/envelope.rs +108 -0
- package/crates/naome-core/src/intent/legacy.rs +138 -0
- package/crates/naome-core/src/intent/legacy_response.rs +76 -0
- package/crates/naome-core/src/intent/model.rs +71 -0
- package/crates/naome-core/src/intent/patterns.rs +170 -0
- package/crates/naome-core/src/intent/resolver.rs +162 -0
- package/crates/naome-core/src/intent/resolver_active.rs +17 -0
- package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
- package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
- package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
- package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
- package/crates/naome-core/src/intent/risk.rs +40 -0
- package/crates/naome-core/src/intent/segment.rs +170 -0
- package/crates/naome-core/src/intent.rs +64 -879
- package/crates/naome-core/src/journal.rs +9 -20
- package/crates/naome-core/src/lib.rs +13 -0
- package/crates/naome-core/src/quality/adapters.rs +178 -0
- package/crates/naome-core/src/quality/baseline.rs +75 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
- package/crates/naome-core/src/quality/checks.rs +228 -0
- package/crates/naome-core/src/quality/cleanup.rs +72 -0
- package/crates/naome-core/src/quality/config.rs +109 -0
- package/crates/naome-core/src/quality/mod.rs +90 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
- package/crates/naome-core/src/quality/scanner.rs +367 -0
- package/crates/naome-core/src/quality/types.rs +289 -0
- package/crates/naome-core/src/route.rs +292 -17
- package/crates/naome-core/src/task_state/admission.rs +63 -0
- package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
- package/crates/naome-core/src/task_state/api.rs +130 -0
- package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
- package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
- package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
- package/crates/naome-core/src/task_state/completion.rs +72 -0
- package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
- package/crates/naome-core/src/task_state/diff.rs +95 -0
- package/crates/naome-core/src/task_state/evidence.rs +154 -0
- package/crates/naome-core/src/task_state/git_io.rs +86 -0
- package/crates/naome-core/src/task_state/git_parse.rs +86 -0
- package/crates/naome-core/src/task_state/git_refs.rs +37 -0
- package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
- package/crates/naome-core/src/task_state/mod.rs +38 -0
- package/crates/naome-core/src/task_state/process_guard.rs +40 -0
- package/crates/naome-core/src/task_state/progress.rs +123 -0
- package/crates/naome-core/src/task_state/proof.rs +139 -0
- package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
- package/crates/naome-core/src/task_state/proof_model.rs +70 -0
- package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
- package/crates/naome-core/src/task_state/push_gate.rs +49 -0
- package/crates/naome-core/src/task_state/reconcile.rs +7 -0
- package/crates/naome-core/src/task_state/repair.rs +168 -0
- package/crates/naome-core/src/task_state/shape.rs +117 -0
- package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
- package/crates/naome-core/src/task_state/task_records.rs +131 -0
- package/crates/naome-core/src/task_state/task_references.rs +126 -0
- package/crates/naome-core/src/task_state/types.rs +87 -0
- package/crates/naome-core/src/task_state/util.rs +137 -0
- package/crates/naome-core/src/verification/render.rs +122 -0
- package/crates/naome-core/src/verification.rs +176 -58
- package/crates/naome-core/src/verification_contract.rs +49 -21
- package/crates/naome-core/src/workflow/integrity.rs +123 -0
- package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
- package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
- package/crates/naome-core/src/workflow/mod.rs +18 -0
- package/crates/naome-core/src/workflow/mutation.rs +68 -0
- package/crates/naome-core/src/workflow/output.rs +111 -0
- package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
- package/crates/naome-core/src/workflow/phases.rs +169 -0
- package/crates/naome-core/src/workflow/policy.rs +156 -0
- package/crates/naome-core/src/workflow/processes.rs +91 -0
- package/crates/naome-core/src/workflow/types.rs +42 -0
- package/crates/naome-core/tests/harness_health.rs +3 -0
- package/crates/naome-core/tests/intent.rs +97 -792
- package/crates/naome-core/tests/intent_support/mod.rs +133 -0
- package/crates/naome-core/tests/intent_v2.rs +90 -0
- package/crates/naome-core/tests/quality.rs +425 -0
- package/crates/naome-core/tests/route.rs +221 -4
- package/crates/naome-core/tests/task_state.rs +3 -0
- package/crates/naome-core/tests/task_state_compact.rs +110 -0
- package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
- package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
- package/crates/naome-core/tests/workflow_integrity.rs +85 -0
- package/crates/naome-core/tests/workflow_policy.rs +139 -0
- package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -2
- package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
- package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
- package/templates/naome-root/.naome/bin/naome.js +34 -63
- package/templates/naome-root/.naome/manifest.json +20 -18
- package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
- package/templates/naome-root/.naome/repository-quality.json +24 -0
- package/templates/naome-root/.naome/task-contract.schema.json +93 -11
- package/templates/naome-root/.naome/upgrade-state.json +1 -1
- package/templates/naome-root/.naome/verification.json +37 -0
- package/templates/naome-root/AGENTS.md +3 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
- package/templates/naome-root/docs/naome/execution.md +25 -21
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +43 -0
- package/templates/naome-root/docs/naome/testing.md +12 -0
- package/crates/naome-core/src/task_state.rs +0 -2210
|
@@ -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
|
+
}
|
|
@@ -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, ¤t_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
|
+
}
|