@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,158 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use super::model::ObjectCandidate;
4
+ use crate::quality::scanner::{stable_fingerprint, FileAnalysis, SymbolAnalysis};
5
+
6
+ pub(super) fn extract_object_candidates(file: &FileAnalysis) -> Vec<ObjectCandidate> {
7
+ let mut candidates = Vec::new();
8
+ let mut start: Option<usize> = None;
9
+ let mut depth = 0isize;
10
+
11
+ for (index, line) in file.raw_lines.iter().enumerate() {
12
+ let delta = brace_delta(line);
13
+ if start.is_none() && line.contains('{') && looks_like_object_start(line) {
14
+ start = Some(index);
15
+ }
16
+ if start.is_some() {
17
+ depth += delta;
18
+ if depth <= 0 {
19
+ if let Some(start_index) = start.take() {
20
+ push_candidate(file, start_index, index, &mut candidates);
21
+ }
22
+ depth = 0;
23
+ }
24
+ }
25
+ }
26
+
27
+ candidates
28
+ }
29
+
30
+ fn push_candidate(
31
+ file: &FileAnalysis,
32
+ start_index: usize,
33
+ end_index: usize,
34
+ candidates: &mut Vec<ObjectCandidate>,
35
+ ) {
36
+ let line_count = end_index.saturating_sub(start_index) + 1;
37
+ if line_count < 8 {
38
+ return;
39
+ }
40
+ let keys = extract_keys(&file.raw_lines[start_index..=end_index]);
41
+ if keys.len() < 3 {
42
+ return;
43
+ }
44
+ let shape = keys.iter().cloned().collect::<Vec<_>>().join("|");
45
+ let shape_hash = stable_fingerprint(&["semantic-shape", &shape]);
46
+ let start_line = start_index + 1;
47
+ candidates.push(ObjectCandidate {
48
+ path: file.path.clone(),
49
+ start_line,
50
+ end_line: end_index + 1,
51
+ symbol: nearest_symbol(&file.symbols, start_line),
52
+ keys,
53
+ shape_hash,
54
+ line_count,
55
+ in_test_context: is_test_context(&file.path),
56
+ });
57
+ }
58
+
59
+ fn extract_keys(lines: &[String]) -> BTreeSet<String> {
60
+ lines
61
+ .iter()
62
+ .filter_map(|line| key_from_line(line))
63
+ .collect::<BTreeSet<_>>()
64
+ }
65
+
66
+ fn key_from_line(line: &str) -> Option<String> {
67
+ let trimmed = line.trim().trim_start_matches(['{', ',', '[']);
68
+ if trimmed.starts_with("//") || trimmed.starts_with('#') {
69
+ return None;
70
+ }
71
+ let (raw_key, _) = trimmed.split_once(':')?;
72
+ let key = raw_key.trim().trim_matches('"').trim_matches('\'').trim();
73
+ let valid = !key.is_empty()
74
+ && key.len() <= 64
75
+ && key.chars().all(|character| {
76
+ character.is_ascii_alphanumeric() || character == '_' || character == '-'
77
+ });
78
+ valid.then(|| key.to_ascii_lowercase())
79
+ }
80
+
81
+ fn brace_delta(line: &str) -> isize {
82
+ let mut delta = 0isize;
83
+ let mut in_string = false;
84
+ let mut quote = '\0';
85
+ let mut escaped = false;
86
+ for character in line.chars() {
87
+ if in_string {
88
+ if escaped {
89
+ escaped = false;
90
+ } else if character == '\\' {
91
+ escaped = true;
92
+ } else if character == quote {
93
+ in_string = false;
94
+ }
95
+ continue;
96
+ }
97
+ if character == '"' || character == '\'' || character == '`' {
98
+ in_string = true;
99
+ quote = character;
100
+ } else if character == '{' {
101
+ delta += 1;
102
+ } else if character == '}' {
103
+ delta -= 1;
104
+ }
105
+ }
106
+ delta
107
+ }
108
+
109
+ fn looks_like_object_start(line: &str) -> bool {
110
+ let trimmed = line.trim();
111
+ if starts_code_block(trimmed) {
112
+ return false;
113
+ }
114
+ trimmed.starts_with('{')
115
+ || trimmed.ends_with('{')
116
+ || trimmed.contains("= {")
117
+ || trimmed.contains(": {")
118
+ || trimmed.contains("({")
119
+ }
120
+
121
+ fn starts_code_block(trimmed: &str) -> bool {
122
+ [
123
+ "fn ",
124
+ "pub fn ",
125
+ "function ",
126
+ "async function ",
127
+ "if ",
128
+ "for ",
129
+ "while ",
130
+ "match ",
131
+ "impl ",
132
+ "mod ",
133
+ "class ",
134
+ "struct ",
135
+ "enum ",
136
+ ]
137
+ .iter()
138
+ .any(|prefix| trimmed.starts_with(prefix))
139
+ }
140
+
141
+ fn nearest_symbol(symbols: &[SymbolAnalysis], line: usize) -> Option<String> {
142
+ symbols
143
+ .iter()
144
+ .filter(|symbol| symbol.start_line <= line)
145
+ .max_by_key(|symbol| symbol.start_line)
146
+ .map(|symbol| symbol.name.clone())
147
+ .filter(|name| !name.is_empty())
148
+ }
149
+
150
+ fn is_test_context(path: &str) -> bool {
151
+ let lower = path.to_ascii_lowercase();
152
+ lower.contains("/test")
153
+ || lower.contains("test/")
154
+ || lower.contains(".test.")
155
+ || lower.contains(".spec.")
156
+ || lower.contains("fixture")
157
+ || lower.contains("support")
158
+ }
@@ -0,0 +1,85 @@
1
+ use std::collections::BTreeSet;
2
+
3
+ use serde::Serialize;
4
+
5
+ #[derive(Debug, Clone, Serialize)]
6
+ #[serde(rename_all = "camelCase")]
7
+ pub struct SemanticReport {
8
+ pub schema: String,
9
+ pub mode: String,
10
+ pub ok: bool,
11
+ pub changed_paths: Vec<String>,
12
+ pub summary: SemanticSummary,
13
+ pub findings: Vec<SemanticFinding>,
14
+ }
15
+
16
+ #[derive(Debug, Clone, Serialize)]
17
+ #[serde(rename_all = "camelCase")]
18
+ pub struct SemanticSummary {
19
+ pub scanned_files: usize,
20
+ pub scanned_path_count: usize,
21
+ pub finding_count: usize,
22
+ pub blocking_finding_count: usize,
23
+ pub truncated: bool,
24
+ pub reason_codes: Vec<String>,
25
+ }
26
+
27
+ #[derive(Debug, Clone, Serialize)]
28
+ #[serde(rename_all = "camelCase")]
29
+ pub struct SemanticFinding {
30
+ pub id: String,
31
+ pub kind: String,
32
+ pub confidence: f64,
33
+ pub severity: String,
34
+ pub mode: String,
35
+ pub summary: String,
36
+ pub occurrences: Vec<SemanticOccurrence>,
37
+ pub cleanup_route: SemanticCleanupRoute,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize)]
41
+ #[serde(rename_all = "camelCase")]
42
+ pub struct SemanticOccurrence {
43
+ pub path: String,
44
+ pub start_line: usize,
45
+ pub end_line: usize,
46
+ pub symbol: Option<String>,
47
+ pub shape_hash: String,
48
+ pub key_count: usize,
49
+ pub line_count: usize,
50
+ }
51
+
52
+ #[derive(Debug, Clone, Serialize)]
53
+ #[serde(rename_all = "camelCase")]
54
+ pub struct SemanticCleanupRoute {
55
+ pub intent: String,
56
+ pub target_suggestion: String,
57
+ pub agent_instructions: Vec<String>,
58
+ pub required_checks: Vec<String>,
59
+ }
60
+
61
+ #[derive(Debug, Clone)]
62
+ pub(super) struct ObjectCandidate {
63
+ pub path: String,
64
+ pub start_line: usize,
65
+ pub end_line: usize,
66
+ pub symbol: Option<String>,
67
+ pub keys: BTreeSet<String>,
68
+ pub shape_hash: String,
69
+ pub line_count: usize,
70
+ pub in_test_context: bool,
71
+ }
72
+
73
+ impl ObjectCandidate {
74
+ pub(super) fn occurrence(&self) -> SemanticOccurrence {
75
+ SemanticOccurrence {
76
+ path: self.path.clone(),
77
+ start_line: self.start_line,
78
+ end_line: self.end_line,
79
+ symbol: self.symbol.clone(),
80
+ shape_hash: self.shape_hash.clone(),
81
+ key_count: self.keys.len(),
82
+ line_count: self.line_count,
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,52 @@
1
+ use super::model::{SemanticCleanupRoute, SemanticOccurrence};
2
+ use crate::quality::scanner::QualityContext;
3
+ use crate::quality::types::QualityMode;
4
+
5
+ pub(super) fn finding_mode(context: &QualityContext) -> &'static str {
6
+ match context.mode {
7
+ QualityMode::ChangedFast => "changed-blocking",
8
+ QualityMode::Report => "report",
9
+ QualityMode::DeepReport => "deep-report",
10
+ }
11
+ }
12
+
13
+ pub(super) fn cleanup_route(
14
+ intent: &str,
15
+ occurrences: &[SemanticOccurrence],
16
+ first_instruction: &str,
17
+ ) -> SemanticCleanupRoute {
18
+ let target_suggestion = target_suggestion(occurrences);
19
+ SemanticCleanupRoute {
20
+ intent: intent.to_string(),
21
+ target_suggestion,
22
+ agent_instructions: vec![
23
+ first_instruction.to_string(),
24
+ "Update every occurrence listed in this finding group; do not leave parallel inline copies behind.".to_string(),
25
+ "Preserve behavior and keep version-specific or schema-specific differences explicit as parameters or named fixtures.".to_string(),
26
+ "Do not auto-remove compatibility coverage unless focused tests prove the behavior remains covered.".to_string(),
27
+ ],
28
+ required_checks: vec![
29
+ "naome semantic check --changed".to_string(),
30
+ "naome quality check --changed".to_string(),
31
+ "git diff --check".to_string(),
32
+ ],
33
+ }
34
+ }
35
+
36
+ fn target_suggestion(occurrences: &[SemanticOccurrence]) -> String {
37
+ let Some(first) = occurrences.first() else {
38
+ return "test-support".to_string();
39
+ };
40
+ let directory = first
41
+ .path
42
+ .rsplit_once('/')
43
+ .map(|(directory, _)| directory)
44
+ .unwrap_or(".");
45
+ if directory == "." {
46
+ "test-support".to_string()
47
+ } else if directory.contains("script") {
48
+ format!("{directory}/test-support")
49
+ } else {
50
+ format!("{directory}/fixtures")
51
+ }
52
+ }
@@ -0,0 +1,68 @@
1
+ mod checks;
2
+ mod extract;
3
+ mod model;
4
+ mod route;
5
+
6
+ use checks::{copied_config_findings, inline_legacy_fixture_findings};
7
+ use extract::extract_object_candidates;
8
+ pub use model::{SemanticFinding, SemanticReport};
9
+
10
+ use super::scanner::QualityContext;
11
+ use model::SemanticSummary;
12
+
13
+ pub fn run_semantic_checks(context: &QualityContext) -> SemanticReport {
14
+ let mut candidates = context
15
+ .files
16
+ .iter()
17
+ .flat_map(extract_object_candidates)
18
+ .collect::<Vec<_>>();
19
+ candidates.sort_by(|left, right| {
20
+ left.path
21
+ .cmp(&right.path)
22
+ .then(left.start_line.cmp(&right.start_line))
23
+ .then(left.shape_hash.cmp(&right.shape_hash))
24
+ });
25
+
26
+ let mut findings = Vec::new();
27
+ findings.extend(copied_config_findings(context, &candidates));
28
+ findings.extend(inline_legacy_fixture_findings(context, &candidates));
29
+ findings.sort_by(|left, right| {
30
+ left.kind
31
+ .cmp(&right.kind)
32
+ .then(left.occurrences[0].path.cmp(&right.occurrences[0].path))
33
+ .then(
34
+ left.occurrences[0]
35
+ .start_line
36
+ .cmp(&right.occurrences[0].start_line),
37
+ )
38
+ .then(left.id.cmp(&right.id))
39
+ });
40
+
41
+ let blocking_finding_count = findings.len();
42
+ SemanticReport {
43
+ schema: "naome.semantic-legacy-report.v1".to_string(),
44
+ mode: context.mode.as_str().to_string(),
45
+ ok: blocking_finding_count == 0,
46
+ changed_paths: context.changed_paths.clone(),
47
+ summary: SemanticSummary {
48
+ scanned_files: context.files.len(),
49
+ scanned_path_count: context.scanned_paths().len(),
50
+ finding_count: findings.len(),
51
+ blocking_finding_count,
52
+ truncated: context.truncated,
53
+ reason_codes: context.reason_codes.clone(),
54
+ },
55
+ findings,
56
+ }
57
+ }
58
+
59
+ pub fn semantic_route_for_finding(
60
+ report: &SemanticReport,
61
+ finding_id: &str,
62
+ ) -> Option<SemanticFinding> {
63
+ report
64
+ .findings
65
+ .iter()
66
+ .find(|finding| finding.id == finding_id)
67
+ .cloned()
68
+ }
@@ -1,5 +1,3 @@
1
- use std::collections::BTreeMap;
2
-
3
1
  use crate::quality::structure::model::{RepositoryStructureModel, StructurePath};
4
2
  use crate::quality::types::{QualityMode, QualityViolation};
5
3
 
@@ -38,7 +36,7 @@ pub(super) fn directory_size(
38
36
  if directory.file_count <= model.config.limits.max_directory_files {
39
37
  continue;
40
38
  }
41
- if mode == QualityMode::Changed && directory.direct_changed_paths.is_empty() {
39
+ if mode.is_changed() && directory.direct_changed_paths.is_empty() {
42
40
  continue;
43
41
  }
44
42
  push_with_limit(
@@ -85,20 +83,11 @@ pub(super) fn case_collisions(
85
83
  mode: QualityMode,
86
84
  violations: &mut Vec<QualityViolation>,
87
85
  ) {
88
- let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
89
- for path in &model.paths {
90
- groups
91
- .entry(path.explanation.path.to_ascii_lowercase())
92
- .or_default()
93
- .push(path.explanation.path.clone());
94
- }
95
- for group in groups.values().filter(|group| group.len() > 1) {
86
+ for group in model.lowercase_paths.values().filter(|group| group.len() > 1) {
96
87
  let changed_group = group.iter().any(|path| {
97
- model.paths.iter().any(|candidate| {
98
- candidate.explanation.path == *path && candidate.explanation.changed
99
- })
88
+ model.changed_paths.contains(path)
100
89
  });
101
- if mode == QualityMode::Changed && !changed_group {
90
+ if mode.is_changed() && !changed_group {
102
91
  continue;
103
92
  }
104
93
  for path in group {
@@ -134,10 +123,11 @@ fn related_module_paths(model: &RepositoryStructureModel, path: &StructurePath)
134
123
  return Vec::new();
135
124
  };
136
125
  model
137
- .paths
138
- .iter()
139
- .filter(|candidate| candidate.explanation.module.as_ref() == Some(module))
140
- .map(|candidate| candidate.explanation.path.clone())
126
+ .module_paths
127
+ .get(module)
128
+ .into_iter()
129
+ .flatten()
130
+ .cloned()
141
131
  .filter(|candidate| candidate != &path.explanation.path)
142
132
  .take(10)
143
133
  .collect()
@@ -53,7 +53,7 @@ fn check_enabled(model: &RepositoryStructureModel, check_id: &str) -> bool {
53
53
  }
54
54
 
55
55
  fn applies(path: &StructurePath, mode: QualityMode) -> bool {
56
- mode == QualityMode::Report || path.explanation.changed
56
+ !mode.is_changed() || path.explanation.changed
57
57
  }
58
58
 
59
59
  fn push(
@@ -24,10 +24,16 @@ pub fn build_structure_model(
24
24
  .collect::<Vec<_>>();
25
25
  paths.sort_by(|left, right| left.explanation.path.cmp(&right.explanation.path));
26
26
  let directories = directories_for(&paths);
27
+ let indexes = indexes_for(&paths);
27
28
  RepositoryStructureModel {
28
29
  config,
29
30
  paths,
30
31
  directories,
32
+ module_paths: indexes.module_paths,
33
+ directory_paths: indexes.directory_paths,
34
+ role_paths: indexes.role_paths,
35
+ lowercase_paths: indexes.lowercase_paths,
36
+ changed_paths: indexes.changed_paths,
31
37
  }
32
38
  }
33
39
 
@@ -92,3 +98,49 @@ fn directory_of(path: &str) -> String {
92
98
  .map(|(directory, _)| directory.to_string())
93
99
  .unwrap_or_else(|| ".".to_string())
94
100
  }
101
+
102
+ struct StructureIndexes {
103
+ module_paths: BTreeMap<String, Vec<String>>,
104
+ directory_paths: BTreeMap<String, Vec<String>>,
105
+ role_paths: BTreeMap<String, Vec<String>>,
106
+ lowercase_paths: BTreeMap<String, Vec<String>>,
107
+ changed_paths: BTreeSet<String>,
108
+ }
109
+
110
+ fn indexes_for(paths: &[StructurePath]) -> StructureIndexes {
111
+ let mut module_paths = BTreeMap::new();
112
+ let mut directory_paths = BTreeMap::new();
113
+ let mut role_paths = BTreeMap::new();
114
+ let mut lowercase_paths = BTreeMap::new();
115
+ let mut changed_paths = BTreeSet::new();
116
+ for path in paths {
117
+ let value = path.explanation.path.clone();
118
+ if let Some(module) = &path.explanation.module {
119
+ push_index(&mut module_paths, module, &value);
120
+ }
121
+ push_index(&mut directory_paths, &path.explanation.directory, &value);
122
+ push_index(&mut role_paths, &path.explanation.role, &value);
123
+ push_index(
124
+ &mut lowercase_paths,
125
+ &path.explanation.path.to_ascii_lowercase(),
126
+ &value,
127
+ );
128
+ if path.explanation.changed {
129
+ changed_paths.insert(value);
130
+ }
131
+ }
132
+ StructureIndexes {
133
+ module_paths,
134
+ directory_paths,
135
+ role_paths,
136
+ lowercase_paths,
137
+ changed_paths,
138
+ }
139
+ }
140
+
141
+ fn push_index(index: &mut BTreeMap<String, Vec<String>>, key: &str, value: &str) {
142
+ index
143
+ .entry(key.to_string())
144
+ .or_default()
145
+ .push(value.to_string());
146
+ }
@@ -11,7 +11,7 @@ use std::path::Path;
11
11
  use crate::models::NaomeError;
12
12
 
13
13
  use super::scanner::QualityContext;
14
- use super::types::{QualityMode, QualityViolation};
14
+ use super::types::QualityViolation;
15
15
  use checks::run_structure_checks;
16
16
  use classify::build_structure_model;
17
17
  use config::read_structure_config;
@@ -33,7 +33,7 @@ pub fn run_repository_structure_checks(
33
33
  .find(|file| file.path == violation.path)
34
34
  .is_some_and(|file| !file.symbols.is_empty())
35
35
  });
36
- if context.mode == QualityMode::Report {
36
+ if !context.mode.is_changed() {
37
37
  for violation in &mut violations {
38
38
  violation.baseline = baseline_fingerprints.contains(&violation.fingerprint);
39
39
  }
@@ -1,4 +1,4 @@
1
- use std::collections::BTreeSet;
1
+ use std::collections::{BTreeMap, BTreeSet};
2
2
 
3
3
  use serde::{Deserialize, Serialize};
4
4
 
@@ -121,4 +121,11 @@ pub struct RepositoryStructureModel {
121
121
  pub config: RepositoryStructureConfig,
122
122
  pub paths: Vec<StructurePath>,
123
123
  pub directories: Vec<StructureDirectory>,
124
+ pub module_paths: BTreeMap<String, Vec<String>>,
125
+ #[allow(dead_code)]
126
+ pub directory_paths: BTreeMap<String, Vec<String>>,
127
+ #[allow(dead_code)]
128
+ pub role_paths: BTreeMap<String, Vec<String>>,
129
+ pub lowercase_paths: BTreeMap<String, Vec<String>>,
130
+ pub changed_paths: BTreeSet<String>,
124
131
  }
@@ -4,15 +4,45 @@ use crate::paths;
4
4
 
5
5
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
6
  pub enum QualityMode {
7
- Changed,
7
+ ChangedFast,
8
8
  Report,
9
+ DeepReport,
9
10
  }
10
11
 
11
12
  impl QualityMode {
13
+ #[allow(non_upper_case_globals)]
14
+ pub const Changed: Self = Self::ChangedFast;
15
+
12
16
  pub fn as_str(self) -> &'static str {
13
17
  match self {
14
- Self::Changed => "changed",
18
+ Self::ChangedFast => "changed",
15
19
  Self::Report => "report",
20
+ Self::DeepReport => "deep-report",
21
+ }
22
+ }
23
+
24
+ pub fn is_changed(self) -> bool {
25
+ self == Self::ChangedFast
26
+ }
27
+
28
+ pub fn is_deep(self) -> bool {
29
+ self == Self::DeepReport
30
+ }
31
+ }
32
+
33
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
34
+ pub enum QualityInitMode {
35
+ SeedOnly,
36
+ Baseline,
37
+ DeepBaseline,
38
+ }
39
+
40
+ impl QualityInitMode {
41
+ pub fn as_str(self) -> &'static str {
42
+ match self {
43
+ Self::SeedOnly => "init",
44
+ Self::Baseline => "baseline",
45
+ Self::DeepBaseline => "deep-baseline",
16
46
  }
17
47
  }
18
48
  }
@@ -187,9 +217,14 @@ pub struct QualityReport {
187
217
  #[serde(rename_all = "camelCase")]
188
218
  pub struct QualitySummary {
189
219
  pub scanned_files: usize,
220
+ pub scanned_path_count: usize,
190
221
  pub violation_count: usize,
191
222
  pub baseline_violation_count: usize,
192
223
  pub blocking_violation_count: usize,
224
+ pub truncated: bool,
225
+ pub reason_codes: Vec<String>,
226
+ pub cache_hits: usize,
227
+ pub cache_misses: usize,
193
228
  }
194
229
 
195
230
  #[derive(Debug, Clone, Serialize)]
@@ -211,9 +246,11 @@ pub struct QualityViolation {
211
246
  #[serde(rename_all = "camelCase")]
212
247
  pub struct QualityInitResult {
213
248
  pub schema: String,
249
+ pub mode: String,
214
250
  pub config_written: bool,
215
251
  pub structure_config_written: bool,
216
252
  pub baseline_written: bool,
253
+ pub baseline_pending: bool,
217
254
  pub baseline_violations: usize,
218
255
  pub config_path: String,
219
256
  pub structure_config_path: String,
@@ -258,6 +295,7 @@ pub fn default_generated_paths() -> Vec<String> {
258
295
  const DEFAULT_IGNORED_PATHS: &str = r#"
259
296
  .git/**
260
297
  .naome/archive/**
298
+ .naome/cache/**
261
299
  .naome/task-state.json
262
300
  .naome/task-journal.jsonl
263
301
  .naome/repository-quality.json
@@ -18,16 +18,6 @@ pub(super) fn run_quality_check(
18
18
  check: &QualityCheck,
19
19
  ) -> Result<(), NaomeError> {
20
20
  match check_id {
21
- "installer-tests" => require_builtin_quality_check(
22
- check_id,
23
- check,
24
- "npm run test:naome-installer",
25
- ),
26
- "rust-build" => require_builtin_quality_check(check_id, check, "npm run build:rust"),
27
- "decision-engine-tests" => {
28
- require_builtin_quality_check(check_id, check, "npm run test:decision-engine")
29
- }
30
- "package-dry-run" => require_builtin_quality_check(check_id, check, "npm run pack:dry-run"),
31
21
  "diff-check" => {
32
22
  require_builtin_quality_check(check_id, check, "git diff --check")?;
33
23
  let output = git_output(root, &["diff", "--check"])?;
@@ -46,10 +36,6 @@ pub(super) fn run_quality_check(
46
36
  )?;
47
37
  run_harness_health_check(root)
48
38
  }
49
- "dogfood-health" => {
50
- require_builtin_quality_check(check_id, check, "npm run dogfood:health")?;
51
- run_harness_health_check(root)
52
- }
53
39
  "task-state-check" => {
54
40
  require_builtin_quality_check(check_id, check, "npm run check:task-state")?;
55
41
  run_template_task_state_check(root)
@@ -85,7 +71,7 @@ pub(super) fn run_quality_check(
85
71
  }
86
72
 
87
73
  fn run_repository_quality_check(root: &Path) -> Result<(), NaomeError> {
88
- let report = check_repository_quality(root, QualityMode::Changed)?;
74
+ let report = check_repository_quality(root, QualityMode::ChangedFast)?;
89
75
  if report.ok {
90
76
  return Ok(());
91
77
  }