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