@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
@@ -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
+ }