@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,139 @@
|
|
|
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::diff::task_diff_from_entries;
|
|
9
|
+
pub(super) use super::evidence::{
|
|
10
|
+
evidence_entry_path, validate_control_state_paths, validate_control_state_patterns,
|
|
11
|
+
validate_evidence_array, validate_evidence_paths,
|
|
12
|
+
};
|
|
13
|
+
use super::git_io::read_git_changed_entries;
|
|
14
|
+
use super::proof_entry::validate_proof_result;
|
|
15
|
+
use super::proof_model::canonical_proofs;
|
|
16
|
+
use super::types::ChangedEntry;
|
|
17
|
+
use super::util::{is_iso_datetime, matches_any_pattern, normalize_path, string_array};
|
|
18
|
+
pub(super) fn validate_proof_result_entries(
|
|
19
|
+
active_task: &Value,
|
|
20
|
+
check_ids: &HashSet<String>,
|
|
21
|
+
root: &Path,
|
|
22
|
+
errors: &mut Vec<String>,
|
|
23
|
+
) -> Result<(), NaomeError> {
|
|
24
|
+
if let Some(proofs) = active_task.get("proofResults").and_then(Value::as_array) {
|
|
25
|
+
for (index, proof) in proofs.iter().enumerate() {
|
|
26
|
+
validate_proof_result(proof, index, check_ids, root, errors, active_task)?;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for proof in canonical_proofs(active_task, root, errors)? {
|
|
30
|
+
if proof.command.trim().is_empty() {
|
|
31
|
+
errors.push(format!(
|
|
32
|
+
"activeTask proof command is empty: {}",
|
|
33
|
+
proof.check_id
|
|
34
|
+
));
|
|
35
|
+
}
|
|
36
|
+
if proof.cwd.trim().is_empty() {
|
|
37
|
+
errors.push(format!("activeTask proof cwd is empty: {}", proof.check_id));
|
|
38
|
+
}
|
|
39
|
+
if !is_iso_datetime(&proof.checked_at) {
|
|
40
|
+
errors.push(format!(
|
|
41
|
+
"activeTask proof checkedAt must be an ISO timestamp: {}",
|
|
42
|
+
proof.check_id
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub(super) fn validate_proof_results(
|
|
51
|
+
active_task: &Value,
|
|
52
|
+
check_ids: &HashSet<String>,
|
|
53
|
+
root: &Path,
|
|
54
|
+
errors: &mut Vec<String>,
|
|
55
|
+
) -> Result<(), NaomeError> {
|
|
56
|
+
let Some(required_check_ids) = active_task
|
|
57
|
+
.get("requiredCheckIds")
|
|
58
|
+
.and_then(Value::as_array)
|
|
59
|
+
else {
|
|
60
|
+
return Ok(());
|
|
61
|
+
};
|
|
62
|
+
let proofs = canonical_proofs(active_task, root, errors)?;
|
|
63
|
+
|
|
64
|
+
for check_id in required_check_ids {
|
|
65
|
+
let Some(check_id) = check_id.as_str().filter(|value| !value.trim().is_empty()) else {
|
|
66
|
+
continue;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
match proofs.iter().find(|proof| proof.check_id == check_id) {
|
|
70
|
+
Some(proof) if proof.exit_code == 0 => {}
|
|
71
|
+
Some(_) => errors.push(format!(
|
|
72
|
+
"activeTask.proofResults failed proof result: {check_id}"
|
|
73
|
+
)),
|
|
74
|
+
None => errors.push(format!(
|
|
75
|
+
"activeTask.proofResults missing proof result: {check_id}"
|
|
76
|
+
)),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
validate_proof_result_entries(active_task, check_ids, root, errors)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub(super) fn validate_proof_evidence_covers_changed_paths(
|
|
84
|
+
active_task: Option<&Value>,
|
|
85
|
+
root: &Path,
|
|
86
|
+
errors: &mut Vec<String>,
|
|
87
|
+
) -> Result<(), NaomeError> {
|
|
88
|
+
let Some(active_task) = active_task else {
|
|
89
|
+
return Ok(());
|
|
90
|
+
};
|
|
91
|
+
let mut proof_errors = Vec::new();
|
|
92
|
+
let proofs = canonical_proofs(active_task, root, &mut proof_errors)?;
|
|
93
|
+
if proofs.is_empty() {
|
|
94
|
+
return Ok(());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let entries = read_git_changed_entries(root)?;
|
|
98
|
+
validate_proof_evidence_covers_changed_entries(active_task, root, &entries, errors)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pub(super) fn validate_proof_evidence_covers_changed_entries(
|
|
102
|
+
active_task: &Value,
|
|
103
|
+
root: &Path,
|
|
104
|
+
entries: &[ChangedEntry],
|
|
105
|
+
errors: &mut Vec<String>,
|
|
106
|
+
) -> Result<(), NaomeError> {
|
|
107
|
+
let mut proof_errors = Vec::new();
|
|
108
|
+
let proofs = canonical_proofs(active_task, root, &mut proof_errors)?;
|
|
109
|
+
if proofs.is_empty() {
|
|
110
|
+
return Ok(());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let changed_paths = task_diff_from_entries(active_task, entries);
|
|
114
|
+
let evidence_paths: HashSet<String> = proofs
|
|
115
|
+
.iter()
|
|
116
|
+
.flat_map(|proof| proof.evidence.clone())
|
|
117
|
+
.filter_map(|entry| evidence_entry_path(&entry).map(normalize_path))
|
|
118
|
+
.collect();
|
|
119
|
+
|
|
120
|
+
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
121
|
+
let changed_allowed_paths: Vec<String> = changed_paths
|
|
122
|
+
.diff_paths
|
|
123
|
+
.into_iter()
|
|
124
|
+
.filter(|path| matches_any_pattern(path, &allowed_paths))
|
|
125
|
+
.collect();
|
|
126
|
+
let missing_paths: Vec<String> = changed_allowed_paths
|
|
127
|
+
.into_iter()
|
|
128
|
+
.filter(|path| !evidence_paths.contains(path))
|
|
129
|
+
.collect();
|
|
130
|
+
|
|
131
|
+
if !missing_paths.is_empty() {
|
|
132
|
+
errors.push(format!(
|
|
133
|
+
"activeTask.proofResults evidence missing changed allowed paths: {}",
|
|
134
|
+
missing_paths.join(", ")
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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::proof::{
|
|
9
|
+
validate_control_state_paths, validate_evidence_array, validate_evidence_paths,
|
|
10
|
+
};
|
|
11
|
+
use super::util::{is_iso_datetime, require_string};
|
|
12
|
+
|
|
13
|
+
pub(super) fn validate_proof_result(
|
|
14
|
+
proof: &Value,
|
|
15
|
+
index: usize,
|
|
16
|
+
check_ids: &HashSet<String>,
|
|
17
|
+
root: &Path,
|
|
18
|
+
errors: &mut Vec<String>,
|
|
19
|
+
active_task: &Value,
|
|
20
|
+
) -> Result<(), NaomeError> {
|
|
21
|
+
let prefix = format!("activeTask.proofResults[{index}]");
|
|
22
|
+
let Some(object) = proof.as_object() else {
|
|
23
|
+
errors.push(format!("{prefix} must be an object."));
|
|
24
|
+
return Ok(());
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
require_string(object.get("checkId"), &format!("{prefix}.checkId"), errors);
|
|
28
|
+
require_string(object.get("command"), &format!("{prefix}.command"), errors);
|
|
29
|
+
require_string(object.get("cwd"), &format!("{prefix}.cwd"), errors);
|
|
30
|
+
validate_evidence_array(
|
|
31
|
+
object.get("evidence"),
|
|
32
|
+
&format!("{prefix}.evidence"),
|
|
33
|
+
errors,
|
|
34
|
+
);
|
|
35
|
+
validate_control_state_paths(
|
|
36
|
+
object.get("evidence"),
|
|
37
|
+
&format!("{prefix}.evidence"),
|
|
38
|
+
errors,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if object.get("exitCode").and_then(Value::as_i64).is_none() {
|
|
42
|
+
errors.push(format!("{prefix}.exitCode must be an integer."));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if !object
|
|
46
|
+
.get("checkedAt")
|
|
47
|
+
.and_then(Value::as_str)
|
|
48
|
+
.is_some_and(is_iso_datetime)
|
|
49
|
+
{
|
|
50
|
+
errors.push(format!("{prefix}.checkedAt must be an ISO timestamp."));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if let Some(check_id) = object.get("checkId").and_then(Value::as_str) {
|
|
54
|
+
if !check_id.trim().is_empty() && !check_ids.contains(check_id) {
|
|
55
|
+
errors.push(format!("{prefix}.checkId unknown check id: {check_id}"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
validate_evidence_paths(
|
|
60
|
+
object.get("evidence"),
|
|
61
|
+
&format!("{prefix}.evidence"),
|
|
62
|
+
root,
|
|
63
|
+
errors,
|
|
64
|
+
active_task,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::compact_proof::compact_proofs;
|
|
8
|
+
use super::proof_sources::{check_id_from_proof, read_verification_defaults};
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub(super) struct CanonicalProof {
|
|
12
|
+
pub(super) check_id: String,
|
|
13
|
+
pub(super) command: String,
|
|
14
|
+
pub(super) cwd: String,
|
|
15
|
+
pub(super) exit_code: i64,
|
|
16
|
+
pub(super) checked_at: String,
|
|
17
|
+
pub(super) evidence: Vec<Value>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone)]
|
|
21
|
+
pub(super) struct VerificationDefaults {
|
|
22
|
+
pub(super) command: String,
|
|
23
|
+
pub(super) cwd: String,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(super) fn canonical_proofs(
|
|
27
|
+
active_task: &Value,
|
|
28
|
+
root: &Path,
|
|
29
|
+
errors: &mut Vec<String>,
|
|
30
|
+
) -> Result<Vec<CanonicalProof>, NaomeError> {
|
|
31
|
+
let defaults = read_verification_defaults(root, errors)?;
|
|
32
|
+
let mut proofs = legacy_proofs(active_task);
|
|
33
|
+
proofs.extend(compact_proofs(active_task, root, errors, &defaults)?);
|
|
34
|
+
Ok(proofs)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub(crate) fn canonical_proof_check_ids(active_task: &Value) -> Vec<String> {
|
|
38
|
+
let mut check_ids = Vec::new();
|
|
39
|
+
if let Some(proofs) = active_task.get("proofResults").and_then(Value::as_array) {
|
|
40
|
+
check_ids.extend(proofs.iter().filter_map(check_id_from_proof));
|
|
41
|
+
}
|
|
42
|
+
if let Some(batches) = active_task.get("proofBatches").and_then(Value::as_array) {
|
|
43
|
+
for batch in batches {
|
|
44
|
+
if let Some(proofs) = batch.get("proofs").and_then(Value::as_array) {
|
|
45
|
+
check_ids.extend(proofs.iter().filter_map(check_id_from_proof));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
check_ids
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn legacy_proofs(active_task: &Value) -> Vec<CanonicalProof> {
|
|
53
|
+
active_task
|
|
54
|
+
.get("proofResults")
|
|
55
|
+
.and_then(Value::as_array)
|
|
56
|
+
.into_iter()
|
|
57
|
+
.flatten()
|
|
58
|
+
.filter_map(|proof| {
|
|
59
|
+
let evidence = proof.get("evidence")?.as_array()?.clone();
|
|
60
|
+
Some(CanonicalProof {
|
|
61
|
+
check_id: proof.get("checkId")?.as_str()?.to_string(),
|
|
62
|
+
command: proof.get("command")?.as_str()?.to_string(),
|
|
63
|
+
cwd: proof.get("cwd")?.as_str()?.to_string(),
|
|
64
|
+
exit_code: proof.get("exitCode")?.as_i64()?,
|
|
65
|
+
checked_at: proof.get("checkedAt")?.as_str()?.to_string(),
|
|
66
|
+
evidence,
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
.collect()
|
|
70
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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::VerificationDefaults;
|
|
12
|
+
use super::util::read_json;
|
|
13
|
+
|
|
14
|
+
pub(super) fn read_path_sets(
|
|
15
|
+
active_task: &Value,
|
|
16
|
+
root: &Path,
|
|
17
|
+
errors: &mut Vec<String>,
|
|
18
|
+
) -> Result<HashMap<String, Vec<Value>>, NaomeError> {
|
|
19
|
+
let mut sets = HashMap::new();
|
|
20
|
+
let Some(path_sets) = active_task.get("proofPathSets") else {
|
|
21
|
+
return Ok(sets);
|
|
22
|
+
};
|
|
23
|
+
let Some(path_sets) = path_sets.as_object() else {
|
|
24
|
+
errors.push("activeTask.proofPathSets must be an object when present.".to_string());
|
|
25
|
+
return Ok(sets);
|
|
26
|
+
};
|
|
27
|
+
for (name, value) in path_sets {
|
|
28
|
+
let prefix = format!("activeTask.proofPathSets.{name}");
|
|
29
|
+
validate_evidence_array(Some(value), &prefix, errors);
|
|
30
|
+
validate_control_state_paths(Some(value), &prefix, errors);
|
|
31
|
+
validate_evidence_paths(Some(value), &prefix, root, errors, active_task)?;
|
|
32
|
+
if let Some(paths) = value.as_array() {
|
|
33
|
+
sets.insert(name.clone(), paths.clone());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
Ok(sets)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub(super) fn read_verification_defaults(
|
|
40
|
+
root: &Path,
|
|
41
|
+
errors: &mut Vec<String>,
|
|
42
|
+
) -> Result<HashMap<String, VerificationDefaults>, NaomeError> {
|
|
43
|
+
let Some(verification) = read_json(root, ".naome/verification.json", errors)? else {
|
|
44
|
+
return Ok(HashMap::new());
|
|
45
|
+
};
|
|
46
|
+
let mut defaults = HashMap::new();
|
|
47
|
+
for check in verification
|
|
48
|
+
.get("checks")
|
|
49
|
+
.and_then(Value::as_array)
|
|
50
|
+
.into_iter()
|
|
51
|
+
.flatten()
|
|
52
|
+
{
|
|
53
|
+
let (Some(id), Some(command), Some(cwd)) = (
|
|
54
|
+
check.get("id").and_then(Value::as_str),
|
|
55
|
+
check.get("command").and_then(Value::as_str),
|
|
56
|
+
check.get("cwd").and_then(Value::as_str),
|
|
57
|
+
) else {
|
|
58
|
+
continue;
|
|
59
|
+
};
|
|
60
|
+
defaults.insert(
|
|
61
|
+
id.to_string(),
|
|
62
|
+
VerificationDefaults {
|
|
63
|
+
command: command.to_string(),
|
|
64
|
+
cwd: cwd.to_string(),
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
Ok(defaults)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub(super) fn check_id_from_proof(proof: &Value) -> Option<String> {
|
|
72
|
+
proof
|
|
73
|
+
.get("checkId")
|
|
74
|
+
.and_then(Value::as_str)
|
|
75
|
+
.map(ToString::to_string)
|
|
76
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use crate::models::NaomeError;
|
|
6
|
+
|
|
7
|
+
use super::repair::{is_completed_task_diff, is_harness_repair_diff, is_naome_baseline_diff};
|
|
8
|
+
use super::shape::validate_blocker;
|
|
9
|
+
use super::types::BLOCKING_STATUS;
|
|
10
|
+
pub(super) fn validate_push_gate(task_state: &Value, errors: &mut Vec<String>) {
|
|
11
|
+
let status = task_state
|
|
12
|
+
.get("status")
|
|
13
|
+
.and_then(Value::as_str)
|
|
14
|
+
.unwrap_or("invalid");
|
|
15
|
+
if !BLOCKING_STATUS.contains(&status) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if status == "blocked" || status == "needs_human_review" {
|
|
20
|
+
validate_blocker(task_state.get("blocker"), errors);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
errors.push(format!("NAOME push gate blocked because task state is {status}. Resolve the active task before pushing. Human options: continue_current_task, request_task_changes, mark_task_blocked, cancel_task_state."));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub(super) fn format_dirty_diff_admission_blocker(
|
|
27
|
+
task_state: &Value,
|
|
28
|
+
root: &Path,
|
|
29
|
+
changed_paths: &[String],
|
|
30
|
+
) -> Result<String, NaomeError> {
|
|
31
|
+
let prefix = format!(
|
|
32
|
+
"Task admission requires a clean git diff. Changed paths: {}.",
|
|
33
|
+
changed_paths.join(", ")
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if is_harness_repair_diff(root, changed_paths)? {
|
|
37
|
+
return Ok(format!("{prefix} These look like completed Harness Repair changes. Run NAOME intent for the next natural-language request before deciding whether to baseline, review, or cancel the repair diff."));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if is_completed_task_diff(task_state, changed_paths) {
|
|
41
|
+
return Ok(format!("{prefix} These look like completed task changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline a valid completed task before creating the next task."));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if is_naome_baseline_diff(changed_paths) {
|
|
45
|
+
return Ok(format!("{prefix} These look like completed NAOME install or upgrade changes. Run NAOME intent for the next natural-language request; deterministic policy can baseline setup before creating the next task."));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Ok(format!("{prefix} Ask the user to choose exactly one: review_task_diff, request_task_changes, cancel_task_changes. Do not start new feature work or commit without explicit user selection."))
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pub(super) use super::completed_refresh::add_completed_task_diff_notice;
|
|
2
|
+
pub(super) use super::push_gate::{format_dirty_diff_admission_blocker, validate_push_gate};
|
|
3
|
+
pub(super) use super::repair::{
|
|
4
|
+
is_deterministic_harness_refresh_diff, is_harness_repair_diff,
|
|
5
|
+
is_install_or_upgrade_baseline_diff, is_packaged_machine_owned_path, is_repair_archive_path,
|
|
6
|
+
is_safe_harness_refresh_path,
|
|
7
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
use crate::install_plan::MACHINE_OWNED_PATHS;
|
|
7
|
+
use crate::models::NaomeError;
|
|
8
|
+
|
|
9
|
+
use super::types::CONTROL_STATE_PATH;
|
|
10
|
+
use super::util::{
|
|
11
|
+
is_non_empty_string, matches_any_pattern, normalize_path, read_json, string_array,
|
|
12
|
+
};
|
|
13
|
+
pub(super) fn is_harness_repair_diff(
|
|
14
|
+
root: &Path,
|
|
15
|
+
changed_paths: &[String],
|
|
16
|
+
) -> Result<bool, NaomeError> {
|
|
17
|
+
let machine_owned_paths = read_machine_owned_paths(root)?;
|
|
18
|
+
if machine_owned_paths.is_empty() {
|
|
19
|
+
return Ok(false);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let has_repair_signal = changed_paths
|
|
23
|
+
.iter()
|
|
24
|
+
.any(|path| machine_owned_paths.contains(path) || is_repair_archive_path(path));
|
|
25
|
+
if !has_repair_signal {
|
|
26
|
+
return Ok(false);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Ok(changed_paths
|
|
30
|
+
.iter()
|
|
31
|
+
.all(|path| machine_owned_paths.contains(path) || is_repair_support_path(path)))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub(super) fn is_repair_support_path(path: &str) -> bool {
|
|
35
|
+
path == ".naome/manifest.json"
|
|
36
|
+
|| path == ".naome/upgrade-state.json"
|
|
37
|
+
|| is_repair_archive_path(path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub(super) fn is_packaged_machine_owned_path(path: &str) -> bool {
|
|
41
|
+
MACHINE_OWNED_PATHS.iter().any(|owned| *owned == path)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub(super) fn is_safe_harness_refresh_path(path: &str) -> bool {
|
|
45
|
+
is_packaged_machine_owned_path(path) || is_repair_support_path(path)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub(super) fn is_deterministic_harness_refresh_diff(changed_paths: &[String]) -> bool {
|
|
49
|
+
!changed_paths.is_empty()
|
|
50
|
+
&& changed_paths
|
|
51
|
+
.iter()
|
|
52
|
+
.any(|path| is_packaged_machine_owned_path(path) || is_repair_archive_path(path))
|
|
53
|
+
&& changed_paths
|
|
54
|
+
.iter()
|
|
55
|
+
.all(|path| is_safe_harness_refresh_path(path))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(super) fn is_repair_archive_path(path: &str) -> bool {
|
|
59
|
+
path.starts_with(".naome/archive/repair-")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(super) fn is_completed_task_diff(task_state: &Value, changed_paths: &[String]) -> bool {
|
|
63
|
+
if task_state.get("status").and_then(Value::as_str) != Some("complete") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
let Some(active_task) = task_state.get("activeTask") else {
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
let allowed_paths = string_array(active_task.get("allowedPaths")).unwrap_or_default();
|
|
70
|
+
let task_paths: Vec<&String> = changed_paths
|
|
71
|
+
.iter()
|
|
72
|
+
.filter(|path| path.as_str() != CONTROL_STATE_PATH)
|
|
73
|
+
.collect();
|
|
74
|
+
!task_paths.is_empty()
|
|
75
|
+
&& task_paths
|
|
76
|
+
.iter()
|
|
77
|
+
.all(|path| matches_any_pattern(path, &allowed_paths))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub(super) fn is_naome_baseline_diff(changed_paths: &[String]) -> bool {
|
|
81
|
+
changed_paths.iter().all(|path| {
|
|
82
|
+
path == "AGENTS.md"
|
|
83
|
+
|| path == ".gitignore"
|
|
84
|
+
|| path == ".naomeignore"
|
|
85
|
+
|| path.starts_with(".naome/")
|
|
86
|
+
|| path.starts_with("docs/naome/")
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub(super) fn is_install_or_upgrade_baseline_diff(
|
|
91
|
+
root: &Path,
|
|
92
|
+
changed_paths: &[String],
|
|
93
|
+
) -> Result<bool, NaomeError> {
|
|
94
|
+
if !is_naome_baseline_diff(changed_paths) {
|
|
95
|
+
return Ok(false);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let has_setup_signal = changed_paths.iter().any(|path| {
|
|
99
|
+
matches!(
|
|
100
|
+
path.as_str(),
|
|
101
|
+
"AGENTS.md"
|
|
102
|
+
| ".gitignore"
|
|
103
|
+
| ".naomeignore"
|
|
104
|
+
| ".naome/init-state.json"
|
|
105
|
+
| ".naome/manifest.json"
|
|
106
|
+
| ".naome/package.json"
|
|
107
|
+
| ".naome/task-contract.schema.json"
|
|
108
|
+
| ".naome/upgrade-state.json"
|
|
109
|
+
)
|
|
110
|
+
});
|
|
111
|
+
if !has_setup_signal {
|
|
112
|
+
return Ok(false);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if read_init_incomplete(root)? || read_upgrade_baseline_signal(root)? {
|
|
116
|
+
return Ok(true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Ok(changed_paths.iter().any(|path| {
|
|
120
|
+
matches!(
|
|
121
|
+
path.as_str(),
|
|
122
|
+
".naome/init-state.json" | ".naome/manifest.json" | ".naome/upgrade-state.json"
|
|
123
|
+
)
|
|
124
|
+
}))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub(super) fn read_init_incomplete(root: &Path) -> Result<bool, NaomeError> {
|
|
128
|
+
let Some(init_state) = read_json(root, ".naome/init-state.json", &mut Vec::new())? else {
|
|
129
|
+
return Ok(false);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
Ok(
|
|
133
|
+
init_state.get("initialized").and_then(Value::as_bool) != Some(true)
|
|
134
|
+
|| init_state.get("intakeStatus").and_then(Value::as_str) != Some("complete"),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub(super) fn read_upgrade_baseline_signal(root: &Path) -> Result<bool, NaomeError> {
|
|
139
|
+
let Some(upgrade_state) = read_json(root, ".naome/upgrade-state.json", &mut Vec::new())? else {
|
|
140
|
+
return Ok(false);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
Ok(upgrade_state.get("fromVersion").is_some()
|
|
144
|
+
|| upgrade_state
|
|
145
|
+
.get("pending")
|
|
146
|
+
.and_then(Value::as_array)
|
|
147
|
+
.is_some_and(|pending| !pending.is_empty())
|
|
148
|
+
|| upgrade_state
|
|
149
|
+
.get("completed")
|
|
150
|
+
.and_then(Value::as_array)
|
|
151
|
+
.is_some_and(|completed| !completed.is_empty()))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub(super) fn read_machine_owned_paths(root: &Path) -> Result<HashSet<String>, NaomeError> {
|
|
155
|
+
let Some(manifest) = read_json(root, ".naome/manifest.json", &mut Vec::new())? else {
|
|
156
|
+
return Ok(HashSet::new());
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
Ok(manifest
|
|
160
|
+
.get("machineOwned")
|
|
161
|
+
.and_then(Value::as_array)
|
|
162
|
+
.into_iter()
|
|
163
|
+
.flatten()
|
|
164
|
+
.filter_map(Value::as_str)
|
|
165
|
+
.filter(|value| is_non_empty_string(value))
|
|
166
|
+
.map(normalize_path)
|
|
167
|
+
.collect())
|
|
168
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
2
|
+
|
|
3
|
+
use super::proof::validate_control_state_patterns;
|
|
4
|
+
pub(super) use super::task_records::{
|
|
5
|
+
format_blocker, validate_blocker, validate_human_review, validate_prompt_record,
|
|
6
|
+
validate_revisions,
|
|
7
|
+
};
|
|
8
|
+
pub(super) use super::task_references::{
|
|
9
|
+
read_verification_check_ids, validate_active_task_references, validate_pending_upgrade,
|
|
10
|
+
validate_required_check_ids,
|
|
11
|
+
};
|
|
12
|
+
use super::types::ALLOWED_STATUS;
|
|
13
|
+
use super::util::{
|
|
14
|
+
is_id, is_iso_datetime, require_string, require_string_array, require_string_array_allow_empty,
|
|
15
|
+
};
|
|
16
|
+
pub(super) fn validate_task_state_shape(task_state: &Value, errors: &mut Vec<String>) {
|
|
17
|
+
let Some(object) = task_state.as_object() else {
|
|
18
|
+
errors.push(".naome/task-state.json must be a JSON object.".to_string());
|
|
19
|
+
return;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
match (
|
|
23
|
+
object.get("schema").and_then(Value::as_str),
|
|
24
|
+
object.get("version").and_then(Value::as_i64),
|
|
25
|
+
) {
|
|
26
|
+
(Some("naome.task-state.v1"), Some(1)) | (Some("naome.task-state.v2"), Some(2)) => {}
|
|
27
|
+
_ => errors.push(
|
|
28
|
+
".naome/task-state.json schema/version must be naome.task-state.v1/1 or naome.task-state.v2/2."
|
|
29
|
+
.to_string(),
|
|
30
|
+
),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let status = object.get("status").and_then(Value::as_str);
|
|
34
|
+
if !status.is_some_and(|value| ALLOWED_STATUS.contains(&value)) {
|
|
35
|
+
errors.push(format!(
|
|
36
|
+
".naome/task-state.json status must be one of: {}.",
|
|
37
|
+
ALLOWED_STATUS.join(", ")
|
|
38
|
+
));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if let Some(updated_at) = object.get("updatedAt") {
|
|
42
|
+
if !updated_at.is_null() && !updated_at.as_str().is_some_and(is_iso_datetime) {
|
|
43
|
+
errors.push(
|
|
44
|
+
".naome/task-state.json updatedAt must be an ISO timestamp or null.".to_string(),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub(super) fn validate_idle_state(task_state: &Value, errors: &mut Vec<String>) {
|
|
51
|
+
if !task_state.get("activeTask").is_some_and(Value::is_null) {
|
|
52
|
+
errors.push("idle task state must have activeTask set to null.".to_string());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if !task_state.get("blocker").is_some_and(Value::is_null) {
|
|
56
|
+
errors.push("idle task state must have blocker set to null.".to_string());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub(super) fn validate_active_task(active_task: Option<&Value>, errors: &mut Vec<String>) {
|
|
61
|
+
let Some(active_task) = active_task.and_then(Value::as_object) else {
|
|
62
|
+
errors.push("activeTask must be an object for active task states.".to_string());
|
|
63
|
+
return;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if !active_task
|
|
67
|
+
.get("id")
|
|
68
|
+
.and_then(Value::as_str)
|
|
69
|
+
.is_some_and(is_id)
|
|
70
|
+
{
|
|
71
|
+
errors.push("activeTask.id must be kebab-case lowercase.".to_string());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
require_string(active_task.get("request"), "activeTask.request", errors);
|
|
75
|
+
validate_prompt_record(
|
|
76
|
+
active_task.get("userPrompt"),
|
|
77
|
+
"activeTask.userPrompt",
|
|
78
|
+
errors,
|
|
79
|
+
);
|
|
80
|
+
require_string_array(
|
|
81
|
+
active_task.get("allowedPaths"),
|
|
82
|
+
"activeTask.allowedPaths",
|
|
83
|
+
errors,
|
|
84
|
+
);
|
|
85
|
+
require_string_array(
|
|
86
|
+
active_task.get("declaredChangeTypes"),
|
|
87
|
+
"activeTask.declaredChangeTypes",
|
|
88
|
+
errors,
|
|
89
|
+
);
|
|
90
|
+
require_string_array_allow_empty(
|
|
91
|
+
active_task.get("requiredCheckIds"),
|
|
92
|
+
"activeTask.requiredCheckIds",
|
|
93
|
+
errors,
|
|
94
|
+
);
|
|
95
|
+
validate_control_state_patterns(
|
|
96
|
+
active_task.get("allowedPaths"),
|
|
97
|
+
"activeTask.allowedPaths",
|
|
98
|
+
errors,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
let proof_results = active_task.get("proofResults");
|
|
102
|
+
let proof_batches = active_task.get("proofBatches");
|
|
103
|
+
if proof_results.is_some_and(|value| !value.is_array()) {
|
|
104
|
+
errors.push("activeTask.proofResults must be an array when present.".to_string());
|
|
105
|
+
}
|
|
106
|
+
if proof_batches.is_some_and(|value| !value.is_array()) {
|
|
107
|
+
errors.push("activeTask.proofBatches must be an array when present.".to_string());
|
|
108
|
+
}
|
|
109
|
+
if proof_results.is_none() && proof_batches.is_none() {
|
|
110
|
+
errors.push(
|
|
111
|
+
"activeTask.proofResults or activeTask.proofBatches must be present.".to_string(),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
validate_revisions(active_task.get("revisions"), errors);
|
|
116
|
+
validate_human_review(active_task.get("humanReview"), errors);
|
|
117
|
+
}
|