@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.
Files changed (125) hide show
  1. package/Cargo.lock +2 -2
  2. package/Cargo.toml +1 -1
  3. package/LICENSE +180 -21
  4. package/README.md +49 -6
  5. package/bin/naome.js +54 -16
  6. package/crates/naome-cli/Cargo.toml +1 -1
  7. package/crates/naome-cli/src/check_commands.rs +135 -0
  8. package/crates/naome-cli/src/cli_args.rs +5 -0
  9. package/crates/naome-cli/src/dispatcher.rs +36 -0
  10. package/crates/naome-cli/src/install_bridge.rs +83 -0
  11. package/crates/naome-cli/src/main.rs +57 -341
  12. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  13. package/crates/naome-cli/src/quality_commands.rs +141 -0
  14. package/crates/naome-cli/src/simple_commands.rs +53 -0
  15. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  16. package/crates/naome-core/Cargo.toml +1 -1
  17. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  18. package/crates/naome-core/src/harness_health.rs +14 -126
  19. package/crates/naome-core/src/install_plan.rs +3 -0
  20. package/crates/naome-core/src/intent/classifier.rs +171 -0
  21. package/crates/naome-core/src/intent/envelope.rs +108 -0
  22. package/crates/naome-core/src/intent/legacy.rs +138 -0
  23. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  24. package/crates/naome-core/src/intent/model.rs +71 -0
  25. package/crates/naome-core/src/intent/patterns.rs +170 -0
  26. package/crates/naome-core/src/intent/resolver.rs +162 -0
  27. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  28. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  29. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  30. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  31. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  32. package/crates/naome-core/src/intent/risk.rs +40 -0
  33. package/crates/naome-core/src/intent/segment.rs +170 -0
  34. package/crates/naome-core/src/intent.rs +64 -879
  35. package/crates/naome-core/src/journal.rs +9 -20
  36. package/crates/naome-core/src/lib.rs +13 -0
  37. package/crates/naome-core/src/quality/adapters.rs +178 -0
  38. package/crates/naome-core/src/quality/baseline.rs +75 -0
  39. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  40. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  41. package/crates/naome-core/src/quality/checks.rs +228 -0
  42. package/crates/naome-core/src/quality/cleanup.rs +72 -0
  43. package/crates/naome-core/src/quality/config.rs +109 -0
  44. package/crates/naome-core/src/quality/mod.rs +90 -0
  45. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  46. package/crates/naome-core/src/quality/scanner.rs +367 -0
  47. package/crates/naome-core/src/quality/types.rs +289 -0
  48. package/crates/naome-core/src/route.rs +62 -0
  49. package/crates/naome-core/src/task_state/admission.rs +63 -0
  50. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  51. package/crates/naome-core/src/task_state/api.rs +130 -0
  52. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  53. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  54. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  55. package/crates/naome-core/src/task_state/completion.rs +72 -0
  56. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  57. package/crates/naome-core/src/task_state/diff.rs +95 -0
  58. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  59. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  60. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  61. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  62. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  63. package/crates/naome-core/src/task_state/mod.rs +38 -0
  64. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  65. package/crates/naome-core/src/task_state/progress.rs +123 -0
  66. package/crates/naome-core/src/task_state/proof.rs +139 -0
  67. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  68. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  69. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  70. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  71. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  72. package/crates/naome-core/src/task_state/repair.rs +168 -0
  73. package/crates/naome-core/src/task_state/shape.rs +117 -0
  74. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  75. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  76. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  77. package/crates/naome-core/src/task_state/types.rs +87 -0
  78. package/crates/naome-core/src/task_state/util.rs +137 -0
  79. package/crates/naome-core/src/verification/render.rs +122 -0
  80. package/crates/naome-core/src/verification.rs +176 -58
  81. package/crates/naome-core/src/verification_contract.rs +49 -21
  82. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  83. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  84. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  85. package/crates/naome-core/src/workflow/mod.rs +18 -0
  86. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  87. package/crates/naome-core/src/workflow/output.rs +111 -0
  88. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  89. package/crates/naome-core/src/workflow/phases.rs +169 -0
  90. package/crates/naome-core/src/workflow/policy.rs +156 -0
  91. package/crates/naome-core/src/workflow/processes.rs +91 -0
  92. package/crates/naome-core/src/workflow/types.rs +42 -0
  93. package/crates/naome-core/tests/harness_health.rs +3 -0
  94. package/crates/naome-core/tests/intent.rs +97 -792
  95. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  96. package/crates/naome-core/tests/intent_v2.rs +90 -0
  97. package/crates/naome-core/tests/quality.rs +425 -0
  98. package/crates/naome-core/tests/route.rs +88 -188
  99. package/crates/naome-core/tests/task_state.rs +3 -0
  100. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  101. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  102. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  103. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  104. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  105. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  106. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  107. package/native/darwin-arm64/naome +0 -0
  108. package/native/linux-x64/naome +0 -0
  109. package/package.json +2 -2
  110. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  111. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  112. package/templates/naome-root/.naome/bin/naome.js +34 -63
  113. package/templates/naome-root/.naome/manifest.json +20 -18
  114. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  115. package/templates/naome-root/.naome/repository-quality.json +24 -0
  116. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  117. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  118. package/templates/naome-root/.naome/verification.json +37 -0
  119. package/templates/naome-root/AGENTS.md +3 -0
  120. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  121. package/templates/naome-root/docs/naome/execution.md +25 -21
  122. package/templates/naome-root/docs/naome/index.md +4 -3
  123. package/templates/naome-root/docs/naome/repository-quality.md +43 -0
  124. package/templates/naome-root/docs/naome/testing.md +12 -0
  125. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -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
- if missing_checks.is_empty() {
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 = match append_checks_preserving_layout(&original_content, &missing_checks) {
49
- Some(content) => content,
50
- None => {
51
- let checks = verification_object
52
- .get_mut("checks")
53
- .and_then(serde_json::Value::as_array_mut)
54
- .expect("checks must be an array after insertion");
55
- for check in &missing_checks {
56
- checks.push(check.value());
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
- content: &'static str,
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
- serde_json::from_str(self.content).expect("built-in verification check must be valid JSON")
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
- content: r#"{
82
- "id": "diff-check",
83
- "command": "git diff --check",
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
- content: r#"{
95
- "id": "naome-harness-health",
96
- "command": "node .naome/bin/check-harness-health.js",
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
- content: r#"{
110
- "id": "naome-task-state",
111
- "command": "node .naome/bin/check-task-state.js",
112
- "cwd": ".",
113
- "purpose": "Validate the NAOME task-state contract for the current repository.",
114
- "cost": "fast",
115
- "source": "NAOME built-in",
116
- "evidence": [
117
- ".naome/bin/check-task-state.js",
118
- ".naome/task-contract.schema.json"
119
- ],
120
- "lastVerified": null
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
- let Some(values) = object.get(field).and_then(Value::as_array) else {
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(format!("{prefix}.{field} must be a string array."));
378
+ errors.push(string_array_error(prefix, field, require_non_empty));
356
379
  return;
357
380
  };
358
381
 
359
- if values
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(format!("{prefix}.{field} must be a string array."));
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
+ }
@@ -0,0 +1,110 @@
1
+ use std::collections::BTreeMap;
2
+ use std::fs;
3
+ use std::path::Path;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ use super::integrity_normalize::{
8
+ HEALTH_CHECKER_PATH, NAOME_COMMAND_PATH, TASK_STATE_CHECKER_PATH,
9
+ };
10
+
11
+ #[cfg(windows)]
12
+ const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust.exe";
13
+ #[cfg(not(windows))]
14
+ const NATIVE_BINARY_PATH: &str = ".naome/bin/naome-rust";
15
+
16
+ pub(super) fn refresh_support_files(
17
+ root: &Path,
18
+ integrity: &BTreeMap<String, String>,
19
+ ) -> Result<Vec<String>, NaomeError> {
20
+ let mut changed = Vec::new();
21
+ for path in [HEALTH_CHECKER_PATH, TASK_STATE_CHECKER_PATH] {
22
+ if replace_expected_integrity_block(root, path, integrity)? {
23
+ changed.push(path.to_string());
24
+ }
25
+ }
26
+ if replace_native_integrity(root, integrity)? {
27
+ changed.push(NAOME_COMMAND_PATH.to_string());
28
+ }
29
+ Ok(changed)
30
+ }
31
+
32
+ fn replace_expected_integrity_block(
33
+ root: &Path,
34
+ relative_path: &str,
35
+ integrity: &BTreeMap<String, String>,
36
+ ) -> Result<bool, NaomeError> {
37
+ let path = root.join(relative_path);
38
+ if !path.is_file() {
39
+ return Ok(false);
40
+ }
41
+ let original = fs::read_to_string(&path)?;
42
+ let Some(next) = expected_integrity_replacement(&original, integrity) else {
43
+ return Ok(false);
44
+ };
45
+ if next == original {
46
+ return Ok(false);
47
+ }
48
+ fs::write(path, next)?;
49
+ Ok(true)
50
+ }
51
+
52
+ fn expected_integrity_replacement(
53
+ content: &str,
54
+ integrity: &BTreeMap<String, String>,
55
+ ) -> Option<String> {
56
+ let marker = "const expectedMachineOwnedIntegrity = Object.freeze({\n";
57
+ let start = content.find(marker)?;
58
+ let after_start = start + marker.len();
59
+ let end = after_start + content[after_start..].find("\n});\n")? + "\n});\n".len();
60
+ let replacement = render_expected_integrity_block(integrity);
61
+ let mut next = String::with_capacity(content.len() + replacement.len());
62
+ next.push_str(&content[..start]);
63
+ next.push_str(&replacement);
64
+ next.push_str(&content[end..]);
65
+ Some(next)
66
+ }
67
+
68
+ fn render_expected_integrity_block(integrity: &BTreeMap<String, String>) -> String {
69
+ let body = integrity
70
+ .iter()
71
+ .map(|(path, hash)| format!(" {path:?}: {hash:?}"))
72
+ .collect::<Vec<_>>()
73
+ .join(",\n");
74
+ format!("const expectedMachineOwnedIntegrity = Object.freeze({{\n{body}\n}});\n")
75
+ }
76
+
77
+ fn replace_native_integrity(
78
+ root: &Path,
79
+ integrity: &BTreeMap<String, String>,
80
+ ) -> Result<bool, NaomeError> {
81
+ let Some(native_hash) = integrity.get(NATIVE_BINARY_PATH) else {
82
+ return Ok(false);
83
+ };
84
+ let path = root.join(NAOME_COMMAND_PATH);
85
+ if !path.is_file() {
86
+ return Ok(false);
87
+ }
88
+ let original = fs::read_to_string(&path)?;
89
+ let Some(next) = native_integrity_replacement(&original, native_hash) else {
90
+ return Ok(false);
91
+ };
92
+ if next == original {
93
+ return Ok(false);
94
+ }
95
+ fs::write(path, next)?;
96
+ Ok(true)
97
+ }
98
+
99
+ fn native_integrity_replacement(content: &str, native_hash: &str) -> Option<String> {
100
+ let prefix = "const expectedNativeBinaryIntegrity = \"";
101
+ let start = content.find(prefix)?;
102
+ let end = start + content[start..].find(";\n")? + ";\n".len();
103
+ let replacement = format!("const expectedNativeBinaryIntegrity = \"{native_hash}\";\n");
104
+ Some(format!(
105
+ "{}{}{}",
106
+ &content[..start],
107
+ replacement,
108
+ &content[end..]
109
+ ))
110
+ }
@@ -0,0 +1,18 @@
1
+ mod integrity;
2
+ mod integrity_normalize;
3
+ mod integrity_support;
4
+ mod mutation;
5
+ mod output;
6
+ mod phase_inference;
7
+ mod phases;
8
+ mod policy;
9
+ mod processes;
10
+ mod types;
11
+
12
+ pub use integrity::{refresh_integrity, IntegrityRefreshReport};
13
+ pub use mutation::classify_mutations;
14
+ pub use output::{summarize_command_output, CommandOutputSummary};
15
+ pub use phases::{verification_phase_plan, CommandCheckResult, VerificationPhasePlan};
16
+ pub use policy::{safe_rg_args, validate_read_boundaries, validate_search_command};
17
+ pub use processes::{tracked_process_report, ProcessReport};
18
+ pub use types::{MutationClassification, ReadActivity, WorkflowFinding};