@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
@@ -1,7 +1,7 @@
1
1
  use std::path::Path;
2
2
 
3
3
  use naome_core::{
4
- classify_mutations, refresh_integrity, safe_rg_args, tracked_process_report,
4
+ classify_mutations, doctor_report, refresh_integrity, safe_rg_args, tracked_process_report,
5
5
  validate_search_command, verification_phase_plan,
6
6
  };
7
7
 
@@ -83,6 +83,26 @@ pub fn run_workflow_command(
83
83
  Ok(())
84
84
  }
85
85
 
86
+ pub fn run_doctor(root: &Path, args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
87
+ let report = doctor_report(root)?;
88
+ if args.iter().any(|arg| arg == "--json") {
89
+ println!("{}", serde_json::to_string_pretty(&report)?);
90
+ } else {
91
+ println!(
92
+ "NAOME doctor: {}",
93
+ if report.ok { "ok" } else { "attention needed" }
94
+ );
95
+ println!("{}", report.next_action);
96
+ if !report.recommended_check_ids.is_empty() {
97
+ println!(
98
+ "Recommended checks: {}",
99
+ report.recommended_check_ids.join(", ")
100
+ );
101
+ }
102
+ }
103
+ Ok(())
104
+ }
105
+
86
106
  fn run_check_search(
87
107
  root: &Path,
88
108
  args: &[String],
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "naome-core"
3
- version = "1.2.1"
3
+ version = "1.3.0"
4
4
  edition.workspace = true
5
5
  license.workspace = true
6
6
  repository.workspace = true
@@ -63,12 +63,14 @@ fn read_naomeignore_patterns(root: &Path) -> Vec<String> {
63
63
  return Vec::new();
64
64
  };
65
65
 
66
- content
66
+ let mut patterns = content
67
67
  .lines()
68
68
  .map(str::trim)
69
69
  .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.starts_with('!'))
70
70
  .map(normalize_ignore_pattern)
71
- .collect()
71
+ .collect::<Vec<_>>();
72
+ patterns.push(".naome/cache/**".to_string());
73
+ patterns
72
74
  }
73
75
 
74
76
  fn normalize_ignore_pattern(pattern: &str) -> String {
@@ -50,6 +50,7 @@ pub const LOCAL_NATIVE_BINARY_PATHS: &[&str] = &[
50
50
  ".naome/bin/naome-rust",
51
51
  ".naome/bin/naome-rust.exe",
52
52
  ".naome/archive",
53
+ ".naome/cache",
53
54
  ".naome/task-journal.jsonl",
54
55
  ];
55
56
 
@@ -72,6 +73,7 @@ pub fn install_plan(harness_version: impl Into<String>) -> InstallPlan {
72
73
  "# NAOME local machine-owned harness files.",
73
74
  ".naome/archive/",
74
75
  ".naome/bin/naome-rust*",
76
+ ".naome/cache/",
75
77
  ".naome/task-journal.jsonl",
76
78
  ];
77
79
  git_exclude_entries.extend_from_slice(LOCAL_ONLY_MACHINE_OWNED_PATHS);
@@ -21,10 +21,13 @@ pub use intent::{evaluate_intent, format_intent, IntentDecision, PromptEvidence}
21
21
  pub use journal::{append_task_journal, TaskJournalEntry};
22
22
  pub use models::{Decision, NaomeError};
23
23
  pub use quality::{
24
- check_repository_quality, explain_repository_structure, init_repository_quality,
25
- plan_quality_cleanup, route_quality_cleanup, QualityCleanupPlan, QualityCleanupRoute,
26
- QualityCleanupTask, QualityInitResult, QualityMode, QualityReport, QualitySummary,
27
- QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig, StructurePathExplanation,
24
+ check_repository_quality, check_semantic_legacy, explain_repository_structure,
25
+ clear_quality_cache, init_repository_quality, init_repository_quality_with_mode,
26
+ plan_quality_cleanup, quality_cache_status, route_quality_cleanup,
27
+ semantic_route_for_finding, QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute,
28
+ QualityCleanupTask, QualityInitMode, QualityInitResult, QualityMode, QualityReport,
29
+ QualitySummary, QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig,
30
+ SemanticFinding, SemanticReport, StructurePathExplanation,
28
31
  };
29
32
  pub use route::{evaluate_route, explain_route, ExplainDecision, RouteDecision, RouteOptions};
30
33
  pub use task_state::{
@@ -34,8 +37,9 @@ pub use task_state::{
34
37
  pub use verification::seed_builtin_verification_checks;
35
38
  pub use verification_contract::validate_verification_contract;
36
39
  pub use workflow::{
37
- classify_mutations, refresh_integrity, safe_rg_args, summarize_command_output,
40
+ classify_mutations, doctor_report, refresh_integrity, safe_rg_args, summarize_command_output,
38
41
  tracked_process_report, validate_read_boundaries, validate_search_command,
39
- verification_phase_plan, CommandCheckResult, CommandOutputSummary, IntegrityRefreshReport,
40
- MutationClassification, ProcessReport, ReadActivity, VerificationPhasePlan, WorkflowFinding,
42
+ verification_phase_plan, CommandCheckResult, CommandOutputSummary, DoctorReport, DoctorSection,
43
+ IntegrityRefreshReport, MutationClassification, ProcessReport, ReadActivity,
44
+ RepositoryPolicySection, VerificationPhasePlan, WorkflowFinding,
41
45
  };
@@ -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
@@ -68,17 +68,50 @@ pub fn cleanup_route_for_path(
68
68
  } else {
69
69
  "repository quality"
70
70
  };
71
+ let agent_instructions = cleanup_instructions(path, topic, &violations);
71
72
  QualityCleanupRoute {
72
73
  schema: "naome.quality-cleanup-route.v1".to_string(),
73
74
  path: path.to_string(),
74
75
  violations,
75
76
  related_paths,
76
- agent_instructions: format!(
77
- "Reduce or split {path} until every {topic} violation is gone. Prefer named modules and reusable helpers/components over dumping logic into generic directories. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
78
- ),
77
+ agent_instructions,
79
78
  required_checks: vec![
80
79
  "naome quality check --changed".to_string(),
81
80
  "git diff --check".to_string(),
82
81
  ],
83
82
  }
84
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
+ }
@@ -1,11 +1,13 @@
1
1
  mod adapter_support;
2
2
  mod adapters;
3
3
  mod baseline;
4
+ mod cache;
4
5
  mod checks;
5
6
  mod cleanup;
6
7
  mod config;
7
8
  mod config_support;
8
9
  mod scanner;
10
+ mod semantic;
9
11
  mod structure;
10
12
  mod types;
11
13
 
@@ -14,18 +16,26 @@ use std::path::Path;
14
16
  use crate::models::NaomeError;
15
17
 
16
18
  pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
19
+ pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
20
+ pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
17
21
  pub use structure::{
18
22
  explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
19
23
  };
20
24
  pub use types::{
21
- QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
22
- QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
25
+ QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
26
+ QualityInitResult, QualityMode, QualityReport, QualitySummary, QualityViolation,
27
+ RepositoryQualityConfig,
23
28
  };
24
29
 
25
- use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_baseline};
30
+ use self::baseline::{
31
+ baseline_relative_path, read_baseline_fingerprints, write_baseline,
32
+ write_empty_baseline_if_missing,
33
+ };
26
34
  use self::checks::run_quality_checks;
27
35
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
36
+ use self::scanner::collect_repo_paths;
28
37
  use self::scanner::scan_repository;
38
+ use self::semantic::run_semantic_checks;
29
39
  use self::structure::{
30
40
  run_repository_structure_checks, structure_config_relative_path,
31
41
  write_default_structure_config_if_missing,
@@ -49,6 +59,10 @@ pub fn check_repository_quality(
49
59
  .filter(|violation| violation.baseline)
50
60
  .count();
51
61
  let ok = blocking_violation_count == 0;
62
+ let mut reason_codes = context.reason_codes.clone();
63
+ if mode == QualityMode::Report {
64
+ reason_codes.push("deep_checks_skipped".to_string());
65
+ }
52
66
 
53
67
  Ok(QualityReport {
54
68
  schema: "naome.repository-quality-report.v1".to_string(),
@@ -58,30 +72,58 @@ pub fn check_repository_quality(
58
72
  scanned_paths: context.scanned_paths(),
59
73
  summary: QualitySummary {
60
74
  scanned_files: context.files.len(),
75
+ scanned_path_count: context.scanned_paths().len(),
61
76
  violation_count: violations.len(),
62
77
  baseline_violation_count,
63
78
  blocking_violation_count,
79
+ truncated: context.truncated,
80
+ reason_codes,
81
+ cache_hits: context.cache_hits,
82
+ cache_misses: context.cache_misses,
64
83
  },
65
84
  violations,
66
85
  })
67
86
  }
68
87
 
69
88
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
89
+ init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
90
+ }
91
+
92
+ pub fn init_repository_quality_with_mode(
93
+ root: &Path,
94
+ mode: QualityInitMode,
95
+ ) -> Result<QualityInitResult, NaomeError> {
70
96
  let config_written = write_default_config_if_missing(root)?;
71
97
  let structure_config_written = {
72
- let config = read_config(root)?;
73
- let context = scan_repository(root, QualityMode::Report, config)?;
74
- write_default_structure_config_if_missing(root, &context.repo_paths)?
98
+ let repo_paths = collect_repo_paths(root)?;
99
+ write_default_structure_config_if_missing(root, &repo_paths)?
100
+ };
101
+ let (baseline_written, baseline_violations, baseline_pending) = match mode {
102
+ QualityInitMode::SeedOnly => {
103
+ write_empty_baseline_if_missing(root)?;
104
+ (false, 0, true)
105
+ }
106
+ QualityInitMode::Baseline | QualityInitMode::DeepBaseline => {
107
+ let report = check_repository_quality(
108
+ root,
109
+ if mode == QualityInitMode::DeepBaseline {
110
+ QualityMode::DeepReport
111
+ } else {
112
+ QualityMode::Report
113
+ },
114
+ )?;
115
+ (write_baseline(root, &report.violations)?, report.violations.len(), false)
116
+ }
75
117
  };
76
- let report = check_repository_quality(root, QualityMode::Report)?;
77
- let baseline_written = write_baseline(root, &report.violations)?;
78
118
 
79
119
  Ok(QualityInitResult {
80
120
  schema: "naome.repository-quality-init.v1".to_string(),
121
+ mode: mode.as_str().to_string(),
81
122
  config_written,
82
123
  structure_config_written,
83
124
  baseline_written,
84
- baseline_violations: report.violations.len(),
125
+ baseline_pending,
126
+ baseline_violations,
85
127
  config_path: config_relative_path().to_string(),
86
128
  structure_config_path: structure_config_relative_path().to_string(),
87
129
  baseline_path: baseline_relative_path().to_string(),
@@ -106,3 +148,9 @@ pub fn route_quality_cleanup(
106
148
  .collect::<Vec<_>>();
107
149
  Ok(cleanup_route_for_path(&path, violations))
108
150
  }
151
+
152
+ pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticReport, NaomeError> {
153
+ let config = read_config(root)?;
154
+ let context = scan_repository(root, mode, config)?;
155
+ Ok(run_semantic_checks(&context))
156
+ }
@@ -0,0 +1,78 @@
1
+ pub(super) fn normalize_line(line: &str) -> Option<String> {
2
+ let trimmed = line.trim();
3
+ if trimmed.is_empty()
4
+ || is_comment_only(trimmed)
5
+ || is_string_list_item(trimmed)
6
+ || is_generated_hash_mapping(trimmed)
7
+ {
8
+ return None;
9
+ }
10
+
11
+ let mut normalized = String::new();
12
+ let mut in_string = false;
13
+ let mut quote = '\0';
14
+ let mut previous_space = false;
15
+ for character in trimmed.chars() {
16
+ if in_string {
17
+ if character == quote {
18
+ in_string = false;
19
+ normalized.push('S');
20
+ previous_space = false;
21
+ }
22
+ continue;
23
+ }
24
+ if character == '"' || character == '\'' || character == '`' {
25
+ in_string = true;
26
+ quote = character;
27
+ continue;
28
+ }
29
+ let next = if character.is_ascii_digit() {
30
+ '0'
31
+ } else if character.is_whitespace() {
32
+ ' '
33
+ } else {
34
+ character.to_ascii_lowercase()
35
+ };
36
+ if next == ' ' {
37
+ if !previous_space {
38
+ normalized.push(next);
39
+ }
40
+ previous_space = true;
41
+ } else {
42
+ normalized.push(next);
43
+ previous_space = false;
44
+ }
45
+ }
46
+ let value = normalized.trim().to_string();
47
+ (!value.is_empty()).then_some(value)
48
+ }
49
+
50
+ pub(super) fn token_set(line: &str) -> Vec<String> {
51
+ line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
52
+ .filter(|token| token.len() > 1)
53
+ .map(ToString::to_string)
54
+ .collect()
55
+ }
56
+
57
+ fn is_comment_only(trimmed: &str) -> bool {
58
+ trimmed.starts_with("//")
59
+ || trimmed.starts_with('#')
60
+ || trimmed.starts_with("/*")
61
+ || trimmed.starts_with('*')
62
+ || trimmed.starts_with("--")
63
+ }
64
+
65
+ fn is_generated_hash_mapping(trimmed: &str) -> bool {
66
+ let Some((key, value)) = trimmed.split_once(':') else {
67
+ return false;
68
+ };
69
+ key.trim_start().starts_with('"')
70
+ && value.trim_start().starts_with("\"sha256:")
71
+ && value.chars().filter(|character| *character == '"').count() >= 2
72
+ }
73
+
74
+ fn is_string_list_item(trimmed: &str) -> bool {
75
+ let value = trimmed.trim_end_matches(',');
76
+ (value.starts_with('"') && value.ends_with('"'))
77
+ || (value.starts_with('\'') && value.ends_with('\''))
78
+ }