@lamentis/naome 1.2.0 → 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 (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -0,0 +1,125 @@
1
+ mod quality_structure_support;
2
+
3
+ use naome_core::{
4
+ check_repository_quality, explain_repository_structure, route_quality_cleanup, QualityMode,
5
+ };
6
+
7
+ use quality_structure_support::{assert_has, StructureFixture};
8
+
9
+ #[test]
10
+ fn legacy_structure_debt_is_reported_but_not_changed_blocking() {
11
+ let repo = StructureFixture::new("structure-legacy-debt");
12
+ repo.write("random.tmp", "legacy scratch\n");
13
+ repo.commit_all("legacy debt");
14
+
15
+ let changed = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
16
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
17
+
18
+ assert!(changed.ok);
19
+ assert_has(&report, "random.tmp", "root-file-sprawl");
20
+ }
21
+
22
+ #[test]
23
+ fn touched_file_inside_debt_directory_blocks_changed_gate() {
24
+ let repo = StructureFixture::new("structure-touched-debt-dir");
25
+ repo.write_js_package();
26
+ repo.write(
27
+ "src/utils/legacy-helper.js",
28
+ "export function legacy() {\n return 1;\n}\n",
29
+ );
30
+ repo.commit_all("legacy dumping ground");
31
+ repo.write(
32
+ "src/utils/legacy-helper.js",
33
+ "export function legacy() {\n return 2;\n}\n",
34
+ );
35
+
36
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
37
+
38
+ assert_has(
39
+ &report,
40
+ "src/utils/legacy-helper.js",
41
+ "dumping-ground-directory",
42
+ );
43
+ }
44
+
45
+ #[test]
46
+ fn quality_report_and_cleanup_route_include_structure_findings() {
47
+ let repo = StructureFixture::new("structure-report-cleanup");
48
+ repo.write(
49
+ "src/utils/random.js",
50
+ "export function random() {\n return 1;\n}\n",
51
+ );
52
+ repo.commit_all("legacy structure debt");
53
+
54
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
55
+ let route = route_quality_cleanup(repo.path(), "src/utils/random.js").unwrap();
56
+
57
+ assert_has(&report, "src/utils/random.js", "dumping-ground-directory");
58
+ assert!(route.agent_instructions.contains("structure"));
59
+ assert!(route
60
+ .violations
61
+ .iter()
62
+ .any(|violation| violation.check_id == "dumping-ground-directory"));
63
+ }
64
+
65
+ #[test]
66
+ fn wildcard_module_roots_explain_monorepo_module_names() {
67
+ let repo = StructureFixture::new("structure-wildcard-module-root");
68
+ repo.write_structure_config(serde_json::json!({
69
+ "sourceRoots": ["packages/*/src/**"],
70
+ "moduleRoots": ["packages/*/src/**"]
71
+ }));
72
+ repo.write("packages/payments/src/lib.rs", "pub fn charge() {}\n");
73
+
74
+ let explanation =
75
+ explain_repository_structure(repo.path(), "packages/payments/src/lib.rs").unwrap();
76
+
77
+ assert_eq!(explanation.module.as_deref(), Some("payments"));
78
+ }
79
+
80
+ #[test]
81
+ fn disabled_structure_checks_are_not_reported() {
82
+ let repo = StructureFixture::new("structure-disabled-checks");
83
+ repo.write_structure_config(serde_json::json!({
84
+ "allowedRootFiles": [".naomeignore", "package.json", "README.md"],
85
+ "disabledChecks": ["test-source-pairing"]
86
+ }));
87
+ repo.write(
88
+ "src/payment-flow.js",
89
+ "export function paymentFlow() {\n return 1;\n}\n",
90
+ );
91
+
92
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
93
+
94
+ assert!(report.ok, "{:#?}", report.violations);
95
+ }
96
+
97
+ #[test]
98
+ fn directory_role_rules_can_allow_configured_role_mixes() {
99
+ let repo = StructureFixture::new("structure-directory-role-rule");
100
+ repo.write_structure_config(serde_json::json!({
101
+ "directoryRoleRules": [
102
+ {
103
+ "id": "generated-source-client",
104
+ "paths": ["src/generated/**"],
105
+ "allowedRoles": ["generated"],
106
+ "maxRoles": 1
107
+ }
108
+ ]
109
+ }));
110
+ repo.write(
111
+ "src/generated/client.ts",
112
+ "export const generated = true;\n",
113
+ );
114
+
115
+ let report = check_repository_quality(repo.path(), QualityMode::Changed).unwrap();
116
+
117
+ assert!(
118
+ !report
119
+ .violations
120
+ .iter()
121
+ .any(|violation| violation.check_id == "directory-role-mixing"),
122
+ "{:#?}",
123
+ report.violations
124
+ );
125
+ }
@@ -0,0 +1,249 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::sync::atomic::{AtomicU64, Ordering};
5
+
6
+ use naome_core::QualityReport;
7
+ use serde_json::json;
8
+
9
+ static FIXTURE_COUNTER: AtomicU64 = AtomicU64::new(0);
10
+
11
+ pub struct StructureFixture {
12
+ root: PathBuf,
13
+ }
14
+
15
+ #[allow(dead_code)]
16
+ impl StructureFixture {
17
+ pub fn new(name: &str) -> Self {
18
+ let root = std::env::temp_dir().join(format!(
19
+ "naome-{name}-{}-{}",
20
+ std::process::id(),
21
+ FIXTURE_COUNTER.fetch_add(1, Ordering::SeqCst)
22
+ ));
23
+ let _ = fs::remove_dir_all(&root);
24
+ fs::create_dir_all(root.join(".naome")).unwrap();
25
+ fs::write(root.join(".naomeignore"), ".naome/archive/\n").unwrap();
26
+ git(&root, &["init"]);
27
+ git(&root, &["config", "user.email", "test@example.com"]);
28
+ git(&root, &["config", "user.name", "Test User"]);
29
+ Self { root }
30
+ }
31
+
32
+ pub fn path(&self) -> &Path {
33
+ &self.root
34
+ }
35
+
36
+ pub fn write(&self, relative_path: impl AsRef<Path>, content: &str) {
37
+ let destination = self.root.join(relative_path.as_ref());
38
+ let parent = destination.parent().expect("fixture paths are file paths");
39
+ fs::create_dir_all(parent)
40
+ .unwrap_or_else(|error| panic!("create {}: {error}", parent.display()));
41
+ fs::write(&destination, content)
42
+ .unwrap_or_else(|error| panic!("write {}: {error}", destination.display()));
43
+ }
44
+
45
+ pub fn write_js_package(&self) {
46
+ self.write("package.json", "{\"scripts\":{\"test\":\"node --test\"}}\n");
47
+ }
48
+
49
+ pub fn write_quality_config(&self) {
50
+ self.write(
51
+ ".naome/repository-quality.json",
52
+ concat!(
53
+ "{\n",
54
+ " \"schema\": \"naome.repository-quality.v1\",\n",
55
+ " \"version\": 1,\n",
56
+ " \"status\": \"ready\",\n",
57
+ " \"limits\": {\n",
58
+ " \"maxFileLines\": 450,\n",
59
+ " \"maxNewFileLines\": 300,\n",
60
+ " \"maxDiffAddedLines\": 180,\n",
61
+ " \"maxFunctionLines\": 100,\n",
62
+ " \"maxTopLevelSymbols\": 30,\n",
63
+ " \"duplicateBlockLines\": 10,\n",
64
+ " \"nearDuplicateSimilarity\": 0.9\n",
65
+ " },\n",
66
+ " \"enabledAdapters\": [],\n",
67
+ " \"disabledChecks\": [],\n",
68
+ " \"ignoredPaths\": [],\n",
69
+ " \"generatedPaths\": [],\n",
70
+ " \"pathRules\": []\n",
71
+ "}\n"
72
+ ),
73
+ );
74
+ }
75
+
76
+ pub fn write_quality_limits(
77
+ &self,
78
+ max_file_lines: usize,
79
+ max_function_lines: usize,
80
+ duplicate_block_lines: usize,
81
+ ) {
82
+ self.write_quality_config_with_adapters(
83
+ max_file_lines,
84
+ max_function_lines,
85
+ duplicate_block_lines,
86
+ &[],
87
+ );
88
+ }
89
+
90
+ pub fn write_quality_config_with_adapters(
91
+ &self,
92
+ max_file_lines: usize,
93
+ max_function_lines: usize,
94
+ duplicate_block_lines: usize,
95
+ enabled_adapters: &[&str],
96
+ ) {
97
+ self.write(
98
+ ".naome/repository-quality.json",
99
+ &format!(
100
+ "{}\n",
101
+ serde_json::to_string_pretty(&json!({
102
+ "schema": "naome.repository-quality.v1",
103
+ "version": 1,
104
+ "status": "ready",
105
+ "limits": {
106
+ "maxFileLines": max_file_lines,
107
+ "maxNewFileLines": max_file_lines,
108
+ "maxDiffAddedLines": 200,
109
+ "maxFunctionLines": max_function_lines,
110
+ "maxTopLevelSymbols": 30,
111
+ "duplicateBlockLines": duplicate_block_lines,
112
+ "nearDuplicateSimilarity": 0.9
113
+ },
114
+ "enabledAdapters": enabled_adapters,
115
+ "disabledChecks": [],
116
+ "ignoredPaths": [],
117
+ "generatedPaths": [],
118
+ "pathRules": []
119
+ }))
120
+ .unwrap()
121
+ ),
122
+ );
123
+ }
124
+ }
125
+
126
+ #[allow(dead_code)]
127
+ impl StructureFixture {
128
+ pub fn write_structure_config(&self, overrides: serde_json::Value) {
129
+ let mut config = json!({
130
+ "schema": "naome.repository-structure.v1",
131
+ "version": 1,
132
+ "status": "ready",
133
+ "enabledAdapters": [],
134
+ "sourceRoots": ["src/**"],
135
+ "testRoots": ["tests/**", "test/**"],
136
+ "docsRoots": ["docs/**"],
137
+ "generatedRoots": ["**/generated/**"],
138
+ "artifactRoots": ["dist/**", "build/**"],
139
+ "moduleRoots": ["src/**"],
140
+ "allowedRootFiles": ["README.md", "LICENSE", "package.json", "Cargo.toml"],
141
+ "directoryRoleRules": [],
142
+ "layerRules": [],
143
+ "ignoredPaths": [],
144
+ "disabledChecks": [],
145
+ "changedCodePolicy": "block",
146
+ "debtPolicy": "report",
147
+ "limits": {
148
+ "maxDirectoryFiles": 40,
149
+ "maxPathDepth": 10,
150
+ "maxDirectoryRoles": 2,
151
+ "maxDumpingGroundFiles": 8
152
+ }
153
+ });
154
+ merge(&mut config, overrides);
155
+ self.write(
156
+ ".naome/repository-structure.json",
157
+ &format!("{}\n", serde_json::to_string_pretty(&config).unwrap()),
158
+ );
159
+ }
160
+
161
+ pub fn commit_all(&self, message: &str) {
162
+ git(&self.root, &["add", "."]);
163
+ git(&self.root, &["commit", "-m", message]);
164
+ }
165
+
166
+ pub fn add_index_only_path(&self, relative_path: &str, content: &str) {
167
+ let mut hash = Command::new("git")
168
+ .args(["hash-object", "-w", "--stdin"])
169
+ .current_dir(&self.root)
170
+ .stdin(std::process::Stdio::piped())
171
+ .stdout(std::process::Stdio::piped())
172
+ .spawn()
173
+ .unwrap();
174
+ {
175
+ use std::io::Write;
176
+ hash.stdin
177
+ .as_mut()
178
+ .unwrap()
179
+ .write_all(content.as_bytes())
180
+ .unwrap();
181
+ }
182
+ let output = hash.wait_with_output().unwrap();
183
+ assert!(output.status.success());
184
+ let oid = String::from_utf8_lossy(&output.stdout).trim().to_string();
185
+ git(
186
+ &self.root,
187
+ &[
188
+ "update-index",
189
+ "--add",
190
+ "--cacheinfo",
191
+ "100644",
192
+ &oid,
193
+ relative_path,
194
+ ],
195
+ );
196
+ }
197
+ }
198
+
199
+ impl Drop for StructureFixture {
200
+ fn drop(&mut self) {
201
+ let _ = fs::remove_dir_all(&self.root);
202
+ }
203
+ }
204
+
205
+ pub fn assert_has(report: &QualityReport, path: &str, check_id: &str) {
206
+ assert!(
207
+ report
208
+ .violations
209
+ .iter()
210
+ .any(|violation| violation.path == path && violation.check_id == check_id),
211
+ "missing {check_id} for {path}; violations: {:#?}",
212
+ report.violations
213
+ );
214
+ }
215
+
216
+ #[allow(dead_code)]
217
+ fn merge(target: &mut serde_json::Value, overrides: serde_json::Value) {
218
+ let Some(target_object) = target.as_object_mut() else {
219
+ return;
220
+ };
221
+ let Some(overrides_object) = overrides.as_object() else {
222
+ return;
223
+ };
224
+ for (key, value) in overrides_object {
225
+ if let (Some(target_value), Some(_)) = (target_object.get_mut(key), value.as_object()) {
226
+ merge(target_value, value.clone());
227
+ } else {
228
+ target_object.insert(key.clone(), value.clone());
229
+ }
230
+ }
231
+ }
232
+
233
+ fn git(root: &Path, args: &[&str]) {
234
+ let output = Command::new("git")
235
+ .args(args)
236
+ .current_dir(root)
237
+ .output()
238
+ .unwrap();
239
+ if output.status.success() {
240
+ return;
241
+ }
242
+ panic!(
243
+ "git failed in {}\ncommand: git {}\nstdout: {}\nstderr: {}",
244
+ root.display(),
245
+ args.join(" "),
246
+ String::from_utf8_lossy(&output.stdout),
247
+ String::from_utf8_lossy(&output.stderr)
248
+ );
249
+ }
@@ -0,0 +1,16 @@
1
+ #![allow(dead_code, unused_imports)]
2
+
3
+ mod repo;
4
+ mod repo_factories;
5
+ mod repo_helpers;
6
+ mod routes;
7
+ mod verification;
8
+ mod verification_values;
9
+
10
+ pub use repo::TestRepo;
11
+ pub use routes::{
12
+ assert_commit_paths, assert_isolated_worktree_ready, assert_user_diff_committed,
13
+ route_commit_request, route_new_task, route_readme_task, try_route_new_task,
14
+ try_route_readme_task,
15
+ };
16
+ pub use verification_values::{change_type, diff_check, verification_value};
@@ -0,0 +1,113 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::Command;
4
+ use std::time::{SystemTime, UNIX_EPOCH};
5
+
6
+ pub struct TestRepo {
7
+ root: PathBuf,
8
+ }
9
+
10
+ impl TestRepo {
11
+ pub fn new(name: &str) -> Self {
12
+ let nonce = SystemTime::now()
13
+ .duration_since(UNIX_EPOCH)
14
+ .unwrap()
15
+ .as_nanos();
16
+ let root = std::env::temp_dir().join(format!("naome-core-{name}-{nonce}"));
17
+ fs::create_dir_all(root.join(".naome")).unwrap();
18
+ Self { root }
19
+ }
20
+
21
+ pub fn path(&self) -> &Path {
22
+ &self.root
23
+ }
24
+
25
+ pub fn write_naome_json(&self, file_name: &str, value: serde_json::Value) {
26
+ let path = self.root.join(".naome").join(file_name);
27
+ fs::write(
28
+ path,
29
+ format!("{}\n", serde_json::to_string_pretty(&value).unwrap()),
30
+ )
31
+ .unwrap();
32
+ }
33
+
34
+ pub fn write_naome_file(&self, file_name: &str, content: &str) {
35
+ fs::write(self.root.join(".naome").join(file_name), content).unwrap();
36
+ }
37
+
38
+ pub fn read_naome_json(&self, file_name: &str) -> serde_json::Value {
39
+ serde_json::from_str(&fs::read_to_string(self.root.join(".naome").join(file_name)).unwrap())
40
+ .unwrap()
41
+ }
42
+
43
+ pub fn read_naome_file(&self, file_name: &str) -> String {
44
+ fs::read_to_string(self.root.join(".naome").join(file_name)).unwrap()
45
+ }
46
+
47
+ pub fn write_testing_markdown(&self, content: &str) {
48
+ fs::create_dir_all(self.root.join("docs").join("naome")).unwrap();
49
+ fs::write(
50
+ self.root.join("docs").join("naome").join("testing.md"),
51
+ content,
52
+ )
53
+ .unwrap();
54
+ }
55
+
56
+ pub fn write_file(&self, relative_path: &str, content: &str) {
57
+ let path = self.root.join(relative_path);
58
+ if let Some(parent) = path.parent() {
59
+ fs::create_dir_all(parent).unwrap();
60
+ }
61
+ fs::write(path, content).unwrap();
62
+ }
63
+
64
+ pub fn init_git(&self) {
65
+ self.git(&["init"]);
66
+ self.git(&["config", "user.email", "naome@example.com"]);
67
+ self.git(&["config", "user.name", "NAOME Test"]);
68
+ }
69
+
70
+ pub fn git(&self, args: &[&str]) {
71
+ let output = Command::new("git")
72
+ .args(args)
73
+ .current_dir(&self.root)
74
+ .output()
75
+ .unwrap();
76
+ assert!(
77
+ output.status.success(),
78
+ "git {:?} failed\nstdout:\n{}\nstderr:\n{}",
79
+ args,
80
+ String::from_utf8_lossy(&output.stdout),
81
+ String::from_utf8_lossy(&output.stderr)
82
+ );
83
+ }
84
+
85
+ pub fn git_stdout(&self, args: &[&str]) -> String {
86
+ let output = Command::new("git")
87
+ .args(args)
88
+ .current_dir(&self.root)
89
+ .output()
90
+ .unwrap();
91
+ assert!(output.status.success());
92
+ String::from_utf8_lossy(&output.stdout).trim().to_string()
93
+ }
94
+
95
+ pub fn git_status_short(&self) -> String {
96
+ self.git_stdout(&["status", "--short"])
97
+ }
98
+
99
+ pub fn git_status_short_at(root: &Path) -> String {
100
+ let output = Command::new("git")
101
+ .args(["status", "--short"])
102
+ .current_dir(root)
103
+ .output()
104
+ .unwrap();
105
+ assert!(output.status.success());
106
+ String::from_utf8_lossy(&output.stdout).trim().to_string()
107
+ }
108
+
109
+ pub fn commit_all(&self, message: &str) {
110
+ self.git(&["add", "."]);
111
+ self.git(&["commit", "-m", message]);
112
+ }
113
+ }
@@ -0,0 +1,99 @@
1
+ use serde_json::{json, Value};
2
+
3
+ use super::repo::TestRepo;
4
+ use super::verification_values::completed_task_state;
5
+
6
+ impl TestRepo {
7
+ pub fn completed_task_with_diff(name: &str) -> Self {
8
+ let repo = Self::new(name);
9
+ repo.init_git();
10
+ repo.write_file("README.md", "# Baseline\n");
11
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
12
+ repo.git(&["add", "."]);
13
+ repo.git(&["commit", "-m", "baseline"]);
14
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
15
+ repo.write_base_naome_state(completed_task_state(&admission_head));
16
+ repo.git(&["add", ".naome/task-state.json"]);
17
+ repo.git(&["commit", "-m", "task state"]);
18
+ repo.write_file("README.md", "# Changed\n");
19
+ repo
20
+ }
21
+
22
+ pub fn completed_task_with_unrelated_user_edit(name: &str) -> Self {
23
+ let repo = Self::new(name);
24
+ repo.init_git();
25
+ repo.write_file("README.md", "# Baseline\n");
26
+ repo.write_file("USER.md", "user baseline\n");
27
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
28
+ repo.git(&["add", "."]);
29
+ repo.git(&["commit", "-m", "baseline"]);
30
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
31
+ repo.write_base_naome_state(completed_task_state(&admission_head));
32
+ repo.git(&["add", ".naome/task-state.json"]);
33
+ repo.git(&["commit", "-m", "task state"]);
34
+ repo.write_file("README.md", "# Changed\n");
35
+ repo.write_file("USER.md", "user local edit\n");
36
+ repo
37
+ }
38
+
39
+ pub fn completed_task_with_harness_refresh_diff(name: &str) -> Self {
40
+ let repo = Self::completed_task_with_diff(name);
41
+ repo.write_current_harness_refresh();
42
+ repo
43
+ }
44
+
45
+ pub fn completed_task_with_harness_refresh_only(name: &str) -> Self {
46
+ let repo = Self::new(name);
47
+ repo.init_git();
48
+ repo.write_file("README.md", "# Baseline\n");
49
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
50
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
51
+ repo.write_harness_manifest("old");
52
+ repo.commit_all("baseline");
53
+ let admission_head = repo.git_stdout(&["rev-parse", "HEAD"]);
54
+ repo.write_base_naome_state(completed_task_state(&admission_head));
55
+ repo.git(&["add", ".naome/task-state.json"]);
56
+ repo.git(&["commit", "-m", "task state"]);
57
+ repo.write_current_harness_refresh();
58
+ repo
59
+ }
60
+
61
+ pub fn dirty_harness_refresh_repo(name: &str, with_user_edit: bool) -> Self {
62
+ let repo = Self::new(name);
63
+ repo.init_git();
64
+ repo.write_file("README.md", "# Baseline\n");
65
+ repo.write_file("USER.md", "user baseline\n");
66
+ repo.write_file("AGENTS.md", "# Agent Instructions\n\nOld harness.\n");
67
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
68
+ repo.write_harness_manifest("old");
69
+ repo.commit_all("baseline");
70
+ repo.write_current_harness_refresh();
71
+ if with_user_edit {
72
+ repo.write_file("USER.md", "user local edit\n");
73
+ }
74
+ repo
75
+ }
76
+
77
+ pub fn idle_readme(name: &str) -> Self {
78
+ let repo = Self::new(name);
79
+ repo.init_git();
80
+ repo.write_file("README.md", "# Baseline\n");
81
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
82
+ repo.commit_all("baseline");
83
+ repo
84
+ }
85
+
86
+ pub fn readme_quality_repo(
87
+ name: &str,
88
+ extra_checks: Vec<Value>,
89
+ required_checks: Vec<&str>,
90
+ ) -> Self {
91
+ let repo = Self::new(name);
92
+ repo.init_git();
93
+ repo.write_file("README.md", "# Baseline\n");
94
+ repo.write_base_naome_state(json!({ "status": "idle", "activeTask": null }));
95
+ repo.write_readme_quality_verification(extra_checks, required_checks);
96
+ repo.commit_all("baseline");
97
+ repo
98
+ }
99
+ }