@lamentis/naome 1.1.2 → 1.2.1

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 (204) 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-node.js +2 -1579
  6. package/bin/naome.js +68 -16
  7. package/crates/naome-cli/Cargo.toml +1 -1
  8. package/crates/naome-cli/src/check_commands.rs +135 -0
  9. package/crates/naome-cli/src/cli_args.rs +5 -0
  10. package/crates/naome-cli/src/dispatcher.rs +37 -0
  11. package/crates/naome-cli/src/install_bridge.rs +83 -0
  12. package/crates/naome-cli/src/main.rs +60 -341
  13. package/crates/naome-cli/src/prompt_commands.rs +68 -0
  14. package/crates/naome-cli/src/quality_commands.rs +229 -0
  15. package/crates/naome-cli/src/simple_commands.rs +53 -0
  16. package/crates/naome-cli/src/workflow_commands.rs +153 -0
  17. package/crates/naome-core/Cargo.toml +1 -1
  18. package/crates/naome-core/src/decision/checks.rs +64 -0
  19. package/crates/naome-core/src/decision/idle.rs +67 -0
  20. package/crates/naome-core/src/decision/json.rs +36 -0
  21. package/crates/naome-core/src/decision/states.rs +165 -0
  22. package/crates/naome-core/src/decision.rs +131 -353
  23. package/crates/naome-core/src/harness_health/integrity.rs +96 -0
  24. package/crates/naome-core/src/harness_health.rs +14 -126
  25. package/crates/naome-core/src/install_plan.rs +5 -0
  26. package/crates/naome-core/src/intent/classifier.rs +171 -0
  27. package/crates/naome-core/src/intent/envelope.rs +108 -0
  28. package/crates/naome-core/src/intent/legacy.rs +138 -0
  29. package/crates/naome-core/src/intent/legacy_response.rs +76 -0
  30. package/crates/naome-core/src/intent/model.rs +71 -0
  31. package/crates/naome-core/src/intent/patterns.rs +170 -0
  32. package/crates/naome-core/src/intent/resolver.rs +162 -0
  33. package/crates/naome-core/src/intent/resolver_active.rs +17 -0
  34. package/crates/naome-core/src/intent/resolver_baseline.rs +55 -0
  35. package/crates/naome-core/src/intent/resolver_catalog.rs +167 -0
  36. package/crates/naome-core/src/intent/resolver_policy.rs +72 -0
  37. package/crates/naome-core/src/intent/resolver_shared.rs +55 -0
  38. package/crates/naome-core/src/intent/risk.rs +40 -0
  39. package/crates/naome-core/src/intent/segment.rs +170 -0
  40. package/crates/naome-core/src/intent.rs +64 -879
  41. package/crates/naome-core/src/journal.rs +9 -20
  42. package/crates/naome-core/src/lib.rs +15 -0
  43. package/crates/naome-core/src/paths.rs +3 -1
  44. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  45. package/crates/naome-core/src/quality/adapters.rs +131 -0
  46. package/crates/naome-core/src/quality/baseline.rs +75 -0
  47. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +175 -0
  48. package/crates/naome-core/src/quality/checks/near_duplicates.rs +130 -0
  49. package/crates/naome-core/src/quality/checks.rs +228 -0
  50. package/crates/naome-core/src/quality/cleanup.rs +84 -0
  51. package/crates/naome-core/src/quality/config.rs +102 -0
  52. package/crates/naome-core/src/quality/config_support.rs +24 -0
  53. package/crates/naome-core/src/quality/mod.rs +108 -0
  54. package/crates/naome-core/src/quality/scanner/repo_paths.rs +103 -0
  55. package/crates/naome-core/src/quality/scanner.rs +379 -0
  56. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  57. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  58. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  59. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  60. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  61. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  62. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  63. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  64. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  65. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  66. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  67. package/crates/naome-core/src/quality/types.rs +292 -0
  68. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  69. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  70. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  71. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  72. package/crates/naome-core/src/route/context.rs +180 -0
  73. package/crates/naome-core/src/route/execution.rs +96 -0
  74. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  75. package/crates/naome-core/src/route/execution_support.rs +57 -0
  76. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  77. package/crates/naome-core/src/route/git_ops.rs +72 -0
  78. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  79. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  80. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  81. package/crates/naome-core/src/route/worktree.rs +75 -0
  82. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  83. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  84. package/crates/naome-core/src/route.rs +44 -1155
  85. package/crates/naome-core/src/task_state/admission.rs +63 -0
  86. package/crates/naome-core/src/task_state/admission_proof.rs +72 -0
  87. package/crates/naome-core/src/task_state/api.rs +130 -0
  88. package/crates/naome-core/src/task_state/commit_gate.rs +138 -0
  89. package/crates/naome-core/src/task_state/compact_proof.rs +160 -0
  90. package/crates/naome-core/src/task_state/completed_refresh.rs +89 -0
  91. package/crates/naome-core/src/task_state/completion.rs +72 -0
  92. package/crates/naome-core/src/task_state/deleted_paths.rs +47 -0
  93. package/crates/naome-core/src/task_state/diff.rs +95 -0
  94. package/crates/naome-core/src/task_state/evidence.rs +154 -0
  95. package/crates/naome-core/src/task_state/git_io.rs +86 -0
  96. package/crates/naome-core/src/task_state/git_parse.rs +86 -0
  97. package/crates/naome-core/src/task_state/git_refs.rs +37 -0
  98. package/crates/naome-core/src/task_state/human_review_state.rs +31 -0
  99. package/crates/naome-core/src/task_state/mod.rs +38 -0
  100. package/crates/naome-core/src/task_state/process_guard.rs +40 -0
  101. package/crates/naome-core/src/task_state/progress.rs +123 -0
  102. package/crates/naome-core/src/task_state/proof.rs +139 -0
  103. package/crates/naome-core/src/task_state/proof_entry.rs +66 -0
  104. package/crates/naome-core/src/task_state/proof_model.rs +70 -0
  105. package/crates/naome-core/src/task_state/proof_sources.rs +76 -0
  106. package/crates/naome-core/src/task_state/push_gate.rs +49 -0
  107. package/crates/naome-core/src/task_state/reconcile.rs +7 -0
  108. package/crates/naome-core/src/task_state/repair.rs +168 -0
  109. package/crates/naome-core/src/task_state/shape.rs +117 -0
  110. package/crates/naome-core/src/task_state/task_diff_api.rs +170 -0
  111. package/crates/naome-core/src/task_state/task_records.rs +131 -0
  112. package/crates/naome-core/src/task_state/task_references.rs +126 -0
  113. package/crates/naome-core/src/task_state/types.rs +87 -0
  114. package/crates/naome-core/src/task_state/util.rs +137 -0
  115. package/crates/naome-core/src/verification/render.rs +122 -0
  116. package/crates/naome-core/src/verification.rs +177 -58
  117. package/crates/naome-core/src/verification_contract.rs +49 -21
  118. package/crates/naome-core/src/workflow/integrity.rs +123 -0
  119. package/crates/naome-core/src/workflow/integrity_normalize.rs +7 -0
  120. package/crates/naome-core/src/workflow/integrity_support.rs +110 -0
  121. package/crates/naome-core/src/workflow/mod.rs +18 -0
  122. package/crates/naome-core/src/workflow/mutation.rs +68 -0
  123. package/crates/naome-core/src/workflow/output.rs +111 -0
  124. package/crates/naome-core/src/workflow/phase_inference.rs +73 -0
  125. package/crates/naome-core/src/workflow/phases.rs +169 -0
  126. package/crates/naome-core/src/workflow/policy.rs +156 -0
  127. package/crates/naome-core/src/workflow/processes.rs +91 -0
  128. package/crates/naome-core/src/workflow/types.rs +42 -0
  129. package/crates/naome-core/tests/decision.rs +24 -118
  130. package/crates/naome-core/tests/harness_health.rs +5 -0
  131. package/crates/naome-core/tests/intent.rs +97 -792
  132. package/crates/naome-core/tests/intent_support/mod.rs +133 -0
  133. package/crates/naome-core/tests/intent_v2.rs +90 -0
  134. package/crates/naome-core/tests/quality.rs +319 -0
  135. package/crates/naome-core/tests/quality_structure.rs +116 -0
  136. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  137. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  138. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  139. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  140. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  141. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  142. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  143. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  144. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  145. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  146. package/crates/naome-core/tests/route.rs +1 -1476
  147. package/crates/naome-core/tests/route_baseline.rs +86 -0
  148. package/crates/naome-core/tests/route_completion.rs +141 -0
  149. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  150. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  151. package/crates/naome-core/tests/route_worktree.rs +54 -0
  152. package/crates/naome-core/tests/task_state.rs +60 -429
  153. package/crates/naome-core/tests/task_state_compact.rs +110 -0
  154. package/crates/naome-core/tests/task_state_compact_support/mod.rs +5 -0
  155. package/crates/naome-core/tests/task_state_compact_support/repo.rs +130 -0
  156. package/crates/naome-core/tests/task_state_compact_support/states.rs +151 -0
  157. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  158. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  159. package/crates/naome-core/tests/verification.rs +4 -45
  160. package/crates/naome-core/tests/verification_contract.rs +22 -78
  161. package/crates/naome-core/tests/workflow_integrity.rs +85 -0
  162. package/crates/naome-core/tests/workflow_policy.rs +139 -0
  163. package/crates/naome-core/tests/workflow_support/mod.rs +194 -0
  164. package/installer/agents.js +90 -0
  165. package/installer/context.js +67 -0
  166. package/installer/filesystem.js +166 -0
  167. package/installer/flows.js +84 -0
  168. package/installer/git-boundary.js +170 -0
  169. package/installer/git-hook-content.js +36 -0
  170. package/installer/git-hooks.js +134 -0
  171. package/installer/git-local.js +2 -0
  172. package/installer/git-shared.js +35 -0
  173. package/installer/harness-file-ops.js +140 -0
  174. package/installer/harness-files.js +56 -0
  175. package/installer/harness-verification.js +123 -0
  176. package/installer/install-plan.js +66 -0
  177. package/installer/main.js +25 -0
  178. package/installer/manifest-state.js +167 -0
  179. package/installer/native-build.js +24 -0
  180. package/installer/native-format.js +6 -0
  181. package/installer/native.js +162 -0
  182. package/installer/output.js +131 -0
  183. package/installer/version.js +32 -0
  184. package/native/darwin-arm64/naome +0 -0
  185. package/native/linux-x64/naome +0 -0
  186. package/package.json +3 -2
  187. package/templates/naome-root/.naome/bin/check-harness-health.js +66 -85
  188. package/templates/naome-root/.naome/bin/check-task-state.js +9 -10
  189. package/templates/naome-root/.naome/bin/naome.js +51 -76
  190. package/templates/naome-root/.naome/manifest.json +22 -18
  191. package/templates/naome-root/.naome/repository-quality-baseline.json +5 -0
  192. package/templates/naome-root/.naome/repository-quality.json +24 -0
  193. package/templates/naome-root/.naome/repository-structure.json +90 -0
  194. package/templates/naome-root/.naome/task-contract.schema.json +93 -11
  195. package/templates/naome-root/.naome/upgrade-state.json +1 -1
  196. package/templates/naome-root/.naome/verification.json +38 -0
  197. package/templates/naome-root/AGENTS.md +3 -0
  198. package/templates/naome-root/docs/naome/agent-workflow.md +25 -12
  199. package/templates/naome-root/docs/naome/execution.md +25 -21
  200. package/templates/naome-root/docs/naome/index.md +5 -3
  201. package/templates/naome-root/docs/naome/repository-quality.md +46 -0
  202. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  203. package/templates/naome-root/docs/naome/testing.md +13 -0
  204. package/crates/naome-core/src/task_state.rs +0 -2210
@@ -0,0 +1,126 @@
1
+ use std::collections::HashSet;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(super) use super::admission_proof::validate_admission_proof;
9
+ use super::git_io::read_git_head;
10
+ use super::proof::validate_proof_result_entries;
11
+ use super::util::{joined_strings, read_json};
12
+ pub(super) fn validate_pending_upgrade(
13
+ _task_state: &Value,
14
+ root: &Path,
15
+ errors: &mut Vec<String>,
16
+ ) -> Result<(), NaomeError> {
17
+ if !root.join(".naome/upgrade-state.json").exists() {
18
+ return Ok(());
19
+ }
20
+
21
+ let Some(upgrade_state) = read_json(root, ".naome/upgrade-state.json", errors)? else {
22
+ return Ok(());
23
+ };
24
+
25
+ if upgrade_state.get("status").and_then(Value::as_str) == Some("needs_agent_upgrade") {
26
+ let pending = joined_strings(upgrade_state.get("pending"), "unknown");
27
+ errors.push(format!(
28
+ "NAOME upgrade is pending. Finish docs/naome/upgrade.md before feature work. Pending: {pending}"
29
+ ));
30
+ }
31
+
32
+ Ok(())
33
+ }
34
+
35
+ pub(super) fn validate_active_task_references(
36
+ active_task: Option<&Value>,
37
+ root: &Path,
38
+ errors: &mut Vec<String>,
39
+ status: Option<&str>,
40
+ ) -> Result<(), NaomeError> {
41
+ let Some(active_task) = active_task else {
42
+ return Ok(());
43
+ };
44
+
45
+ validate_admission_proof(active_task.get("admission"), root, errors)?;
46
+ validate_external_git_reconciliation(active_task, status, root, errors)?;
47
+ let check_ids = read_verification_check_ids(root, errors)?;
48
+ validate_required_check_ids(active_task, &check_ids, errors);
49
+ validate_proof_result_entries(active_task, &check_ids, root, errors)?;
50
+ Ok(())
51
+ }
52
+
53
+ pub(super) fn validate_external_git_reconciliation(
54
+ active_task: &Value,
55
+ status: Option<&str>,
56
+ root: &Path,
57
+ errors: &mut Vec<String>,
58
+ ) -> Result<(), NaomeError> {
59
+ if status == Some("complete") {
60
+ return Ok(());
61
+ }
62
+
63
+ let Some(admission_head) = active_task
64
+ .get("admission")
65
+ .and_then(|admission| admission.get("gitHead"))
66
+ .and_then(Value::as_str)
67
+ .filter(|head| !head.trim().is_empty())
68
+ else {
69
+ return Ok(());
70
+ };
71
+
72
+ let Some(current_head) = read_git_head(root)? else {
73
+ return Ok(());
74
+ };
75
+
76
+ if current_head != admission_head {
77
+ errors.push(format!("Task git HEAD changed after admission from {admission_head} to {current_head}. Reconcile external git work before continuing. Human options: mark_task_complete_from_git, reopen_task_revision, recover_current_diff, cancel_task_state."));
78
+ }
79
+
80
+ Ok(())
81
+ }
82
+
83
+ pub(super) fn read_verification_check_ids(
84
+ root: &Path,
85
+ errors: &mut Vec<String>,
86
+ ) -> Result<HashSet<String>, NaomeError> {
87
+ let mut check_ids = HashSet::new();
88
+ let Some(verification) = read_json(root, ".naome/verification.json", errors)? else {
89
+ return Ok(check_ids);
90
+ };
91
+
92
+ if let Some(checks) = verification.get("checks").and_then(Value::as_array) {
93
+ for check in checks {
94
+ if let Some(id) = check.get("id").and_then(Value::as_str) {
95
+ if !id.trim().is_empty() {
96
+ check_ids.insert(id.to_string());
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ Ok(check_ids)
103
+ }
104
+
105
+ pub(super) fn validate_required_check_ids(
106
+ active_task: &Value,
107
+ check_ids: &HashSet<String>,
108
+ errors: &mut Vec<String>,
109
+ ) {
110
+ let Some(required_check_ids) = active_task
111
+ .get("requiredCheckIds")
112
+ .and_then(Value::as_array)
113
+ else {
114
+ return;
115
+ };
116
+
117
+ for check_id in required_check_ids {
118
+ if let Some(check_id) = check_id.as_str().filter(|value| !value.trim().is_empty()) {
119
+ if !check_ids.contains(check_id) {
120
+ errors.push(format!(
121
+ "activeTask.requiredCheckIds unknown check id: {check_id}"
122
+ ));
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,87 @@
1
+ use crate::harness_health::HarnessHealthOptions;
2
+
3
+ pub(super) const CONTROL_STATE_PATH: &str = ".naome/task-state.json";
4
+ pub(super) const ALLOWED_STATUS: &[&str] = &[
5
+ "idle",
6
+ "planning",
7
+ "implementing",
8
+ "revising",
9
+ "verifying",
10
+ "needs_human_review",
11
+ "blocked",
12
+ "complete",
13
+ ];
14
+ pub(super) const BLOCKING_STATUS: &[&str] = &[
15
+ "planning",
16
+ "implementing",
17
+ "revising",
18
+ "verifying",
19
+ "needs_human_review",
20
+ "blocked",
21
+ ];
22
+ pub(super) const ALLOWED_EVIDENCE_STATUS: &[&str] = &["added", "modified", "deleted", "renamed"];
23
+
24
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25
+ pub enum TaskStateMode {
26
+ State,
27
+ Admission,
28
+ Progress,
29
+ CommitGate,
30
+ PushGate,
31
+ }
32
+
33
+ #[derive(Debug, Clone)]
34
+ pub struct TaskStateOptions {
35
+ pub mode: TaskStateMode,
36
+ pub harness_health: Option<HarnessHealthOptions>,
37
+ }
38
+
39
+ impl Default for TaskStateOptions {
40
+ fn default() -> Self {
41
+ Self {
42
+ mode: TaskStateMode::State,
43
+ harness_health: None,
44
+ }
45
+ }
46
+ }
47
+
48
+ #[derive(Debug, Clone, PartialEq, Eq)]
49
+ pub struct TaskStateReport {
50
+ pub errors: Vec<String>,
51
+ pub notices: Vec<String>,
52
+ }
53
+
54
+ #[derive(Debug, Clone)]
55
+ pub(super) struct ChangedEntry {
56
+ pub(super) path: String,
57
+ pub(super) status: String,
58
+ }
59
+
60
+ #[derive(Debug, Clone, PartialEq, Eq)]
61
+ pub struct CompletedTaskHarnessRefreshDiff {
62
+ pub harness_paths: Vec<String>,
63
+ pub task_paths: Vec<String>,
64
+ }
65
+
66
+ #[derive(Debug, Clone, PartialEq, Eq)]
67
+ pub struct HarnessRefreshWithUnrelatedDiff {
68
+ pub harness_paths: Vec<String>,
69
+ pub unrelated_paths: Vec<String>,
70
+ }
71
+
72
+ #[derive(Debug, Clone, PartialEq, Eq)]
73
+ pub struct HarnessRefreshDiff {
74
+ pub harness_paths: Vec<String>,
75
+ pub unrelated_paths: Vec<String>,
76
+ }
77
+
78
+ #[derive(Debug, Clone, PartialEq, Eq)]
79
+ pub struct CompletedTaskCommitDiff {
80
+ pub task_paths: Vec<String>,
81
+ pub unrelated_paths: Vec<String>,
82
+ }
83
+
84
+ pub(super) struct TaskDiff {
85
+ pub(super) diff_paths: Vec<String>,
86
+ pub(super) outside_paths: Vec<String>,
87
+ }
@@ -0,0 +1,137 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde_json::Value;
5
+
6
+ use crate::models::NaomeError;
7
+
8
+ pub(super) fn matches_any_pattern(path: &str, patterns: &[String]) -> bool {
9
+ crate::paths::matches_any(path, patterns)
10
+ }
11
+ pub(super) fn read_json(
12
+ root: &Path,
13
+ relative_path: &str,
14
+ errors: &mut Vec<String>,
15
+ ) -> Result<Option<Value>, NaomeError> {
16
+ let json_path = root.join(relative_path);
17
+ if !json_path.is_file() {
18
+ errors.push(format!("{relative_path} is missing."));
19
+ return Ok(None);
20
+ }
21
+
22
+ let content = fs::read_to_string(json_path)?;
23
+ match serde_json::from_str::<Value>(&content) {
24
+ Ok(parsed) => Ok(Some(parsed)),
25
+ Err(error) => {
26
+ errors.push(format!("{relative_path} is not valid JSON: {error}"));
27
+ Ok(None)
28
+ }
29
+ }
30
+ }
31
+
32
+ pub(super) fn require_string(value: Option<&Value>, field_name: &str, errors: &mut Vec<String>) {
33
+ if !value
34
+ .and_then(Value::as_str)
35
+ .is_some_and(is_non_empty_string)
36
+ {
37
+ errors.push(format!("{field_name} must be a non-empty string."));
38
+ }
39
+ }
40
+
41
+ pub(super) fn require_string_array(
42
+ value: Option<&Value>,
43
+ field_name: &str,
44
+ errors: &mut Vec<String>,
45
+ ) {
46
+ let Some(values) = value.and_then(Value::as_array) else {
47
+ errors.push(format!("{field_name} must be a non-empty string array."));
48
+ return;
49
+ };
50
+
51
+ if values.is_empty()
52
+ || values
53
+ .iter()
54
+ .any(|entry| !entry.as_str().is_some_and(is_non_empty_string))
55
+ {
56
+ errors.push(format!("{field_name} must be a non-empty string array."));
57
+ }
58
+ }
59
+
60
+ pub(super) fn require_string_array_allow_empty(
61
+ value: Option<&Value>,
62
+ field_name: &str,
63
+ errors: &mut Vec<String>,
64
+ ) {
65
+ let Some(values) = value.and_then(Value::as_array) else {
66
+ errors.push(format!("{field_name} must be a string array."));
67
+ return;
68
+ };
69
+
70
+ if values
71
+ .iter()
72
+ .any(|entry| !entry.as_str().is_some_and(is_non_empty_string))
73
+ {
74
+ errors.push(format!("{field_name} must be a string array."));
75
+ }
76
+ }
77
+
78
+ pub(super) fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
79
+ value.and_then(Value::as_array).and_then(|values| {
80
+ values
81
+ .iter()
82
+ .map(|entry| {
83
+ entry
84
+ .as_str()
85
+ .filter(|value| is_non_empty_string(value))
86
+ .map(ToString::to_string)
87
+ })
88
+ .collect()
89
+ })
90
+ }
91
+
92
+ pub(super) fn joined_strings(value: Option<&Value>, fallback: &str) -> String {
93
+ string_array(value)
94
+ .filter(|values| !values.is_empty())
95
+ .map(|values| values.join(", "))
96
+ .unwrap_or_else(|| fallback.to_string())
97
+ }
98
+
99
+ pub(super) fn is_non_empty_string(value: &str) -> bool {
100
+ !value.trim().is_empty()
101
+ }
102
+
103
+ pub(super) fn is_id(value: &str) -> bool {
104
+ let mut chars = value.chars();
105
+ let Some(first) = chars.next() else {
106
+ return false;
107
+ };
108
+ (first.is_ascii_lowercase() || first.is_ascii_digit())
109
+ && value
110
+ .chars()
111
+ .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
112
+ }
113
+
114
+ pub(super) fn is_iso_datetime(value: &str) -> bool {
115
+ let bytes = value.as_bytes();
116
+ bytes.len() == 24
117
+ && bytes[4] == b'-'
118
+ && bytes[7] == b'-'
119
+ && bytes[10] == b'T'
120
+ && bytes[13] == b':'
121
+ && bytes[16] == b':'
122
+ && bytes[19] == b'.'
123
+ && bytes[23] == b'Z'
124
+ && bytes
125
+ .iter()
126
+ .enumerate()
127
+ .filter(|(index, _)| ![4, 7, 10, 13, 16, 19, 23].contains(index))
128
+ .all(|(_, byte)| byte.is_ascii_digit())
129
+ }
130
+
131
+ pub(super) fn normalize_path(value: impl AsRef<str>) -> String {
132
+ value
133
+ .as_ref()
134
+ .replace('\\', "/")
135
+ .trim_start_matches("./")
136
+ .to_string()
137
+ }
@@ -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
+ }