@lamentis/naome 1.2.1 → 1.3.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 (57) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome.js +16 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +6 -2
  6. package/crates/naome-cli/src/main.rs +35 -23
  7. package/crates/naome-cli/src/quality_commands.rs +230 -11
  8. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  9. package/crates/naome-core/Cargo.toml +1 -1
  10. package/crates/naome-core/src/git.rs +4 -2
  11. package/crates/naome-core/src/install_plan.rs +2 -0
  12. package/crates/naome-core/src/lib.rs +11 -7
  13. package/crates/naome-core/src/quality/baseline.rs +8 -0
  14. package/crates/naome-core/src/quality/cache.rs +153 -0
  15. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  16. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  17. package/crates/naome-core/src/quality/checks.rs +7 -8
  18. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  19. package/crates/naome-core/src/quality/mod.rs +57 -9
  20. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  21. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  22. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  23. package/crates/naome-core/src/quality/scanner.rs +193 -220
  24. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  25. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  26. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  27. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  28. package/crates/naome-core/src/quality/semantic.rs +68 -0
  29. package/crates/naome-core/src/quality/structure/checks/directory.rs +9 -19
  30. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  31. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  32. package/crates/naome-core/src/quality/structure/mod.rs +2 -2
  33. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  34. package/crates/naome-core/src/quality/types.rs +40 -2
  35. package/crates/naome-core/src/route/builtin_checks.rs +1 -15
  36. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  37. package/crates/naome-core/src/workflow/mod.rs +2 -0
  38. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  39. package/crates/naome-core/tests/install_plan.rs +2 -0
  40. package/crates/naome-core/tests/quality.rs +14 -5
  41. package/crates/naome-core/tests/quality_performance.rs +231 -0
  42. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  43. package/crates/naome-core/tests/route_user_diff.rs +10 -6
  44. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  45. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  46. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  47. package/installer/git-boundary.js +1 -0
  48. package/native/darwin-arm64/naome +0 -0
  49. package/native/linux-x64/naome +0 -0
  50. package/package.json +1 -1
  51. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  52. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  53. package/templates/naome-root/.naome/bin/naome.js +11 -4
  54. package/templates/naome-root/.naome/manifest.json +2 -2
  55. package/templates/naome-root/.naomeignore +1 -0
  56. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  57. package/templates/naome-root/docs/naome/repository-quality.md +63 -4
@@ -0,0 +1,144 @@
1
+ use std::fs;
2
+ use std::path::Path;
3
+
4
+ use serde::Serialize;
5
+
6
+ use crate::harness_health::{validate_harness_health, HarnessHealthOptions};
7
+ use crate::models::NaomeError;
8
+ use crate::task_state::{validate_task_state, TaskStateMode, TaskStateOptions};
9
+ use crate::verification_contract::validate_verification_contract;
10
+
11
+ use super::phases::verification_phase_plan;
12
+
13
+ #[derive(Debug, Clone, Serialize)]
14
+ #[serde(rename_all = "camelCase")]
15
+ pub struct DoctorReport {
16
+ pub schema: String,
17
+ pub ok: bool,
18
+ pub harness_health: DoctorSection,
19
+ pub task_state: DoctorSection,
20
+ pub verification: DoctorSection,
21
+ pub repository_quality: RepositoryPolicySection,
22
+ pub repository_structure: RepositoryPolicySection,
23
+ pub recommended_check_ids: Vec<String>,
24
+ pub withheld_check_ids: Vec<String>,
25
+ pub next_action: String,
26
+ }
27
+
28
+ #[derive(Debug, Clone, Serialize)]
29
+ #[serde(rename_all = "camelCase")]
30
+ pub struct DoctorSection {
31
+ pub status: String,
32
+ pub messages: Vec<String>,
33
+ }
34
+
35
+ #[derive(Debug, Clone, Serialize)]
36
+ #[serde(rename_all = "camelCase")]
37
+ pub struct RepositoryPolicySection {
38
+ pub config_present: bool,
39
+ pub baseline_present: bool,
40
+ pub status: String,
41
+ }
42
+
43
+ pub fn doctor_report(root: &Path) -> Result<DoctorReport, NaomeError> {
44
+ let harness_errors = validate_harness_health(
45
+ root,
46
+ HarnessHealthOptions {
47
+ allow_missing_integrity: true,
48
+ allow_missing_archive: true,
49
+ ..HarnessHealthOptions::default()
50
+ },
51
+ )?;
52
+ let task_report = validate_task_state(
53
+ root,
54
+ TaskStateOptions {
55
+ mode: TaskStateMode::State,
56
+ harness_health: None,
57
+ },
58
+ )?;
59
+ let verification_errors = validate_verification_contract(root)?;
60
+ let phase_plan = verification_phase_plan(root, &[])?;
61
+
62
+ let repository_quality = policy_section(
63
+ root,
64
+ ".naome/repository-quality.json",
65
+ ".naome/repository-quality-baseline.json",
66
+ );
67
+ let repository_structure = policy_section(root, ".naome/repository-structure.json", "");
68
+
69
+ let harness_health = section(harness_errors);
70
+ let task_state = section(task_report.errors);
71
+ let verification = section(verification_errors);
72
+ let ok = harness_health.status == "ok"
73
+ && task_state.status == "ok"
74
+ && verification.status == "ok"
75
+ && repository_quality.status == "ok"
76
+ && repository_structure.status == "ok";
77
+ let next_action = next_action(
78
+ &harness_health,
79
+ &task_state,
80
+ &verification,
81
+ &repository_quality,
82
+ &repository_structure,
83
+ );
84
+
85
+ Ok(DoctorReport {
86
+ schema: "naome.doctor.v1".to_string(),
87
+ ok,
88
+ harness_health,
89
+ task_state,
90
+ verification,
91
+ repository_quality,
92
+ repository_structure,
93
+ recommended_check_ids: phase_plan.recommended_check_ids,
94
+ withheld_check_ids: phase_plan.withheld_check_ids,
95
+ next_action,
96
+ })
97
+ }
98
+
99
+ fn section(messages: Vec<String>) -> DoctorSection {
100
+ DoctorSection {
101
+ status: if messages.is_empty() { "ok" } else { "blocked" }.to_string(),
102
+ messages,
103
+ }
104
+ }
105
+
106
+ fn policy_section(root: &Path, config_path: &str, baseline_path: &str) -> RepositoryPolicySection {
107
+ let config_present =
108
+ fs::metadata(root.join(config_path)).is_ok_and(|metadata| metadata.is_file());
109
+ let baseline_present = baseline_path.is_empty()
110
+ || fs::metadata(root.join(baseline_path)).is_ok_and(|metadata| metadata.is_file());
111
+ RepositoryPolicySection {
112
+ config_present,
113
+ baseline_present,
114
+ status: if config_present && baseline_present {
115
+ "ok"
116
+ } else {
117
+ "missing"
118
+ }
119
+ .to_string(),
120
+ }
121
+ }
122
+
123
+ fn next_action(
124
+ harness_health: &DoctorSection,
125
+ task_state: &DoctorSection,
126
+ verification: &DoctorSection,
127
+ repository_quality: &RepositoryPolicySection,
128
+ repository_structure: &RepositoryPolicySection,
129
+ ) -> String {
130
+ if task_state.status != "ok" {
131
+ return "Finish or resolve the active task before starting new work.".to_string();
132
+ }
133
+ if harness_health.status != "ok" {
134
+ return "Repair the NAOME harness before feature work.".to_string();
135
+ }
136
+ if verification.status != "ok" {
137
+ return "Fix .naome/verification.json before relying on gates.".to_string();
138
+ }
139
+ if repository_quality.status != "ok" || repository_structure.status != "ok" {
140
+ return "Run naome quality init or naome sync to seed repository quality policy."
141
+ .to_string();
142
+ }
143
+ "NAOME is ready for task work; follow the recommended verification phase order.".to_string()
144
+ }
@@ -1,3 +1,4 @@
1
+ mod doctor;
1
2
  mod integrity;
2
3
  mod integrity_normalize;
3
4
  mod integrity_support;
@@ -9,6 +10,7 @@ mod policy;
9
10
  mod processes;
10
11
  mod types;
11
12
 
13
+ pub use doctor::{doctor_report, DoctorReport, DoctorSection, RepositoryPolicySection};
12
14
  pub use integrity::{refresh_integrity, IntegrityRefreshReport};
13
15
  pub use mutation::classify_mutations;
14
16
  pub use output::{summarize_command_output, CommandOutputSummary};
@@ -34,13 +34,12 @@ fn mutation_class(path: &str) -> &'static str {
34
34
  {
35
35
  return "local-only repair";
36
36
  }
37
- if path.starts_with("packages/naome/templates/") || path.starts_with(".naome/bin/") {
37
+ if path.starts_with(".naome/bin/") {
38
38
  return "harness template";
39
39
  }
40
40
  if path.ends_with(".tgz")
41
41
  || path.starts_with("dist/")
42
42
  || path.contains("/dist/")
43
- || path.starts_with("packages/naome/native/")
44
43
  || path.starts_with("native/")
45
44
  {
46
45
  return "release artifact";
@@ -28,6 +28,7 @@ fn install_plan_includes_git_exclude_and_untrack_policy() {
28
28
  .gitignore_entries
29
29
  .contains(&".naome/task-journal.jsonl"));
30
30
  assert!(plan.git_exclude_entries.contains(&".naome/archive/"));
31
+ assert!(plan.git_exclude_entries.contains(&".naome/cache/"));
31
32
  assert!(plan.git_exclude_entries.contains(&".naome/bin/naome-rust*"));
32
33
  assert!(plan
33
34
  .git_exclude_entries
@@ -36,6 +37,7 @@ fn install_plan_includes_git_exclude_and_untrack_policy() {
36
37
  .git_exclude_entries
37
38
  .contains(&".naome/bin/check-harness-health.js"));
38
39
  assert!(plan.git_untrack_paths.contains(&".naome/archive"));
40
+ assert!(plan.git_untrack_paths.contains(&".naome/cache"));
39
41
  assert!(plan.git_untrack_paths.contains(&".naome/bin/naome-rust"));
40
42
  assert!(plan
41
43
  .git_untrack_paths
@@ -1,8 +1,9 @@
1
1
  use std::fs;
2
2
 
3
3
  use naome_core::{
4
- check_repository_quality, init_repository_quality, plan_quality_cleanup, route_quality_cleanup,
5
- seed_builtin_verification_checks, QualityMode,
4
+ check_repository_quality, init_repository_quality, init_repository_quality_with_mode,
5
+ plan_quality_cleanup, route_quality_cleanup, seed_builtin_verification_checks, QualityInitMode,
6
+ QualityMode,
6
7
  };
7
8
  use serde_json::Value;
8
9
 
@@ -65,6 +66,7 @@ fn changed_check_blocks_new_duplicate_blocks_against_existing_code() {
65
66
  "export function existing() {\n const alpha = input.alpha;\n const beta = input.beta;\n const total = alpha + beta;\n return total;\n}\n",
66
67
  );
67
68
  repo.commit_all("existing helper");
69
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
68
70
 
69
71
  repo.write(
70
72
  "src/copied.js",
@@ -92,6 +94,7 @@ fn duplicate_blocks_report_one_finding_per_overlapping_region() {
92
94
  "export function existing() {\n const alpha = input.alpha;\n const beta = input.beta;\n const gamma = input.gamma;\n const total = alpha + beta + gamma;\n return total;\n}\n",
93
95
  );
94
96
  repo.commit_all("existing helper");
97
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
95
98
 
96
99
  repo.write(
97
100
  "src/copied.js",
@@ -186,7 +189,7 @@ fn near_duplicate_functions_do_not_compare_containers_with_child_symbols() {
186
189
  }
187
190
 
188
191
  #[test]
189
- fn init_writes_default_config_and_baselines_existing_debt() {
192
+ fn init_writes_default_config_and_marks_baseline_pending() {
190
193
  let repo = QualityFixture::new("quality-init-baseline");
191
194
  let large_file = (0..520)
192
195
  .map(|index| format!("export const value{index} = {index};\n"))
@@ -197,13 +200,19 @@ fn init_writes_default_config_and_baselines_existing_debt() {
197
200
  let init = init_repository_quality(repo.path()).unwrap();
198
201
 
199
202
  assert!(init.config_written);
200
- assert!(init.baseline_written);
203
+ assert!(!init.baseline_written);
204
+ assert!(init.baseline_pending);
201
205
  assert!(repo.path().join(".naome/repository-quality.json").is_file());
202
206
  assert!(repo
203
207
  .path()
204
208
  .join(".naome/repository-quality-baseline.json")
205
209
  .is_file());
206
- assert!(init.baseline_violations > 0);
210
+ assert_eq!(init.baseline_violations, 0);
211
+
212
+ let baseline =
213
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::Baseline).unwrap();
214
+ assert!(baseline.baseline_written);
215
+ assert!(baseline.baseline_violations > 0);
207
216
  }
208
217
 
209
218
  #[test]
@@ -0,0 +1,231 @@
1
+ mod repo_support;
2
+
3
+ use std::fs;
4
+
5
+ use naome_core::{
6
+ check_repository_quality, check_semantic_legacy, init_repository_quality,
7
+ init_repository_quality_with_mode, quality_cache_status, QualityInitMode, QualityMode,
8
+ };
9
+
10
+ use repo_support::TestRepo;
11
+
12
+ #[test]
13
+ fn quality_init_seeds_policy_without_baseline_scan() {
14
+ let repo = quality_repo("quality-init-seed-only");
15
+ repo.write_file("src/large.js", &large_file());
16
+ repo.commit_all("legacy debt");
17
+
18
+ let init = init_repository_quality(repo.path()).unwrap();
19
+
20
+ assert!(init.config_written);
21
+ assert!(init.structure_config_written);
22
+ assert!(!init.baseline_written);
23
+ assert!(init.baseline_pending);
24
+ assert_eq!(init.baseline_violations, 0);
25
+ assert!(repo
26
+ .path()
27
+ .join(".naome/repository-quality-baseline.json")
28
+ .is_file());
29
+ }
30
+
31
+ #[test]
32
+ fn quality_baseline_modes_are_explicit() {
33
+ let repo = quality_repo("quality-init-explicit-baseline");
34
+ repo.write_file("src/large.js", &large_file());
35
+ repo.commit_all("legacy debt");
36
+
37
+ let baseline =
38
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::Baseline).unwrap();
39
+ assert!(baseline.baseline_written);
40
+ assert!(!baseline.baseline_pending);
41
+ assert!(baseline.baseline_violations > 0);
42
+
43
+ let deep =
44
+ init_repository_quality_with_mode(repo.path(), QualityInitMode::DeepBaseline).unwrap();
45
+ assert_eq!(deep.mode, "deep-baseline");
46
+ assert!(deep.baseline_violations >= baseline.baseline_violations);
47
+ }
48
+
49
+ #[test]
50
+ fn changed_fast_scans_only_changed_file_contents() {
51
+ let repo = quality_repo("quality-changed-fast-focused");
52
+ for index in 0..1000 {
53
+ repo.write_file(
54
+ &format!("src/unchanged_{index}.js"),
55
+ "export const value = 1;\n",
56
+ );
57
+ }
58
+ repo.write_file("src/changed.js", "export const before = 1;\n");
59
+ repo.commit_all("large baseline");
60
+ repo.write_file("src/changed.js", "export const after = 2;\n");
61
+
62
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
63
+
64
+ assert_eq!(report.mode, "changed");
65
+ assert_eq!(report.summary.scanned_files, 1);
66
+ assert_eq!(report.summary.scanned_path_count, 1);
67
+ assert!(report.changed_paths.contains(&"src/changed.js".to_string()));
68
+ }
69
+
70
+ #[test]
71
+ fn semantic_changed_check_scans_only_changed_file_contents() {
72
+ let repo = quality_repo("semantic-changed-fast-focused");
73
+ for index in 0..1000 {
74
+ repo.write_file(
75
+ &format!("src/unchanged_{index}.js"),
76
+ &legacy_fixture("oldConfig"),
77
+ );
78
+ }
79
+ repo.write_file("scripts/changed.test.js", "const value = 1;\n");
80
+ repo.commit_all("large baseline");
81
+ repo.write_file("scripts/changed.test.js", &legacy_fixture("newConfig"));
82
+
83
+ let report = check_semantic_legacy(repo.path(), QualityMode::ChangedFast).unwrap();
84
+
85
+ assert_eq!(report.mode, "changed");
86
+ assert_eq!(report.summary.scanned_files, 1);
87
+ }
88
+
89
+ #[test]
90
+ fn second_report_uses_file_analysis_cache() {
91
+ let repo = quality_repo("quality-cache-second-report");
92
+ repo.write_file("src/a.js", "export const value = 1;\n");
93
+ repo.write_file("src/b.js", "export const other = 2;\n");
94
+ repo.commit_all("baseline");
95
+
96
+ let first = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
97
+ let cache = quality_cache_status(repo.path()).unwrap();
98
+ let second = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
99
+
100
+ assert_eq!(first.summary.scanned_files, 2);
101
+ assert!(cache.entry_count >= 2);
102
+ assert_eq!(second.summary.cache_hits, 2);
103
+ assert_eq!(second.summary.cache_misses, 0);
104
+ }
105
+
106
+ #[test]
107
+ fn report_budget_marks_truncated_reports() {
108
+ let repo = quality_repo("quality-report-budget");
109
+ for index in 0..5005 {
110
+ repo.write_file(&format!("src/file_{index}.js"), "export const value = 1;\n");
111
+ }
112
+ repo.commit_all("large baseline");
113
+
114
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
115
+
116
+ assert!(report.summary.truncated);
117
+ assert!(report
118
+ .summary
119
+ .reason_codes
120
+ .contains(&"max_scanned_files".to_string()));
121
+ assert_eq!(report.summary.scanned_files, 5000);
122
+ }
123
+
124
+ #[test]
125
+ fn report_skips_deep_only_duplicate_checks() {
126
+ let repo = quality_repo("quality-deep-only-duplicates");
127
+ repo.write_file("src/a.js", &duplicate_block("one"));
128
+ repo.write_file("src/b.js", &duplicate_block("two"));
129
+ repo.commit_all("duplicate baseline");
130
+
131
+ let report = check_repository_quality(repo.path(), QualityMode::Report).unwrap();
132
+ let deep = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
133
+
134
+ assert!(report
135
+ .summary
136
+ .reason_codes
137
+ .contains(&"deep_checks_skipped".to_string()));
138
+ assert!(!report
139
+ .violations
140
+ .iter()
141
+ .any(|violation| violation.check_id == "duplicate-blocks"));
142
+ assert!(deep
143
+ .violations
144
+ .iter()
145
+ .any(|violation| violation.check_id == "duplicate-blocks"));
146
+ }
147
+
148
+ #[test]
149
+ fn changed_fast_compares_changed_files_to_cached_candidates() {
150
+ let repo = quality_repo("quality-changed-cache-duplicates");
151
+ repo.write_file("src/existing.js", &duplicate_block("existing"));
152
+ repo.write_file("src/new.js", "export const value = 1;\n");
153
+ repo.commit_all("baseline");
154
+ let _ = check_repository_quality(repo.path(), QualityMode::DeepReport).unwrap();
155
+
156
+ repo.write_file("src/new.js", &duplicate_block("newCopy"));
157
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
158
+
159
+ assert!(report.summary.cache_hits >= 1);
160
+ assert!(report.violations.iter().any(|violation| {
161
+ violation.check_id == "duplicate-blocks" && violation.path == "src/new.js"
162
+ }));
163
+ }
164
+
165
+ #[test]
166
+ fn changed_fast_compares_changed_files_to_cold_cache_candidates() {
167
+ let repo = quality_repo("quality-changed-cold-cache-duplicates");
168
+ repo.write_file("src/existing.js", &duplicate_block("existing"));
169
+ repo.write_file("src/new.js", "export const value = 1;\n");
170
+ repo.commit_all("baseline");
171
+
172
+ repo.write_file("src/new.js", &duplicate_block("newCopy"));
173
+ let report = check_repository_quality(repo.path(), QualityMode::ChangedFast).unwrap();
174
+
175
+ assert_eq!(report.summary.cache_hits, 0);
176
+ assert!(report.summary.cache_misses >= 2);
177
+ assert_eq!(report.summary.scanned_files, 1);
178
+ assert!(report.violations.iter().any(|violation| {
179
+ violation.check_id == "duplicate-blocks"
180
+ && violation.path == "src/new.js"
181
+ && violation
182
+ .related_paths
183
+ .contains(&"src/existing.js".to_string())
184
+ }));
185
+ }
186
+
187
+ fn quality_repo(name: &str) -> TestRepo {
188
+ let repo = TestRepo::new(name);
189
+ repo.init_git();
190
+ fs::create_dir_all(repo.path().join(".naome")).unwrap();
191
+ repo
192
+ }
193
+
194
+ fn large_file() -> String {
195
+ (0..520)
196
+ .map(|index| format!("export const value{index} = {index};\n"))
197
+ .collect()
198
+ }
199
+
200
+ fn legacy_fixture(name: &str) -> String {
201
+ format!(
202
+ r#"const {name} = {{
203
+ schema: "naome.performance-fixture.v1",
204
+ version: 1,
205
+ status: "ready",
206
+ checks: [],
207
+ changeTypes: [],
208
+ releaseGates: []
209
+ }};
210
+ "#
211
+ )
212
+ }
213
+
214
+ fn duplicate_block(name: &str) -> String {
215
+ format!(
216
+ r#"export function {name}() {{
217
+ const one = 1;
218
+ const two = 2;
219
+ const three = 3;
220
+ const four = 4;
221
+ const five = 5;
222
+ const six = 6;
223
+ const seven = 7;
224
+ const eight = 8;
225
+ const nine = 9;
226
+ const ten = 10;
227
+ return one + two + three + four + five + six + seven + eight + nine + ten;
228
+ }}
229
+ "#
230
+ )
231
+ }
@@ -62,6 +62,25 @@ fn quality_report_and_cleanup_route_include_structure_findings() {
62
62
  .any(|violation| violation.check_id == "dumping-ground-directory"));
63
63
  }
64
64
 
65
+ #[test]
66
+ fn cleanup_route_gives_specific_test_pairing_instructions() {
67
+ let repo = StructureFixture::new("structure-cleanup-test-pairing");
68
+ repo.write_js_package();
69
+ repo.write(
70
+ "src/billing/invoice.js",
71
+ "export function invoiceTotal() {\n return 1;\n}\n",
72
+ );
73
+
74
+ let route = route_quality_cleanup(repo.path(), "src/billing/invoice.js").unwrap();
75
+
76
+ assert!(route
77
+ .agent_instructions
78
+ .contains("Create or update a nearby or module-matched test"));
79
+ assert!(route
80
+ .required_checks
81
+ .contains(&"naome quality check --changed".to_string()));
82
+ }
83
+
65
84
  #[test]
66
85
  fn wildcard_module_roots_explain_monorepo_module_names() {
67
86
  let repo = StructureFixture::new("structure-wildcard-module-root");
@@ -70,20 +70,24 @@ fn execute_route_runs_repository_quality_check_without_shelling_to_repo_command(
70
70
  }
71
71
 
72
72
  #[test]
73
- fn execute_route_commits_product_diff_with_declared_safe_quality_checks() {
73
+ fn execute_route_refuses_product_named_checks_without_generic_builtin_policy() {
74
74
  let repo = TestRepo::product_quality_repo("route-product-diff-quality-pass");
75
75
  repo.write_file(
76
76
  "packages/naome/crates/naome-core/src/route.rs",
77
77
  "pub fn changed() {}\n",
78
78
  );
79
+ let before_head = repo.git_stdout(&["rev-parse", "HEAD"]);
79
80
 
80
81
  let route = route_commit_request(&repo);
81
82
 
82
- assert_user_diff_committed(&route, &repo);
83
-
84
- let committed_paths = repo.git_stdout(&["show", "--name-only", "--format=", "HEAD"]);
85
- assert!(committed_paths.contains("packages/naome/crates/naome-core/src/route.rs"));
86
- assert!(route.user_message.contains("quality gates passed"));
83
+ assert_quality_gate_blocked(
84
+ &repo,
85
+ &route,
86
+ before_head,
87
+ "packages/naome/crates/naome-core/src/route.rs",
88
+ );
89
+ assert!(route.user_message.contains("installer-tests"));
90
+ assert!(route.user_message.contains("not a built-in safe check"));
87
91
  }
88
92
 
89
93
  #[test]
@@ -0,0 +1,140 @@
1
+ mod repo_support;
2
+
3
+ use naome_core::{check_semantic_legacy, semantic_route_for_finding, QualityMode};
4
+
5
+ use repo_support::TestRepo;
6
+
7
+ #[test]
8
+ fn report_groups_repeated_config_fixture_shapes_across_repository() {
9
+ let repo = semantic_repo("semantic-report-groups");
10
+ repo.write_file("scripts/a.test.js", &fixture_object("qualityConfig"));
11
+ repo.write_file(
12
+ "scripts/b.test.js",
13
+ &fixture_object("repositoryQualityConfig"),
14
+ );
15
+ repo.commit_all("baseline");
16
+
17
+ let report = check_semantic_legacy(repo.path(), QualityMode::DeepReport).unwrap();
18
+
19
+ let finding = report
20
+ .findings
21
+ .iter()
22
+ .find(|finding| finding.kind == "copied-config-object")
23
+ .expect("expected copied config finding");
24
+ assert_eq!(finding.occurrences.len(), 2);
25
+ assert!(finding
26
+ .occurrences
27
+ .iter()
28
+ .any(|occurrence| occurrence.path == "scripts/a.test.js"));
29
+ assert!(finding
30
+ .cleanup_route
31
+ .agent_instructions
32
+ .iter()
33
+ .any(|instruction| instruction.contains("every occurrence")));
34
+ }
35
+
36
+ #[test]
37
+ fn changed_check_blocks_new_inline_legacy_fixture_only() {
38
+ let repo = semantic_repo("semantic-changed-only");
39
+ repo.write_file("scripts/old.test.js", &fixture_object("oldQualityConfig"));
40
+ repo.commit_all("baseline");
41
+ repo.write_file("README.md", "# harmless change\n");
42
+
43
+ let unrelated = check_semantic_legacy(repo.path(), QualityMode::Changed).unwrap();
44
+ assert!(unrelated.ok, "{:#?}", unrelated.findings);
45
+
46
+ repo.write_file("scripts/new.test.js", &fixture_object("newQualityConfig"));
47
+
48
+ let changed = check_semantic_legacy(repo.path(), QualityMode::Changed).unwrap();
49
+ assert!(!changed.ok);
50
+ assert!(changed
51
+ .findings
52
+ .iter()
53
+ .any(|finding| finding.kind == "inline-legacy-fixture"));
54
+ }
55
+
56
+ #[test]
57
+ fn changed_check_allows_shared_fixture_factory_objects() {
58
+ let repo = semantic_repo("semantic-shared-factory");
59
+ repo.commit_all("baseline");
60
+ repo.write_file(
61
+ "scripts/test-support.js",
62
+ &[
63
+ "function verificationContract() {",
64
+ " return {",
65
+ " schema: \"naome.verification.v1\",",
66
+ " version: 1,",
67
+ " status: \"ready\",",
68
+ " checks: [],",
69
+ " changeTypes: [],",
70
+ " releaseGates: []",
71
+ " };",
72
+ "}",
73
+ "",
74
+ ]
75
+ .join("\n"),
76
+ );
77
+
78
+ let report = check_semantic_legacy(repo.path(), QualityMode::Changed).unwrap();
79
+
80
+ assert!(report.ok, "{:#?}", report.findings);
81
+ }
82
+
83
+ #[test]
84
+ fn semantic_route_returns_complete_agent_cleanup_task() {
85
+ let repo = semantic_repo("semantic-route");
86
+ repo.write_file("scripts/a.test.js", &fixture_object("qualityConfig"));
87
+ repo.write_file(
88
+ "scripts/b.test.js",
89
+ &fixture_object("repositoryQualityConfig"),
90
+ );
91
+ repo.commit_all("baseline");
92
+ let report = check_semantic_legacy(repo.path(), QualityMode::DeepReport).unwrap();
93
+ let finding_id = report.findings[0].id.clone();
94
+
95
+ let finding = semantic_route_for_finding(&report, &finding_id).unwrap();
96
+
97
+ assert_eq!(finding.id, finding_id);
98
+ assert!(!finding.cleanup_route.target_suggestion.is_empty());
99
+ assert!(finding
100
+ .cleanup_route
101
+ .required_checks
102
+ .contains(&"naome semantic check --changed".to_string()));
103
+ }
104
+
105
+ fn semantic_repo(name: &str) -> TestRepo {
106
+ let repo = TestRepo::new(name);
107
+ repo.init_git();
108
+ repo.write_file("README.md", "# Semantic fixture\n");
109
+ repo.write_naome_json(
110
+ "repository-quality.json",
111
+ serde_json::json!({"schema":"naome.repository-quality.v1","version":1,"status":"ready","limits":{"maxFileLines":450,"maxNewFileLines":300,"maxDiffAddedLines":180,"maxFunctionLines":100,"maxTopLevelSymbols":30,"duplicateBlockLines":10,"nearDuplicateSimilarity":0.9},"enabledAdapters":[],"disabledChecks":[],"ignoredPaths":[],"generatedPaths":[],"pathRules":[]}),
112
+ );
113
+ repo
114
+ }
115
+
116
+ fn fixture_object(name: &str) -> String {
117
+ [
118
+ format!("const {name} = {{"),
119
+ " schema: \"naome.repository-quality.v1\",".to_string(),
120
+ " version: 1,".to_string(),
121
+ " status: \"ready\",".to_string(),
122
+ " limits: {".to_string(),
123
+ " maxFileLines: 450,".to_string(),
124
+ " maxNewFileLines: 300,".to_string(),
125
+ " maxDiffAddedLines: 180,".to_string(),
126
+ " maxFunctionLines: 100,".to_string(),
127
+ " maxTopLevelSymbols: 30,".to_string(),
128
+ " duplicateBlockLines: 10,".to_string(),
129
+ " nearDuplicateSimilarity: 0.9".to_string(),
130
+ " },".to_string(),
131
+ " enabledAdapters: [],".to_string(),
132
+ " disabledChecks: [],".to_string(),
133
+ " ignoredPaths: [],".to_string(),
134
+ " generatedPaths: [],".to_string(),
135
+ " pathRules: []".to_string(),
136
+ "};".to_string(),
137
+ String::new(),
138
+ ]
139
+ .join("\n")
140
+ }