@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,122 @@
|
|
|
1
|
+
use serde_json::Value;
|
|
2
|
+
|
|
3
|
+
pub(super) const CHECK_KEYS: &[&str] = &[
|
|
4
|
+
"id",
|
|
5
|
+
"command",
|
|
6
|
+
"cwd",
|
|
7
|
+
"purpose",
|
|
8
|
+
"cost",
|
|
9
|
+
"source",
|
|
10
|
+
"evidence",
|
|
11
|
+
"lastVerified",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
pub(super) fn serialize_verification_preserving_order(verification: &Value) -> Option<String> {
|
|
15
|
+
let object = verification.as_object()?;
|
|
16
|
+
let ordered_keys = [
|
|
17
|
+
"schema",
|
|
18
|
+
"version",
|
|
19
|
+
"status",
|
|
20
|
+
"lastUpdated",
|
|
21
|
+
"checks",
|
|
22
|
+
"phases",
|
|
23
|
+
"changeTypes",
|
|
24
|
+
"releaseGates",
|
|
25
|
+
];
|
|
26
|
+
let mut blocks = Vec::new();
|
|
27
|
+
|
|
28
|
+
for key in ordered_keys {
|
|
29
|
+
if let Some(value) = object.get(key) {
|
|
30
|
+
blocks.push(render_property(key, value)?);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (key, value) in object {
|
|
34
|
+
if !ordered_keys.contains(&key.as_str()) {
|
|
35
|
+
blocks.push(render_property(key, value)?);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Some(format!("{{\n{}\n}}", blocks.join(",\n")))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn render_property(key: &str, value: &Value) -> Option<String> {
|
|
43
|
+
let rendered = match key {
|
|
44
|
+
"checks" => render_ordered_array(value, CHECK_KEYS)?,
|
|
45
|
+
"phases" => render_ordered_array(value, &["id", "order", "checkIds"])?,
|
|
46
|
+
"changeTypes" => render_ordered_array(
|
|
47
|
+
value,
|
|
48
|
+
&[
|
|
49
|
+
"id",
|
|
50
|
+
"description",
|
|
51
|
+
"paths",
|
|
52
|
+
"requiredChecks",
|
|
53
|
+
"recommendedChecks",
|
|
54
|
+
"humanReview",
|
|
55
|
+
],
|
|
56
|
+
)?,
|
|
57
|
+
"releaseGates" => render_ordered_array(value, &["checkId", "requiredWhen"])?,
|
|
58
|
+
_ => serde_json::to_string_pretty(value).ok()?,
|
|
59
|
+
};
|
|
60
|
+
render_named_value(key, &rendered, " ")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn render_ordered_array(value: &Value, ordered_keys: &[&str]) -> Option<String> {
|
|
64
|
+
let array = value.as_array()?;
|
|
65
|
+
if array.is_empty() {
|
|
66
|
+
return Some("[]".to_string());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let mut rendered_items = Vec::new();
|
|
70
|
+
for item in array {
|
|
71
|
+
rendered_items.push(render_ordered_object(item, ordered_keys)?);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
Some(format!(
|
|
75
|
+
"[\n{}\n]",
|
|
76
|
+
rendered_items
|
|
77
|
+
.iter()
|
|
78
|
+
.map(|item| indent_block(item, " "))
|
|
79
|
+
.collect::<Vec<_>>()
|
|
80
|
+
.join(",\n")
|
|
81
|
+
))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pub(super) fn render_ordered_object(value: &Value, ordered_keys: &[&str]) -> Option<String> {
|
|
85
|
+
let object = value.as_object()?;
|
|
86
|
+
let mut properties = Vec::new();
|
|
87
|
+
|
|
88
|
+
for key in ordered_keys {
|
|
89
|
+
if let Some(value) = object.get(*key) {
|
|
90
|
+
let rendered = serde_json::to_string_pretty(value).ok()?;
|
|
91
|
+
properties.push(render_named_value(key, &rendered, " ")?);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (key, value) in object {
|
|
95
|
+
if !ordered_keys.contains(&key.as_str()) {
|
|
96
|
+
let rendered = serde_json::to_string_pretty(value).ok()?;
|
|
97
|
+
properties.push(render_named_value(key, &rendered, " ")?);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Some(format!("{{\n{}\n}}", properties.join(",\n")))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn render_named_value(key: &str, rendered: &str, indent: &str) -> Option<String> {
|
|
105
|
+
let mut lines = rendered.lines();
|
|
106
|
+
let first = lines.next()?;
|
|
107
|
+
let mut block = format!("{indent}\"{key}\": {first}");
|
|
108
|
+
for line in lines {
|
|
109
|
+
block.push('\n');
|
|
110
|
+
block.push_str(indent);
|
|
111
|
+
block.push_str(line);
|
|
112
|
+
}
|
|
113
|
+
Some(block)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub(super) fn indent_block(content: &str, indent: &str) -> String {
|
|
117
|
+
content
|
|
118
|
+
.lines()
|
|
119
|
+
.map(|line| format!("{indent}{line}"))
|
|
120
|
+
.collect::<Vec<_>>()
|
|
121
|
+
.join("\n")
|
|
122
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
mod render;
|
|
2
|
+
|
|
1
3
|
use std::collections::HashSet;
|
|
2
4
|
use std::fs;
|
|
3
5
|
use std::path::Path;
|
|
4
6
|
|
|
5
|
-
use serde_json::Value;
|
|
7
|
+
use serde_json::{json, Value};
|
|
6
8
|
|
|
7
9
|
use crate::models::NaomeError;
|
|
10
|
+
use render::{
|
|
11
|
+
indent_block, render_ordered_object, serialize_verification_preserving_order, CHECK_KEYS,
|
|
12
|
+
};
|
|
8
13
|
|
|
9
14
|
pub fn seed_builtin_verification_checks(root: &Path) -> Result<bool, NaomeError> {
|
|
10
15
|
let verification_path = root.join(".naome").join("verification.json");
|
|
@@ -41,21 +46,37 @@ pub fn seed_builtin_verification_checks(root: &Path) -> Result<bool, NaomeError>
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
let wired_change_types = wire_repository_quality_check(verification_object);
|
|
50
|
+
let phases_added = ensure_default_phases(verification_object);
|
|
51
|
+
|
|
52
|
+
if missing_checks.is_empty() && !wired_change_types && !phases_added {
|
|
45
53
|
return Ok(false);
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
let next_content =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
let next_content = if wired_change_types || phases_added {
|
|
57
|
+
let checks = verification_object
|
|
58
|
+
.get_mut("checks")
|
|
59
|
+
.and_then(serde_json::Value::as_array_mut)
|
|
60
|
+
.expect("checks must be an array after insertion");
|
|
61
|
+
for check in &missing_checks {
|
|
62
|
+
checks.push(check.value());
|
|
63
|
+
}
|
|
64
|
+
serialize_verification_preserving_order(&verification)
|
|
65
|
+
.unwrap_or_else(|| original_content.clone())
|
|
66
|
+
} else {
|
|
67
|
+
match append_checks_preserving_layout(&original_content, &missing_checks) {
|
|
68
|
+
Some(content) => content,
|
|
69
|
+
None => {
|
|
70
|
+
let checks = verification_object
|
|
71
|
+
.get_mut("checks")
|
|
72
|
+
.and_then(serde_json::Value::as_array_mut)
|
|
73
|
+
.expect("checks must be an array after insertion");
|
|
74
|
+
for check in &missing_checks {
|
|
75
|
+
checks.push(check.value());
|
|
76
|
+
}
|
|
77
|
+
serde_json::to_string_pretty(&verification)
|
|
78
|
+
.unwrap_or_else(|_| original_content.clone())
|
|
57
79
|
}
|
|
58
|
-
serde_json::to_string_pretty(&verification).unwrap_or_else(|_| original_content.clone())
|
|
59
80
|
}
|
|
60
81
|
};
|
|
61
82
|
|
|
@@ -63,14 +84,133 @@ pub fn seed_builtin_verification_checks(root: &Path) -> Result<bool, NaomeError>
|
|
|
63
84
|
Ok(true)
|
|
64
85
|
}
|
|
65
86
|
|
|
87
|
+
fn ensure_default_phases(verification_object: &mut serde_json::Map<String, Value>) -> bool {
|
|
88
|
+
if verification_object
|
|
89
|
+
.get("phases")
|
|
90
|
+
.and_then(Value::as_array)
|
|
91
|
+
.is_some_and(|phases| !phases.is_empty())
|
|
92
|
+
{
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
verification_object.insert(
|
|
96
|
+
"phases".to_string(),
|
|
97
|
+
Value::Array(
|
|
98
|
+
default_phases()
|
|
99
|
+
.into_iter()
|
|
100
|
+
.map(|phase| phase.value())
|
|
101
|
+
.collect(),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
struct BuiltinPhase {
|
|
108
|
+
id: &'static str,
|
|
109
|
+
order: u32,
|
|
110
|
+
check_ids: &'static [&'static str],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
impl BuiltinPhase {
|
|
114
|
+
fn value(&self) -> Value {
|
|
115
|
+
json!({ "id": self.id, "order": self.order, "checkIds": self.check_ids })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn default_phases() -> Vec<BuiltinPhase> {
|
|
120
|
+
vec![
|
|
121
|
+
BuiltinPhase {
|
|
122
|
+
id: "shape-health",
|
|
123
|
+
order: 10,
|
|
124
|
+
check_ids: &["naome-harness-health", "naome-task-state"],
|
|
125
|
+
},
|
|
126
|
+
BuiltinPhase {
|
|
127
|
+
id: "quality",
|
|
128
|
+
order: 20,
|
|
129
|
+
check_ids: &["repository-quality-check"],
|
|
130
|
+
},
|
|
131
|
+
BuiltinPhase {
|
|
132
|
+
id: "diff-check",
|
|
133
|
+
order: 60,
|
|
134
|
+
check_ids: &["diff-check"],
|
|
135
|
+
},
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn wire_repository_quality_check(verification_object: &mut serde_json::Map<String, Value>) -> bool {
|
|
140
|
+
let Some(change_types) = verification_object
|
|
141
|
+
.get_mut("changeTypes")
|
|
142
|
+
.and_then(Value::as_array_mut)
|
|
143
|
+
else {
|
|
144
|
+
return false;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let mut changed = false;
|
|
148
|
+
for change_type in change_types {
|
|
149
|
+
let Some(change_type_object) = change_type.as_object_mut() else {
|
|
150
|
+
continue;
|
|
151
|
+
};
|
|
152
|
+
if !change_type_object
|
|
153
|
+
.get("paths")
|
|
154
|
+
.is_some_and(|paths| paths.as_array().is_some_and(|paths| !paths.is_empty()))
|
|
155
|
+
{
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
let already_wired = ["requiredChecks", "recommendedChecks"].iter().any(|field| {
|
|
159
|
+
change_type_object
|
|
160
|
+
.get(*field)
|
|
161
|
+
.and_then(Value::as_array)
|
|
162
|
+
.is_some_and(|checks| {
|
|
163
|
+
checks
|
|
164
|
+
.iter()
|
|
165
|
+
.any(|check| check.as_str() == Some("repository-quality-check"))
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
if already_wired {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if !change_type_object
|
|
172
|
+
.get("requiredChecks")
|
|
173
|
+
.is_some_and(Value::is_array)
|
|
174
|
+
{
|
|
175
|
+
change_type_object.insert("requiredChecks".to_string(), Value::Array(Vec::new()));
|
|
176
|
+
}
|
|
177
|
+
let Some(required_checks) = change_type_object
|
|
178
|
+
.get_mut("requiredChecks")
|
|
179
|
+
.and_then(Value::as_array_mut)
|
|
180
|
+
else {
|
|
181
|
+
continue;
|
|
182
|
+
};
|
|
183
|
+
required_checks.push(Value::String("repository-quality-check".to_string()));
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
changed
|
|
188
|
+
}
|
|
189
|
+
|
|
66
190
|
struct BuiltinCheck {
|
|
67
191
|
id: &'static str,
|
|
68
|
-
|
|
192
|
+
command: &'static str,
|
|
193
|
+
purpose: &'static str,
|
|
194
|
+
evidence: &'static [&'static str],
|
|
69
195
|
}
|
|
70
196
|
|
|
71
197
|
impl BuiltinCheck {
|
|
72
198
|
fn value(&self) -> Value {
|
|
73
|
-
|
|
199
|
+
json!({
|
|
200
|
+
"id": self.id,
|
|
201
|
+
"command": self.command,
|
|
202
|
+
"cwd": ".",
|
|
203
|
+
"purpose": self.purpose,
|
|
204
|
+
"cost": "fast",
|
|
205
|
+
"source": "NAOME built-in",
|
|
206
|
+
"evidence": self.evidence,
|
|
207
|
+
"lastVerified": null
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn content(&self) -> String {
|
|
212
|
+
render_ordered_object(&self.value(), CHECK_KEYS)
|
|
213
|
+
.expect("built-in verification check must serialize as JSON")
|
|
74
214
|
}
|
|
75
215
|
}
|
|
76
216
|
|
|
@@ -78,47 +218,33 @@ fn builtin_checks() -> Vec<BuiltinCheck> {
|
|
|
78
218
|
vec![
|
|
79
219
|
BuiltinCheck {
|
|
80
220
|
id: "diff-check",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"cwd": ".",
|
|
85
|
-
"purpose": "Reject whitespace errors in the current diff.",
|
|
86
|
-
"cost": "fast",
|
|
87
|
-
"source": "NAOME built-in",
|
|
88
|
-
"evidence": [],
|
|
89
|
-
"lastVerified": null
|
|
90
|
-
}"#,
|
|
221
|
+
command: "git diff --check",
|
|
222
|
+
purpose: "Reject whitespace errors in the current diff.",
|
|
223
|
+
evidence: &[],
|
|
91
224
|
},
|
|
92
225
|
BuiltinCheck {
|
|
93
226
|
id: "naome-harness-health",
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"cwd": ".",
|
|
98
|
-
"purpose": "Validate the installed NAOME harness before feature work or task completion.",
|
|
99
|
-
"cost": "fast",
|
|
100
|
-
"source": "NAOME built-in",
|
|
101
|
-
"evidence": [
|
|
102
|
-
".naome/bin/check-harness-health.js"
|
|
103
|
-
],
|
|
104
|
-
"lastVerified": null
|
|
105
|
-
}"#,
|
|
227
|
+
command: "node .naome/bin/check-harness-health.js",
|
|
228
|
+
purpose: "Validate the installed NAOME harness before feature work or task completion.",
|
|
229
|
+
evidence: &[".naome/bin/check-harness-health.js"],
|
|
106
230
|
},
|
|
107
231
|
BuiltinCheck {
|
|
108
232
|
id: "naome-task-state",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
233
|
+
command: "node .naome/bin/check-task-state.js",
|
|
234
|
+
purpose: "Validate the NAOME task-state contract for the current repository.",
|
|
235
|
+
evidence: &[
|
|
236
|
+
".naome/bin/check-task-state.js",
|
|
237
|
+
".naome/task-contract.schema.json",
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
BuiltinCheck {
|
|
241
|
+
id: "repository-quality-check",
|
|
242
|
+
command: "node .naome/bin/naome.js quality check --changed",
|
|
243
|
+
purpose: "Validate changed files against deterministic NAOME repository quality rules.",
|
|
244
|
+
evidence: &[
|
|
245
|
+
".naome/repository-quality.json",
|
|
246
|
+
".naome/repository-quality-baseline.json",
|
|
247
|
+
],
|
|
122
248
|
},
|
|
123
249
|
]
|
|
124
250
|
}
|
|
@@ -143,7 +269,7 @@ fn append_checks_preserving_layout(
|
|
|
143
269
|
if index > 0 {
|
|
144
270
|
insertion.push_str(",\n");
|
|
145
271
|
}
|
|
146
|
-
insertion.push_str(&indent_block(check.content, &item_indent));
|
|
272
|
+
insertion.push_str(&indent_block(&check.content(), &item_indent));
|
|
147
273
|
}
|
|
148
274
|
|
|
149
275
|
insertion.push('\n');
|
|
@@ -207,11 +333,3 @@ fn line_indent_before(content: &str, index: usize) -> String {
|
|
|
207
333
|
.take_while(|character| *character == ' ' || *character == '\t')
|
|
208
334
|
.collect()
|
|
209
335
|
}
|
|
210
|
-
|
|
211
|
-
fn indent_block(content: &str, indent: &str) -> String {
|
|
212
|
-
content
|
|
213
|
-
.lines()
|
|
214
|
-
.map(|line| format!("{indent}{line}"))
|
|
215
|
-
.collect::<Vec<_>>()
|
|
216
|
-
.join("\n")
|
|
217
|
-
}
|
|
@@ -22,10 +22,12 @@ const ALLOWED_TOP_LEVEL_KEYS: &[&str] = &[
|
|
|
22
22
|
"status",
|
|
23
23
|
"lastUpdated",
|
|
24
24
|
"checks",
|
|
25
|
+
"phases",
|
|
25
26
|
"changeTypes",
|
|
26
27
|
"releaseGates",
|
|
27
28
|
];
|
|
28
29
|
const MAX_CHECKS: usize = 20;
|
|
30
|
+
const MAX_PHASES: usize = 8;
|
|
29
31
|
const MAX_CHANGE_TYPES: usize = 12;
|
|
30
32
|
const MAX_RELEASE_GATES: usize = 10;
|
|
31
33
|
|
|
@@ -125,6 +127,12 @@ fn validate_contract_shape(contract: &Value, errors: &mut Vec<String>) {
|
|
|
125
127
|
validate_array_limit(release_gates, "releaseGates", MAX_RELEASE_GATES, errors);
|
|
126
128
|
|
|
127
129
|
let check_ids = validate_checks(checks, errors);
|
|
130
|
+
if let Some(phases) = object.get("phases").and_then(Value::as_array) {
|
|
131
|
+
validate_array_limit(phases, "phases", MAX_PHASES, errors);
|
|
132
|
+
validate_phases(phases, &check_ids, errors);
|
|
133
|
+
} else if object.get("phases").is_some() {
|
|
134
|
+
errors.push("phases must be an array when present.".to_string());
|
|
135
|
+
}
|
|
128
136
|
validate_change_types(change_types, &check_ids, errors);
|
|
129
137
|
validate_release_gates(release_gates, &check_ids, errors);
|
|
130
138
|
|
|
@@ -143,6 +151,26 @@ fn validate_contract_shape(contract: &Value, errors: &mut Vec<String>) {
|
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
fn validate_phases(phases: &[Value], check_ids: &HashSet<String>, errors: &mut Vec<String>) {
|
|
155
|
+
let mut phase_ids = HashSet::new();
|
|
156
|
+
for (index, phase) in phases.iter().enumerate() {
|
|
157
|
+
let prefix = format!("phases[{index}]");
|
|
158
|
+
let Some(object) = phase.as_object() else {
|
|
159
|
+
errors.push(format!("{prefix} must be an object."));
|
|
160
|
+
continue;
|
|
161
|
+
};
|
|
162
|
+
match object.get("id").and_then(Value::as_str) {
|
|
163
|
+
Some(id) if is_id(id) && phase_ids.insert(id.to_string()) => {}
|
|
164
|
+
Some(id) if is_id(id) => errors.push(format!("{prefix}.id duplicates phase id: {id}")),
|
|
165
|
+
_ => errors.push(format!("{prefix}.id must be kebab-case lowercase.")),
|
|
166
|
+
}
|
|
167
|
+
if object.get("order").and_then(Value::as_u64).is_none() {
|
|
168
|
+
errors.push(format!("{prefix}.order must be a non-negative integer."));
|
|
169
|
+
}
|
|
170
|
+
validate_check_reference_array(object, "checkIds", &prefix, check_ids, errors, true);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
146
174
|
fn validate_array<'a>(
|
|
147
175
|
object: &'a serde_json::Map<String, Value>,
|
|
148
176
|
name: &str,
|
|
@@ -327,22 +355,7 @@ fn require_string_array(
|
|
|
327
355
|
prefix: &str,
|
|
328
356
|
errors: &mut Vec<String>,
|
|
329
357
|
) {
|
|
330
|
-
|
|
331
|
-
errors.push(format!(
|
|
332
|
-
"{prefix}.{field} must be a non-empty string array."
|
|
333
|
-
));
|
|
334
|
-
return;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
if values.is_empty()
|
|
338
|
-
|| values
|
|
339
|
-
.iter()
|
|
340
|
-
.any(|value| !value.as_str().is_some_and(|entry| !entry.trim().is_empty()))
|
|
341
|
-
{
|
|
342
|
-
errors.push(format!(
|
|
343
|
-
"{prefix}.{field} must be a non-empty string array."
|
|
344
|
-
));
|
|
345
|
-
}
|
|
358
|
+
require_string_array_shape(object, field, prefix, errors, true);
|
|
346
359
|
}
|
|
347
360
|
|
|
348
361
|
fn require_string_array_allow_empty(
|
|
@@ -350,20 +363,35 @@ fn require_string_array_allow_empty(
|
|
|
350
363
|
field: &str,
|
|
351
364
|
prefix: &str,
|
|
352
365
|
errors: &mut Vec<String>,
|
|
366
|
+
) {
|
|
367
|
+
require_string_array_shape(object, field, prefix, errors, false);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fn require_string_array_shape(
|
|
371
|
+
object: &serde_json::Map<String, Value>,
|
|
372
|
+
field: &str,
|
|
373
|
+
prefix: &str,
|
|
374
|
+
errors: &mut Vec<String>,
|
|
375
|
+
require_non_empty: bool,
|
|
353
376
|
) {
|
|
354
377
|
let Some(values) = object.get(field).and_then(Value::as_array) else {
|
|
355
|
-
errors.push(
|
|
378
|
+
errors.push(string_array_error(prefix, field, require_non_empty));
|
|
356
379
|
return;
|
|
357
380
|
};
|
|
358
381
|
|
|
359
|
-
|
|
382
|
+
let invalid = values
|
|
360
383
|
.iter()
|
|
361
|
-
.any(|value| !value.as_str().is_some_and(|entry| !entry.trim().is_empty()))
|
|
362
|
-
{
|
|
363
|
-
errors.push(
|
|
384
|
+
.any(|value| !value.as_str().is_some_and(|entry| !entry.trim().is_empty()));
|
|
385
|
+
if invalid || (require_non_empty && values.is_empty()) {
|
|
386
|
+
errors.push(string_array_error(prefix, field, require_non_empty));
|
|
364
387
|
}
|
|
365
388
|
}
|
|
366
389
|
|
|
390
|
+
fn string_array_error(prefix: &str, field: &str, require_non_empty: bool) -> String {
|
|
391
|
+
let qualifier = if require_non_empty { "non-empty " } else { "" };
|
|
392
|
+
format!("{prefix}.{field} must be a {qualifier}string array.")
|
|
393
|
+
}
|
|
394
|
+
|
|
367
395
|
fn is_id(value: &str) -> bool {
|
|
368
396
|
let mut chars = value.chars();
|
|
369
397
|
let Some(first) = chars.next() else {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde::Serialize;
|
|
6
|
+
use serde_json::Value;
|
|
7
|
+
|
|
8
|
+
use crate::models::NaomeError;
|
|
9
|
+
|
|
10
|
+
use super::integrity_normalize::integrity_hash;
|
|
11
|
+
use super::integrity_support::refresh_support_files;
|
|
12
|
+
#[cfg(windows)]
|
|
13
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
|
|
14
|
+
#[cfg(not(windows))]
|
|
15
|
+
const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
18
|
+
#[serde(rename_all = "camelCase")]
|
|
19
|
+
pub struct IntegrityRefreshReport {
|
|
20
|
+
pub schema: String,
|
|
21
|
+
pub updated: bool,
|
|
22
|
+
pub changed_paths: Vec<String>,
|
|
23
|
+
pub refreshed_integrity_paths: Vec<String>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn refresh_integrity(root: &Path) -> Result<IntegrityRefreshReport, NaomeError> {
|
|
27
|
+
let manifest_path = root.join(".naome/manifest.json");
|
|
28
|
+
let original_manifest = fs::read_to_string(&manifest_path)?;
|
|
29
|
+
let mut manifest: Value = serde_json::from_str(&original_manifest)?;
|
|
30
|
+
let refresh_paths = refresh_paths(root, &manifest);
|
|
31
|
+
let integrity = compute_integrity(root, &refresh_paths)?;
|
|
32
|
+
let changed_support_files = refresh_support_files(root, &integrity)?;
|
|
33
|
+
set_manifest_integrity(root, &mut manifest, &integrity);
|
|
34
|
+
|
|
35
|
+
let next_manifest = format!("{}\n", serde_json::to_string_pretty(&manifest)?);
|
|
36
|
+
let mut changed_paths = changed_support_files;
|
|
37
|
+
if next_manifest != original_manifest {
|
|
38
|
+
fs::write(&manifest_path, next_manifest)?;
|
|
39
|
+
changed_paths.push(".naome/manifest.json".to_string());
|
|
40
|
+
}
|
|
41
|
+
changed_paths.sort();
|
|
42
|
+
changed_paths.dedup();
|
|
43
|
+
|
|
44
|
+
Ok(IntegrityRefreshReport {
|
|
45
|
+
schema: "naome.integrity-refresh.v1".to_string(),
|
|
46
|
+
updated: !changed_paths.is_empty(),
|
|
47
|
+
changed_paths,
|
|
48
|
+
refreshed_integrity_paths: integrity.keys().cloned().collect(),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn refresh_paths(root: &Path, manifest: &Value) -> Vec<String> {
|
|
53
|
+
let mut paths = BTreeSet::new();
|
|
54
|
+
add_string_array(manifest.get("machineOwned"), &mut paths);
|
|
55
|
+
if let Some(integrity) = manifest.get("integrity").and_then(Value::as_object) {
|
|
56
|
+
paths.extend(integrity.keys().cloned());
|
|
57
|
+
}
|
|
58
|
+
if root.join(NATIVE_BINARY_PATH).is_file() {
|
|
59
|
+
paths.insert(NATIVE_BINARY_PATH.to_string());
|
|
60
|
+
}
|
|
61
|
+
paths
|
|
62
|
+
.into_iter()
|
|
63
|
+
.filter(|path| root.join(path).is_file())
|
|
64
|
+
.collect()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn add_string_array(value: Option<&Value>, paths: &mut BTreeSet<String>) {
|
|
68
|
+
if let Some(values) = value.and_then(Value::as_array) {
|
|
69
|
+
paths.extend(
|
|
70
|
+
values
|
|
71
|
+
.iter()
|
|
72
|
+
.filter_map(Value::as_str)
|
|
73
|
+
.map(ToString::to_string),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn compute_integrity(
|
|
79
|
+
root: &Path,
|
|
80
|
+
refresh_paths: &[String],
|
|
81
|
+
) -> Result<BTreeMap<String, String>, NaomeError> {
|
|
82
|
+
let mut integrity = BTreeMap::new();
|
|
83
|
+
for path in refresh_paths {
|
|
84
|
+
let content = fs::read(root.join(path))?;
|
|
85
|
+
integrity.insert(path.clone(), integrity_hash(path, &content));
|
|
86
|
+
}
|
|
87
|
+
Ok(integrity)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn set_manifest_integrity(root: &Path, manifest: &mut Value, integrity: &BTreeMap<String, String>) {
|
|
91
|
+
let machine_owned = manifest
|
|
92
|
+
.get("machineOwned")
|
|
93
|
+
.and_then(Value::as_array)
|
|
94
|
+
.into_iter()
|
|
95
|
+
.flatten()
|
|
96
|
+
.filter_map(Value::as_str)
|
|
97
|
+
.map(ToString::to_string)
|
|
98
|
+
.collect::<BTreeSet<_>>();
|
|
99
|
+
let object = manifest
|
|
100
|
+
.as_object_mut()
|
|
101
|
+
.expect("manifest must be a JSON object after parsing");
|
|
102
|
+
let mut merged = object
|
|
103
|
+
.get("integrity")
|
|
104
|
+
.and_then(Value::as_object)
|
|
105
|
+
.map(|existing| {
|
|
106
|
+
existing
|
|
107
|
+
.iter()
|
|
108
|
+
.filter_map(|(path, hash)| {
|
|
109
|
+
if root.join(path).is_file() || machine_owned.contains(path) {
|
|
110
|
+
hash.as_str()
|
|
111
|
+
.map(|hash| (path.clone(), Value::String(hash.to_string())))
|
|
112
|
+
} else {
|
|
113
|
+
None
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
.collect::<serde_json::Map<String, Value>>()
|
|
117
|
+
})
|
|
118
|
+
.unwrap_or_default();
|
|
119
|
+
for (path, hash) in integrity {
|
|
120
|
+
merged.insert(path.clone(), Value::String(hash.clone()));
|
|
121
|
+
}
|
|
122
|
+
object.insert("integrity".to_string(), Value::Object(merged));
|
|
123
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pub(super) const HEALTH_CHECKER_PATH: &str = ".naome/bin/check-harness-health.js";
|
|
2
|
+
pub(super) const TASK_STATE_CHECKER_PATH: &str = ".naome/bin/check-task-state.js";
|
|
3
|
+
pub(super) const NAOME_COMMAND_PATH: &str = ".naome/bin/naome.js";
|
|
4
|
+
|
|
5
|
+
pub(super) fn integrity_hash(relative_path: &str, content: &[u8]) -> String {
|
|
6
|
+
crate::harness_health::machine_integrity_hash(relative_path, content)
|
|
7
|
+
}
|