@lamentis/naome 1.2.0 → 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 (139) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +108 -47
  3. package/bin/naome-node.js +2 -1579
  4. package/bin/naome.js +34 -5
  5. package/crates/naome-cli/Cargo.toml +1 -1
  6. package/crates/naome-cli/src/dispatcher.rs +7 -2
  7. package/crates/naome-cli/src/main.rs +37 -22
  8. package/crates/naome-cli/src/quality_commands.rs +317 -10
  9. package/crates/naome-cli/src/workflow_commands.rs +21 -1
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/decision/checks.rs +64 -0
  12. package/crates/naome-core/src/decision/idle.rs +67 -0
  13. package/crates/naome-core/src/decision/json.rs +36 -0
  14. package/crates/naome-core/src/decision/states.rs +165 -0
  15. package/crates/naome-core/src/decision.rs +131 -353
  16. package/crates/naome-core/src/git.rs +4 -2
  17. package/crates/naome-core/src/install_plan.rs +4 -0
  18. package/crates/naome-core/src/lib.rs +12 -6
  19. package/crates/naome-core/src/paths.rs +3 -1
  20. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  21. package/crates/naome-core/src/quality/adapters.rs +20 -67
  22. package/crates/naome-core/src/quality/baseline.rs +8 -0
  23. package/crates/naome-core/src/quality/cache.rs +153 -0
  24. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
  25. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  26. package/crates/naome-core/src/quality/checks.rs +7 -8
  27. package/crates/naome-core/src/quality/cleanup.rs +48 -3
  28. package/crates/naome-core/src/quality/config.rs +8 -15
  29. package/crates/naome-core/src/quality/config_support.rs +24 -0
  30. package/crates/naome-core/src/quality/mod.rs +72 -6
  31. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  32. package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
  33. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  34. package/crates/naome-core/src/quality/scanner.rs +200 -215
  35. package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
  36. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  37. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  38. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  39. package/crates/naome-core/src/quality/semantic.rs +68 -0
  40. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  41. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  42. package/crates/naome-core/src/quality/structure/checks/directory.rs +134 -0
  43. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  44. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  45. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  46. package/crates/naome-core/src/quality/structure/classify.rs +146 -0
  47. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  48. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  49. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  50. package/crates/naome-core/src/quality/structure/model.rs +131 -0
  51. package/crates/naome-core/src/quality/types.rs +43 -2
  52. package/crates/naome-core/src/route/builtin_checks.rs +141 -0
  53. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  54. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  55. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  56. package/crates/naome-core/src/route/context.rs +180 -0
  57. package/crates/naome-core/src/route/execution.rs +96 -0
  58. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  59. package/crates/naome-core/src/route/execution_support.rs +57 -0
  60. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  61. package/crates/naome-core/src/route/git_ops.rs +72 -0
  62. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  63. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  64. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  65. package/crates/naome-core/src/route/worktree.rs +75 -0
  66. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  67. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  68. package/crates/naome-core/src/route.rs +44 -1217
  69. package/crates/naome-core/src/verification.rs +1 -0
  70. package/crates/naome-core/src/workflow/doctor.rs +144 -0
  71. package/crates/naome-core/src/workflow/mod.rs +2 -0
  72. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  73. package/crates/naome-core/tests/decision.rs +24 -118
  74. package/crates/naome-core/tests/harness_health.rs +2 -0
  75. package/crates/naome-core/tests/install_plan.rs +2 -0
  76. package/crates/naome-core/tests/quality.rs +26 -123
  77. package/crates/naome-core/tests/quality_performance.rs +231 -0
  78. package/crates/naome-core/tests/quality_structure.rs +116 -0
  79. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  80. package/crates/naome-core/tests/quality_structure_policy.rs +144 -0
  81. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  82. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  83. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  84. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  85. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  86. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  87. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  88. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  89. package/crates/naome-core/tests/route.rs +1 -1376
  90. package/crates/naome-core/tests/route_baseline.rs +86 -0
  91. package/crates/naome-core/tests/route_completion.rs +141 -0
  92. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  93. package/crates/naome-core/tests/route_user_diff.rs +202 -0
  94. package/crates/naome-core/tests/route_worktree.rs +54 -0
  95. package/crates/naome-core/tests/semantic_legacy.rs +140 -0
  96. package/crates/naome-core/tests/task_state.rs +60 -432
  97. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  98. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  99. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  100. package/crates/naome-core/tests/verification.rs +4 -45
  101. package/crates/naome-core/tests/verification_contract.rs +22 -78
  102. package/crates/naome-core/tests/workflow_doctor.rs +24 -0
  103. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  104. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  105. package/installer/agents.js +90 -0
  106. package/installer/context.js +67 -0
  107. package/installer/filesystem.js +166 -0
  108. package/installer/flows.js +84 -0
  109. package/installer/git-boundary.js +171 -0
  110. package/installer/git-hook-content.js +36 -0
  111. package/installer/git-hooks.js +134 -0
  112. package/installer/git-local.js +2 -0
  113. package/installer/git-shared.js +35 -0
  114. package/installer/harness-file-ops.js +140 -0
  115. package/installer/harness-files.js +56 -0
  116. package/installer/harness-verification.js +123 -0
  117. package/installer/install-plan.js +66 -0
  118. package/installer/main.js +25 -0
  119. package/installer/manifest-state.js +167 -0
  120. package/installer/native-build.js +24 -0
  121. package/installer/native-format.js +6 -0
  122. package/installer/native.js +162 -0
  123. package/installer/output.js +131 -0
  124. package/installer/version.js +32 -0
  125. package/native/darwin-arm64/naome +0 -0
  126. package/native/linux-x64/naome +0 -0
  127. package/package.json +2 -1
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +3 -3
  129. package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
  130. package/templates/naome-root/.naome/bin/naome.js +32 -21
  131. package/templates/naome-root/.naome/manifest.json +5 -3
  132. package/templates/naome-root/.naome/repository-structure.json +90 -0
  133. package/templates/naome-root/.naome/verification.json +1 -0
  134. package/templates/naome-root/.naomeignore +1 -0
  135. package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
  136. package/templates/naome-root/docs/naome/index.md +4 -3
  137. package/templates/naome-root/docs/naome/repository-quality.md +66 -4
  138. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  139. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -0,0 +1,89 @@
1
+ use std::collections::HashSet;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ pub(crate) struct RepoSignals<'a> {
6
+ paths: &'a [String],
7
+ }
8
+
9
+ impl<'a> RepoSignals<'a> {
10
+ pub(crate) fn new(paths: &'a [String]) -> Self {
11
+ Self { paths }
12
+ }
13
+
14
+ pub(crate) fn has_manifest(&self, expected: &str) -> bool {
15
+ let nested_suffix = format!("/{expected}");
16
+ self.paths
17
+ .iter()
18
+ .any(|path| path == expected || path.ends_with(&nested_suffix))
19
+ }
20
+
21
+ pub(crate) fn has_extension(&self, extensions: &[&str]) -> bool {
22
+ self.paths
23
+ .iter()
24
+ .any(|path| extensions.iter().any(|extension| path.ends_with(extension)))
25
+ }
26
+ }
27
+
28
+ pub(crate) trait AdapterDescriptor {
29
+ fn id(&self) -> &'static str;
30
+ fn detects(&self, signals: &RepoSignals<'_>) -> bool;
31
+ }
32
+
33
+ pub(crate) fn detected_ids<T: AdapterDescriptor>(paths: &[String], registry: &[T]) -> Vec<String> {
34
+ let signals = RepoSignals::new(paths);
35
+ registry
36
+ .iter()
37
+ .filter(|adapter| adapter.detects(&signals))
38
+ .map(|adapter| adapter.id().to_string())
39
+ .collect()
40
+ }
41
+
42
+ pub(crate) fn find_adapter_by_id<'a, T: AdapterDescriptor>(
43
+ registry: &'a [T],
44
+ id: &str,
45
+ config_path: &str,
46
+ ) -> Result<&'a T, NaomeError> {
47
+ registry
48
+ .iter()
49
+ .find(|adapter| adapter.id() == id)
50
+ .ok_or_else(|| {
51
+ NaomeError::new(format!(
52
+ "{config_path} enabledAdapters contains unknown adapter '{id}'."
53
+ ))
54
+ })
55
+ }
56
+
57
+ pub(crate) fn validate_ids<T: AdapterDescriptor>(
58
+ ids: &[String],
59
+ registry: &[T],
60
+ config_path: &str,
61
+ ) -> Result<(), NaomeError> {
62
+ let mut seen = HashSet::new();
63
+ for adapter_id in ids {
64
+ if !seen.insert(adapter_id) {
65
+ return Err(NaomeError::new(format!(
66
+ "{config_path} enabledAdapters contains duplicate adapter '{adapter_id}'."
67
+ )));
68
+ }
69
+ find_adapter_by_id(registry, adapter_id, config_path)?;
70
+ }
71
+ Ok(())
72
+ }
73
+
74
+ pub(crate) fn extend_unique(target: &mut Vec<String>, values: &[&str]) {
75
+ for value in values {
76
+ if !target.iter().any(|existing| existing == value) {
77
+ target.push((*value).to_string());
78
+ }
79
+ }
80
+ }
81
+
82
+ pub(crate) fn detects_rust_project(signals: &RepoSignals<'_>) -> bool {
83
+ signals.has_manifest("Cargo.toml") || signals.has_extension(&[".rs"])
84
+ }
85
+
86
+ pub(crate) fn detects_javascript_typescript_project(signals: &RepoSignals<'_>) -> bool {
87
+ signals.has_manifest("package.json")
88
+ || signals.has_extension(&[".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"])
89
+ }
@@ -1,9 +1,13 @@
1
- use std::collections::HashSet;
2
-
3
1
  use crate::models::NaomeError;
4
2
 
3
+ use super::adapter_support::{
4
+ detected_ids, detects_javascript_typescript_project, detects_rust_project, extend_unique,
5
+ find_adapter_by_id, validate_ids, AdapterDescriptor, RepoSignals,
6
+ };
5
7
  use super::types::{QualityLimitOverrides, QualityPathRule, RepositoryQualityConfig};
6
8
 
9
+ const CONFIG_PATH: &str = ".naome/repository-quality.json";
10
+
7
11
  pub(crate) struct QualityAdapter {
8
12
  pub id: &'static str,
9
13
  pub generated_paths: &'static [&'static str],
@@ -11,44 +15,28 @@ pub(crate) struct QualityAdapter {
11
15
  path_rules: fn() -> Vec<QualityPathRule>,
12
16
  }
13
17
 
14
- struct RepoSignals<'a> {
15
- paths: &'a [String],
16
- }
17
- impl RepoSignals<'_> {
18
- fn has_manifest(&self, expected: &str) -> bool {
19
- let nested_suffix = format!("/{expected}");
20
- self.paths
21
- .iter()
22
- .any(|path| path == expected || path.ends_with(&nested_suffix))
18
+ impl AdapterDescriptor for QualityAdapter {
19
+ fn id(&self) -> &'static str {
20
+ self.id
23
21
  }
24
22
 
25
- fn has_extension(&self, extensions: &[&str]) -> bool {
26
- self.paths
27
- .iter()
28
- .any(|path| extensions.iter().any(|extension| path.ends_with(extension)))
23
+ fn detects(&self, signals: &RepoSignals<'_>) -> bool {
24
+ (self.detect)(signals)
29
25
  }
30
26
  }
27
+
31
28
  pub(crate) fn detected_adapter_ids(paths: &[String]) -> Vec<String> {
32
- let signals = RepoSignals { paths };
33
- registry()
34
- .iter()
35
- .filter(|adapter| (adapter.detect)(&signals))
36
- .map(|adapter| adapter.id.to_string())
37
- .collect()
29
+ detected_ids(paths, registry())
38
30
  }
31
+
39
32
  pub(crate) fn apply_enabled_adapters(
40
33
  mut config: RepositoryQualityConfig,
41
34
  ) -> Result<RepositoryQualityConfig, NaomeError> {
42
- let mut seen = HashSet::new();
35
+ validate_adapter_ids(&config.enabled_adapters)?;
43
36
  let local_path_rules = std::mem::take(&mut config.path_rules);
44
37
 
45
38
  for adapter_id in config.enabled_adapters.clone() {
46
- if !seen.insert(adapter_id.clone()) {
47
- return Err(NaomeError::new(format!(
48
- ".naome/repository-quality.json enabledAdapters contains duplicate adapter '{adapter_id}'."
49
- )));
50
- }
51
- let adapter = adapter_by_id(&adapter_id)?;
39
+ let adapter = find_adapter_by_id(registry(), &adapter_id, CONFIG_PATH)?;
52
40
  extend_unique(&mut config.generated_paths, adapter.generated_paths);
53
41
  config.path_rules.extend((adapter.path_rules)());
54
42
  }
@@ -56,27 +44,9 @@ pub(crate) fn apply_enabled_adapters(
56
44
  config.path_rules.extend(local_path_rules);
57
45
  Ok(config)
58
46
  }
47
+
59
48
  pub(crate) fn validate_adapter_ids(ids: &[String]) -> Result<(), NaomeError> {
60
- let mut seen = HashSet::new();
61
- for adapter_id in ids {
62
- if !seen.insert(adapter_id) {
63
- return Err(NaomeError::new(format!(
64
- ".naome/repository-quality.json enabledAdapters contains duplicate adapter '{adapter_id}'."
65
- )));
66
- }
67
- adapter_by_id(adapter_id)?;
68
- }
69
- Ok(())
70
- }
71
- fn adapter_by_id(id: &str) -> Result<&'static QualityAdapter, NaomeError> {
72
- registry()
73
- .iter()
74
- .find(|adapter| adapter.id == id)
75
- .ok_or_else(|| {
76
- NaomeError::new(format!(
77
- ".naome/repository-quality.json enabledAdapters contains unknown adapter '{id}'."
78
- ))
79
- })
49
+ validate_ids(ids, registry(), CONFIG_PATH)
80
50
  }
81
51
 
82
52
  fn registry() -> &'static [QualityAdapter] {
@@ -84,27 +54,18 @@ fn registry() -> &'static [QualityAdapter] {
84
54
  QualityAdapter {
85
55
  id: "rust",
86
56
  generated_paths: &[],
87
- detect: detects_rust,
57
+ detect: detects_rust_project,
88
58
  path_rules: rust_path_rules,
89
59
  },
90
60
  QualityAdapter {
91
61
  id: "javascript-typescript",
92
62
  generated_paths: &["coverage/**", "**/coverage/**", ".next/**", "**/.next/**"],
93
- detect: detects_javascript_typescript,
63
+ detect: detects_javascript_typescript_project,
94
64
  path_rules: javascript_typescript_path_rules,
95
65
  },
96
66
  ]
97
67
  }
98
68
 
99
- fn detects_rust(signals: &RepoSignals<'_>) -> bool {
100
- signals.has_manifest("Cargo.toml") || signals.has_extension(&[".rs"])
101
- }
102
-
103
- fn detects_javascript_typescript(signals: &RepoSignals<'_>) -> bool {
104
- signals.has_manifest("package.json")
105
- || signals.has_extension(&[".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"])
106
- }
107
-
108
69
  fn rust_path_rules() -> Vec<QualityPathRule> {
109
70
  vec![path_rule(
110
71
  "rust-tests",
@@ -168,11 +129,3 @@ fn path_rule(
168
129
  fn string_list(values: &[&str]) -> Vec<String> {
169
130
  values.iter().map(|value| (*value).to_string()).collect()
170
131
  }
171
-
172
- fn extend_unique(target: &mut Vec<String>, values: &[&str]) {
173
- for value in values {
174
- if !target.iter().any(|existing| existing == value) {
175
- target.push((*value).to_string());
176
- }
177
- }
178
- }
@@ -73,3 +73,11 @@ pub fn write_baseline(root: &Path, violations: &[QualityViolation]) -> Result<bo
73
73
  }
74
74
  Ok(changed)
75
75
  }
76
+
77
+ pub fn write_empty_baseline_if_missing(root: &Path) -> Result<bool, NaomeError> {
78
+ let path = root.join(BASELINE_RELATIVE_PATH);
79
+ if path.is_file() {
80
+ return Ok(false);
81
+ }
82
+ write_baseline(root, &[])
83
+ }
@@ -0,0 +1,153 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use serde::{Deserialize, Serialize};
5
+ use sha2::{Digest, Sha256};
6
+
7
+ use crate::models::NaomeError;
8
+
9
+ use super::scanner::FileAnalysis;
10
+ use super::types::RepositoryQualityConfig;
11
+
12
+ const CACHE_RELATIVE_PATH: &str = ".naome/cache/quality";
13
+ const CACHE_SCHEMA: &str = "naome.quality-cache-entry.v1";
14
+ const ADAPTER_VERSION: &str = "quality-core-v1";
15
+
16
+ #[derive(Debug, Clone)]
17
+ pub(crate) struct QualityCache {
18
+ root: PathBuf,
19
+ config_hash: String,
20
+ }
21
+
22
+ #[derive(Debug, Clone, Serialize, Deserialize)]
23
+ #[serde(rename_all = "camelCase")]
24
+ struct CacheEntry {
25
+ schema: String,
26
+ naome_version: String,
27
+ config_hash: String,
28
+ adapter_version: String,
29
+ path: String,
30
+ content_hash: String,
31
+ analysis: FileAnalysis,
32
+ }
33
+
34
+ #[derive(Debug, Clone, Serialize)]
35
+ #[serde(rename_all = "camelCase")]
36
+ pub struct QualityCacheStatus {
37
+ pub schema: String,
38
+ pub path: String,
39
+ pub entry_count: usize,
40
+ pub bytes: u64,
41
+ }
42
+
43
+ impl QualityCache {
44
+ pub(crate) fn new(root: &Path, config: &RepositoryQualityConfig) -> Self {
45
+ Self {
46
+ root: root.to_path_buf(),
47
+ config_hash: config_hash(config),
48
+ }
49
+ }
50
+
51
+ pub(crate) fn read(&self, path: &str, content_hash: &str) -> Option<FileAnalysis> {
52
+ let entry = fs::read_to_string(self.entry_path(path, content_hash)).ok()?;
53
+ let entry: CacheEntry = serde_json::from_str(&entry).ok()?;
54
+ if entry.schema == CACHE_SCHEMA
55
+ && entry.naome_version == env!("CARGO_PKG_VERSION")
56
+ && entry.config_hash == self.config_hash
57
+ && entry.adapter_version == ADAPTER_VERSION
58
+ && entry.path == path
59
+ && entry.content_hash == content_hash
60
+ {
61
+ Some(entry.analysis)
62
+ } else {
63
+ None
64
+ }
65
+ }
66
+
67
+ pub(crate) fn write(
68
+ &self,
69
+ path: &str,
70
+ content_hash: &str,
71
+ analysis: &FileAnalysis,
72
+ ) -> Result<(), NaomeError> {
73
+ let cache_path = self.entry_path(path, content_hash);
74
+ if let Some(parent) = cache_path.parent() {
75
+ fs::create_dir_all(parent)?;
76
+ }
77
+ let entry = CacheEntry {
78
+ schema: CACHE_SCHEMA.to_string(),
79
+ naome_version: env!("CARGO_PKG_VERSION").to_string(),
80
+ config_hash: self.config_hash.clone(),
81
+ adapter_version: ADAPTER_VERSION.to_string(),
82
+ path: path.to_string(),
83
+ content_hash: content_hash.to_string(),
84
+ analysis: analysis.clone(),
85
+ };
86
+ let content = format!("{}\n", serde_json::to_string(&entry)?);
87
+ if fs::read_to_string(&cache_path).map_or(true, |current| current != content) {
88
+ fs::write(cache_path, content)?;
89
+ }
90
+ Ok(())
91
+ }
92
+
93
+ fn entry_path(&self, path: &str, content_hash: &str) -> PathBuf {
94
+ self.root
95
+ .join(CACHE_RELATIVE_PATH)
96
+ .join(stable_key(&[
97
+ env!("CARGO_PKG_VERSION"),
98
+ &self.config_hash,
99
+ ADAPTER_VERSION,
100
+ path,
101
+ content_hash,
102
+ ]))
103
+ }
104
+ }
105
+
106
+ pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
107
+ let path = root.join(CACHE_RELATIVE_PATH);
108
+ let mut entry_count = 0;
109
+ let mut bytes = 0;
110
+ if path.is_dir() {
111
+ for entry in fs::read_dir(&path)? {
112
+ let entry = entry?;
113
+ let metadata = entry.metadata()?;
114
+ if metadata.is_file() {
115
+ entry_count += 1;
116
+ bytes += metadata.len();
117
+ }
118
+ }
119
+ }
120
+ Ok(QualityCacheStatus {
121
+ schema: "naome.quality-cache-status.v1".to_string(),
122
+ path: CACHE_RELATIVE_PATH.to_string(),
123
+ entry_count,
124
+ bytes,
125
+ })
126
+ }
127
+
128
+ pub fn clear_quality_cache(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
129
+ let path = root.join(CACHE_RELATIVE_PATH);
130
+ if path.exists() {
131
+ fs::remove_dir_all(&path)?;
132
+ }
133
+ quality_cache_status(root)
134
+ }
135
+
136
+ pub(crate) fn content_hash(content: &str) -> String {
137
+ stable_key(&[content])
138
+ }
139
+
140
+ fn config_hash(config: &RepositoryQualityConfig) -> String {
141
+ serde_json::to_string(config)
142
+ .map(|content| stable_key(&[&content]))
143
+ .unwrap_or_else(|_| stable_key(&["invalid-config"]))
144
+ }
145
+
146
+ fn stable_key(parts: &[&str]) -> String {
147
+ let mut hasher = Sha256::new();
148
+ for part in parts {
149
+ hasher.update(part.as_bytes());
150
+ hasher.update(b"\0");
151
+ }
152
+ format!("{:x}.json", hasher.finalize())
153
+ }
@@ -1,6 +1,8 @@
1
1
  use std::collections::{HashMap, HashSet};
2
2
 
3
- use super::super::scanner::{stable_fingerprint, QualityContext};
3
+ use sha2::{Digest, Sha256};
4
+
5
+ use super::super::scanner::{stable_fingerprint, NormalizedLine, QualityContext};
4
6
  use super::super::types::QualityViolation;
5
7
  use super::{is_code_like_path, QualityCheck};
6
8
 
@@ -12,22 +14,23 @@ impl QualityCheck for DuplicateBlockCheck {
12
14
  }
13
15
 
14
16
  fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
17
+ if context.mode == super::super::types::QualityMode::Report {
18
+ return;
19
+ }
15
20
  let mut occurrences: HashMap<String, Vec<DuplicateOccurrence>> = HashMap::new();
16
- for file in context.files.iter().filter(|file| {
17
- is_code_like_path(&file.path)
18
- && context.config.check_enabled_for_path(self.id(), &file.path)
19
- }) {
21
+ for file in context
22
+ .comparison_candidate_files()
23
+ .filter(|file| {
24
+ is_code_like_path(&file.path)
25
+ && context.config.check_enabled_for_path(self.id(), &file.path)
26
+ })
27
+ {
20
28
  let window = context.limits_for(&file.path).duplicate_block_lines;
21
29
  if file.normalized_lines.len() < window {
22
30
  continue;
23
31
  }
24
32
  for lines in file.normalized_lines.windows(window) {
25
- let joined = lines
26
- .iter()
27
- .map(|line| line.value.as_str())
28
- .collect::<Vec<_>>()
29
- .join("\n");
30
- let fingerprint = stable_fingerprint(&[self.id(), &joined]);
33
+ let fingerprint = window_fingerprint(self.id(), lines);
31
34
  occurrences
32
35
  .entry(fingerprint.clone())
33
36
  .or_default()
@@ -72,6 +75,17 @@ impl QualityCheck for DuplicateBlockCheck {
72
75
  }
73
76
  }
74
77
 
78
+ fn window_fingerprint(check_id: &str, lines: &[NormalizedLine]) -> String {
79
+ let mut hasher = Sha256::new();
80
+ hasher.update(check_id.as_bytes());
81
+ hasher.update(b"\0");
82
+ for line in lines {
83
+ hasher.update(line.value.as_bytes());
84
+ hasher.update(b"\n");
85
+ }
86
+ format!("sha256:{:x}", hasher.finalize())
87
+ }
88
+
75
89
  #[derive(Debug, Clone)]
76
90
  struct DuplicateOccurrence {
77
91
  path: String,
@@ -12,6 +12,9 @@ impl QualityCheck for NearDuplicateFunctionCheck {
12
12
  }
13
13
 
14
14
  fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
15
+ if context.mode == super::super::types::QualityMode::Report {
16
+ return;
17
+ }
15
18
  let symbols = collect_function_occurrences(context, self.id());
16
19
  let mut emitted = HashSet::new();
17
20
 
@@ -59,8 +62,7 @@ fn collect_function_occurrences<'a>(
59
62
  check_id: &str,
60
63
  ) -> Vec<FunctionOccurrence<'a>> {
61
64
  context
62
- .files
63
- .iter()
65
+ .comparison_candidate_files()
64
66
  .filter(|file| {
65
67
  is_code_like_path(&file.path)
66
68
  && context.config.check_enabled_for_path(check_id, &file.path)
@@ -4,7 +4,7 @@ mod near_duplicates;
4
4
  use std::collections::HashSet;
5
5
 
6
6
  use super::scanner::{stable_fingerprint, QualityContext};
7
- use super::types::{QualityMode, QualityViolation};
7
+ use super::types::QualityViolation;
8
8
  use duplicate_blocks::DuplicateBlockCheck;
9
9
  use near_duplicates::NearDuplicateFunctionCheck;
10
10
 
@@ -64,12 +64,11 @@ impl QualityCheck for FileLengthCheck {
64
64
  .filter(|file| context.check_applies_to(self.id(), &file.path))
65
65
  {
66
66
  let limits = context.limits_for(&file.path);
67
- let limit =
68
- if context.mode == QualityMode::Changed && file.added_lines == file.line_count {
69
- limits.max_new_file_lines
70
- } else {
71
- limits.max_file_lines
72
- };
67
+ let limit = if context.mode.is_changed() && file.added_lines == file.line_count {
68
+ limits.max_new_file_lines
69
+ } else {
70
+ limits.max_file_lines
71
+ };
73
72
  if file.line_count > limit {
74
73
  violations.push(violation(
75
74
  self.id(),
@@ -96,7 +95,7 @@ impl QualityCheck for DiffGrowthCheck {
96
95
  }
97
96
 
98
97
  fn evaluate(&self, context: &QualityContext, violations: &mut Vec<QualityViolation>) {
99
- if context.mode != QualityMode::Changed {
98
+ if !context.mode.is_changed() {
100
99
  return;
101
100
  }
102
101
  for file in context
@@ -56,17 +56,62 @@ pub fn cleanup_route_for_path(
56
56
  .into_iter()
57
57
  .collect::<Vec<_>>();
58
58
  related_paths.retain(|related| related != path);
59
+ let topic = if violations.iter().any(|violation| {
60
+ violation.check_id.starts_with("directory-")
61
+ || violation.check_id == "root-file-sprawl"
62
+ || violation.check_id == "dumping-ground-directory"
63
+ || violation.check_id == "path-depth"
64
+ || violation.check_id == "case-collision"
65
+ || violation.check_id == "test-source-pairing"
66
+ }) {
67
+ "repository structure"
68
+ } else {
69
+ "repository quality"
70
+ };
71
+ let agent_instructions = cleanup_instructions(path, topic, &violations);
59
72
  QualityCleanupRoute {
60
73
  schema: "naome.quality-cleanup-route.v1".to_string(),
61
74
  path: path.to_string(),
62
75
  violations,
63
76
  related_paths,
64
- agent_instructions: format!(
65
- "Reduce or split {path} until every repository-quality violation is gone. Prefer extracting reusable helpers/components over copying code. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
66
- ),
77
+ agent_instructions,
67
78
  required_checks: vec![
68
79
  "naome quality check --changed".to_string(),
69
80
  "git diff --check".to_string(),
70
81
  ],
71
82
  }
72
83
  }
84
+
85
+ fn cleanup_instructions(path: &str, topic: &str, violations: &[QualityViolation]) -> String {
86
+ let mut instructions = Vec::new();
87
+ instructions.push(format!(
88
+ "Clean up {path} until every {topic} violation is gone while preserving behavior."
89
+ ));
90
+ for check_id in violations
91
+ .iter()
92
+ .map(|violation| violation.check_id.as_str())
93
+ .collect::<BTreeSet<_>>()
94
+ {
95
+ instructions.push(match check_id {
96
+ "test-source-pairing" => "Create or update a nearby or module-matched test that exercises the changed source behavior.".to_string(),
97
+ "dumping-ground-directory" => "Move feature logic out of generic utils/helpers/common/shared directories into a named module, or extract a reusable helper with a clear owner.".to_string(),
98
+ "directory-role-mixing" => "Separate source, generated, artifact, and dependency/vendor roles into directories that match repository policy.".to_string(),
99
+ "misplaced-file-role" => "Move the file to a configured root for its role, such as source, test, docs, config, or script.".to_string(),
100
+ "root-file-sprawl" => "Move new root-level work into an existing module, docs, config, or script directory unless it is a recognized root manifest.".to_string(),
101
+ "directory-size" => "Split the directory by module or responsibility until direct file count is below the configured limit.".to_string(),
102
+ "path-depth" => "Shorten the path by moving the file closer to its owning module boundary.".to_string(),
103
+ "case-collision" => "Rename colliding paths so they are distinct on case-insensitive filesystems.".to_string(),
104
+ "file-length" => "Reduce or split the file into focused modules with stable public behavior.".to_string(),
105
+ "function-length" => "Extract cohesive helper functions or components until each function fits the configured limit.".to_string(),
106
+ "top-level-symbols" => "Group related symbols into smaller modules and export only the intended public surface.".to_string(),
107
+ "duplicate-block" | "near-duplicate-function" => "Extract shared behavior into a reusable helper, fixture, builder, or component used by all duplicate call sites.".to_string(),
108
+ "diff-growth" => "Reduce the diff by narrowing scope, splitting generated changes, or moving unrelated cleanup to a separate task.".to_string(),
109
+ _ => format!("Resolve the {check_id} finding without weakening generic policy."),
110
+ });
111
+ }
112
+ instructions.push(
113
+ "Run naome quality check --changed and git diff --check before task completion."
114
+ .to_string(),
115
+ );
116
+ instructions.join(" ")
117
+ }
@@ -4,6 +4,7 @@ use std::path::Path;
4
4
  use crate::models::NaomeError;
5
5
 
6
6
  use super::adapters::{apply_enabled_adapters, detected_adapter_ids, validate_adapter_ids};
7
+ use super::config_support::validate_ready_schema;
7
8
  use super::scanner::collect_repo_paths;
8
9
  use super::types::RepositoryQualityConfig;
9
10
 
@@ -46,21 +47,13 @@ fn generated_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError>
46
47
  }
47
48
 
48
49
  fn validate_config(config: &RepositoryQualityConfig) -> Result<(), NaomeError> {
49
- if config.schema != "naome.repository-quality.v1" {
50
- return Err(NaomeError::new(
51
- ".naome/repository-quality.json schema must be naome.repository-quality.v1.",
52
- ));
53
- }
54
- if config.version != 1 {
55
- return Err(NaomeError::new(
56
- ".naome/repository-quality.json version must be 1.",
57
- ));
58
- }
59
- if config.status != "ready" {
60
- return Err(NaomeError::new(
61
- ".naome/repository-quality.json status must be ready.",
62
- ));
63
- }
50
+ validate_ready_schema(
51
+ CONFIG_RELATIVE_PATH,
52
+ &config.schema,
53
+ "naome.repository-quality.v1",
54
+ config.version,
55
+ &config.status,
56
+ )?;
64
57
  if config.limits.duplicate_block_lines < 3 {
65
58
  return Err(NaomeError::new(
66
59
  ".naome/repository-quality.json duplicateBlockLines must be at least 3.",
@@ -0,0 +1,24 @@
1
+ use crate::models::NaomeError;
2
+
3
+ pub(crate) fn validate_ready_schema(
4
+ config_path: &str,
5
+ actual_schema: &str,
6
+ expected_schema: &str,
7
+ version: u32,
8
+ status: &str,
9
+ ) -> Result<(), NaomeError> {
10
+ if actual_schema != expected_schema {
11
+ return Err(NaomeError::new(format!(
12
+ "{config_path} schema must be {expected_schema}."
13
+ )));
14
+ }
15
+ if version != 1 {
16
+ return Err(NaomeError::new(format!("{config_path} version must be 1.")));
17
+ }
18
+ if status != "ready" {
19
+ return Err(NaomeError::new(format!(
20
+ "{config_path} status must be ready."
21
+ )));
22
+ }
23
+ Ok(())
24
+ }