@lamentis/naome 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/Cargo.lock +2 -2
  2. package/bin/naome-node.js +2 -1579
  3. package/bin/naome.js +19 -5
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/dispatcher.rs +2 -1
  6. package/crates/naome-cli/src/main.rs +3 -0
  7. package/crates/naome-cli/src/quality_commands.rs +90 -2
  8. package/crates/naome-core/Cargo.toml +1 -1
  9. package/crates/naome-core/src/decision/checks.rs +64 -0
  10. package/crates/naome-core/src/decision/idle.rs +67 -0
  11. package/crates/naome-core/src/decision/json.rs +36 -0
  12. package/crates/naome-core/src/decision/states.rs +165 -0
  13. package/crates/naome-core/src/decision.rs +131 -353
  14. package/crates/naome-core/src/install_plan.rs +2 -0
  15. package/crates/naome-core/src/lib.rs +5 -3
  16. package/crates/naome-core/src/paths.rs +3 -1
  17. package/crates/naome-core/src/quality/adapter_support.rs +89 -0
  18. package/crates/naome-core/src/quality/adapters.rs +20 -67
  19. package/crates/naome-core/src/quality/cleanup.rs +13 -1
  20. package/crates/naome-core/src/quality/config.rs +8 -15
  21. package/crates/naome-core/src/quality/config_support.rs +24 -0
  22. package/crates/naome-core/src/quality/mod.rs +18 -0
  23. package/crates/naome-core/src/quality/scanner.rs +20 -8
  24. package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
  25. package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
  26. package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
  27. package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
  28. package/crates/naome-core/src/quality/structure/checks.rs +124 -0
  29. package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
  30. package/crates/naome-core/src/quality/structure/classify.rs +94 -0
  31. package/crates/naome-core/src/quality/structure/config.rs +89 -0
  32. package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
  33. package/crates/naome-core/src/quality/structure/mod.rs +77 -0
  34. package/crates/naome-core/src/quality/structure/model.rs +124 -0
  35. package/crates/naome-core/src/quality/types.rs +3 -0
  36. package/crates/naome-core/src/route/builtin_checks.rs +155 -0
  37. package/crates/naome-core/src/route/builtin_context.rs +73 -0
  38. package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
  39. package/crates/naome-core/src/route/builtin_require.rs +40 -0
  40. package/crates/naome-core/src/route/context.rs +180 -0
  41. package/crates/naome-core/src/route/execution.rs +96 -0
  42. package/crates/naome-core/src/route/execution_baselines.rs +146 -0
  43. package/crates/naome-core/src/route/execution_support.rs +57 -0
  44. package/crates/naome-core/src/route/execution_tasks.rs +71 -0
  45. package/crates/naome-core/src/route/git_ops.rs +72 -0
  46. package/crates/naome-core/src/route/quality_gate.rs +73 -0
  47. package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
  48. package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
  49. package/crates/naome-core/src/route/worktree.rs +75 -0
  50. package/crates/naome-core/src/route/worktree_files.rs +32 -0
  51. package/crates/naome-core/src/route/worktree_plan.rs +131 -0
  52. package/crates/naome-core/src/route.rs +44 -1217
  53. package/crates/naome-core/src/verification.rs +1 -0
  54. package/crates/naome-core/tests/decision.rs +24 -118
  55. package/crates/naome-core/tests/harness_health.rs +2 -0
  56. package/crates/naome-core/tests/quality.rs +12 -118
  57. package/crates/naome-core/tests/quality_structure.rs +116 -0
  58. package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
  59. package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
  60. package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
  61. package/crates/naome-core/tests/repo_support/mod.rs +16 -0
  62. package/crates/naome-core/tests/repo_support/repo.rs +113 -0
  63. package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
  64. package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
  65. package/crates/naome-core/tests/repo_support/routes.rs +81 -0
  66. package/crates/naome-core/tests/repo_support/verification.rs +168 -0
  67. package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
  68. package/crates/naome-core/tests/route.rs +1 -1376
  69. package/crates/naome-core/tests/route_baseline.rs +86 -0
  70. package/crates/naome-core/tests/route_completion.rs +141 -0
  71. package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
  72. package/crates/naome-core/tests/route_user_diff.rs +198 -0
  73. package/crates/naome-core/tests/route_worktree.rs +54 -0
  74. package/crates/naome-core/tests/task_state.rs +60 -432
  75. package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
  76. package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
  77. package/crates/naome-core/tests/task_state_support/states.rs +84 -0
  78. package/crates/naome-core/tests/verification.rs +4 -45
  79. package/crates/naome-core/tests/verification_contract.rs +22 -78
  80. package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
  81. package/installer/agents.js +90 -0
  82. package/installer/context.js +67 -0
  83. package/installer/filesystem.js +166 -0
  84. package/installer/flows.js +84 -0
  85. package/installer/git-boundary.js +170 -0
  86. package/installer/git-hook-content.js +36 -0
  87. package/installer/git-hooks.js +134 -0
  88. package/installer/git-local.js +2 -0
  89. package/installer/git-shared.js +35 -0
  90. package/installer/harness-file-ops.js +140 -0
  91. package/installer/harness-files.js +56 -0
  92. package/installer/harness-verification.js +123 -0
  93. package/installer/install-plan.js +66 -0
  94. package/installer/main.js +25 -0
  95. package/installer/manifest-state.js +167 -0
  96. package/installer/native-build.js +24 -0
  97. package/installer/native-format.js +6 -0
  98. package/installer/native.js +162 -0
  99. package/installer/output.js +131 -0
  100. package/installer/version.js +32 -0
  101. package/native/darwin-arm64/naome +0 -0
  102. package/native/linux-x64/naome +0 -0
  103. package/package.json +2 -1
  104. package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
  105. package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
  106. package/templates/naome-root/.naome/bin/naome.js +25 -21
  107. package/templates/naome-root/.naome/manifest.json +4 -2
  108. package/templates/naome-root/.naome/repository-structure.json +90 -0
  109. package/templates/naome-root/.naome/verification.json +1 -0
  110. package/templates/naome-root/docs/naome/index.md +4 -3
  111. package/templates/naome-root/docs/naome/repository-quality.md +3 -0
  112. package/templates/naome-root/docs/naome/repository-structure.md +51 -0
  113. package/templates/naome-root/docs/naome/testing.md +2 -1
@@ -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
- }
@@ -56,13 +56,25 @@ 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
+ };
59
71
  QualityCleanupRoute {
60
72
  schema: "naome.quality-cleanup-route.v1".to_string(),
61
73
  path: path.to_string(),
62
74
  violations,
63
75
  related_paths,
64
76
  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."
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."
66
78
  ),
67
79
  required_checks: vec![
68
80
  "naome quality check --changed".to_string(),
@@ -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
+ }
@@ -1,9 +1,12 @@
1
+ mod adapter_support;
1
2
  mod adapters;
2
3
  mod baseline;
3
4
  mod checks;
4
5
  mod cleanup;
5
6
  mod config;
7
+ mod config_support;
6
8
  mod scanner;
9
+ mod structure;
7
10
  mod types;
8
11
 
9
12
  use std::path::Path;
@@ -11,6 +14,9 @@ use std::path::Path;
11
14
  use crate::models::NaomeError;
12
15
 
13
16
  pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
17
+ pub use structure::{
18
+ explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
19
+ };
14
20
  pub use types::{
15
21
  QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitResult, QualityMode,
16
22
  QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
@@ -20,6 +26,10 @@ use self::baseline::{baseline_relative_path, read_baseline_fingerprints, write_b
20
26
  use self::checks::run_quality_checks;
21
27
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
22
28
  use self::scanner::scan_repository;
29
+ use self::structure::{
30
+ run_repository_structure_checks, structure_config_relative_path,
31
+ write_default_structure_config_if_missing,
32
+ };
23
33
 
24
34
  pub fn check_repository_quality(
25
35
  root: &Path,
@@ -29,6 +39,7 @@ pub fn check_repository_quality(
29
39
  let context = scan_repository(root, mode, config)?;
30
40
  let baseline = read_baseline_fingerprints(root)?;
31
41
  let mut violations = run_quality_checks(&context);
42
+ violations.extend(run_repository_structure_checks(root, &context, &baseline)?);
32
43
  for violation in &mut violations {
33
44
  violation.baseline = baseline.contains(&violation.fingerprint);
34
45
  }
@@ -57,15 +68,22 @@ pub fn check_repository_quality(
57
68
 
58
69
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
59
70
  let config_written = write_default_config_if_missing(root)?;
71
+ 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)?
75
+ };
60
76
  let report = check_repository_quality(root, QualityMode::Report)?;
61
77
  let baseline_written = write_baseline(root, &report.violations)?;
62
78
 
63
79
  Ok(QualityInitResult {
64
80
  schema: "naome.repository-quality-init.v1".to_string(),
65
81
  config_written,
82
+ structure_config_written,
66
83
  baseline_written,
67
84
  baseline_violations: report.violations.len(),
68
85
  config_path: config_relative_path().to_string(),
86
+ structure_config_path: structure_config_relative_path().to_string(),
69
87
  baseline_path: baseline_relative_path().to_string(),
70
88
  })
71
89
  }
@@ -20,6 +20,7 @@ pub struct QualityContext {
20
20
  pub mode: QualityMode,
21
21
  pub config: RepositoryQualityConfig,
22
22
  pub changed_paths: Vec<String>,
23
+ pub repo_paths: Vec<String>,
23
24
  pub target_paths: HashSet<String>,
24
25
  pub files: Vec<FileAnalysis>,
25
26
  }
@@ -80,9 +81,12 @@ pub fn scan_repository(
80
81
  ) -> Result<QualityContext, NaomeError> {
81
82
  let changed_paths = git::changed_paths(root)?;
82
83
  let target_paths = changed_paths.iter().cloned().collect::<HashSet<_>>();
84
+ let mut whole_repo_paths = collect_repo_paths(root)?;
85
+ whole_repo_paths.sort();
86
+ whole_repo_paths.dedup();
83
87
  let scan_paths = match mode {
84
88
  QualityMode::Changed => changed_paths.clone(),
85
- QualityMode::Report => collect_repo_paths(root)?,
89
+ QualityMode::Report => whole_repo_paths.clone(),
86
90
  };
87
91
  let added_lines = added_lines_by_path(root)?;
88
92
  let ignore_patterns = ignore_patterns(root, &config);
@@ -98,18 +102,15 @@ pub fn scan_repository(
98
102
  }
99
103
 
100
104
  if mode == QualityMode::Changed {
101
- let mut whole_repo_paths = collect_repo_paths(root)?;
102
- whole_repo_paths.sort();
103
- whole_repo_paths.dedup();
104
105
  let scanned = files
105
106
  .iter()
106
107
  .map(|file| file.path.clone())
107
108
  .collect::<HashSet<_>>();
108
- for path in whole_repo_paths {
109
- if scanned.contains(&path) || should_skip_path(&path, &ignore_patterns) {
109
+ for path in &whole_repo_paths {
110
+ if scanned.contains(path) || should_skip_path(path, &ignore_patterns) {
110
111
  continue;
111
112
  }
112
- if let Some(file) = analyze_repo_file(root, &path, &added_lines) {
113
+ if let Some(file) = analyze_repo_file(root, path, &added_lines) {
113
114
  files.push(file);
114
115
  }
115
116
  }
@@ -120,6 +121,7 @@ pub fn scan_repository(
120
121
  mode,
121
122
  config,
122
123
  changed_paths,
124
+ repo_paths: whole_repo_paths,
123
125
  target_paths,
124
126
  files,
125
127
  })
@@ -249,7 +251,11 @@ fn symbol_name(rest: &str) -> String {
249
251
 
250
252
  fn normalize_line(line: &str) -> Option<String> {
251
253
  let trimmed = line.trim();
252
- if trimmed.is_empty() || is_comment_only(trimmed) || is_generated_hash_mapping(trimmed) {
254
+ if trimmed.is_empty()
255
+ || is_comment_only(trimmed)
256
+ || is_string_list_item(trimmed)
257
+ || is_generated_hash_mapping(trimmed)
258
+ {
253
259
  return None;
254
260
  }
255
261
 
@@ -309,6 +315,12 @@ fn is_generated_hash_mapping(trimmed: &str) -> bool {
309
315
  && value.chars().filter(|character| *character == '"').count() >= 2
310
316
  }
311
317
 
318
+ fn is_string_list_item(trimmed: &str) -> bool {
319
+ let value = trimmed.trim_end_matches(',');
320
+ (value.starts_with('"') && value.ends_with('"'))
321
+ || (value.starts_with('\'') && value.ends_with('\''))
322
+ }
323
+
312
324
  fn token_set(line: &str) -> Vec<String> {
313
325
  line.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
314
326
  .filter(|token| token.len() > 1)
@@ -0,0 +1,84 @@
1
+ use crate::models::NaomeError;
2
+
3
+ use crate::quality::adapter_support::{
4
+ detected_ids, detects_javascript_typescript_project, detects_rust_project, extend_unique,
5
+ find_adapter_by_id, validate_ids, AdapterDescriptor, RepoSignals,
6
+ };
7
+
8
+ use super::model::RepositoryStructureConfig;
9
+
10
+ const CONFIG_PATH: &str = ".naome/repository-structure.json";
11
+
12
+ struct StructureAdapter {
13
+ id: &'static str,
14
+ detect: fn(&RepoSignals<'_>) -> bool,
15
+ source_roots: &'static [&'static str],
16
+ test_roots: &'static [&'static str],
17
+ module_roots: &'static [&'static str],
18
+ allowed_root_files: &'static [&'static str],
19
+ }
20
+
21
+ impl AdapterDescriptor for StructureAdapter {
22
+ fn id(&self) -> &'static str {
23
+ self.id
24
+ }
25
+
26
+ fn detects(&self, signals: &RepoSignals<'_>) -> bool {
27
+ (self.detect)(signals)
28
+ }
29
+ }
30
+
31
+ pub fn detected_structure_adapter_ids(paths: &[String]) -> Vec<String> {
32
+ detected_ids(paths, registry())
33
+ }
34
+
35
+ pub fn apply_structure_adapters(
36
+ mut config: RepositoryStructureConfig,
37
+ ) -> Result<RepositoryStructureConfig, NaomeError> {
38
+ validate_structure_adapter_ids(&config.enabled_adapters)?;
39
+ for adapter_id in config.enabled_adapters.clone() {
40
+ let adapter = find_adapter_by_id(registry(), &adapter_id, CONFIG_PATH)?;
41
+ extend_unique(&mut config.source_roots, adapter.source_roots);
42
+ extend_unique(&mut config.test_roots, adapter.test_roots);
43
+ extend_unique(&mut config.module_roots, adapter.module_roots);
44
+ extend_unique(&mut config.allowed_root_files, adapter.allowed_root_files);
45
+ }
46
+ Ok(config)
47
+ }
48
+
49
+ pub fn validate_structure_adapter_ids(ids: &[String]) -> Result<(), NaomeError> {
50
+ validate_ids(ids, registry(), CONFIG_PATH)
51
+ }
52
+
53
+ fn registry() -> &'static [StructureAdapter] {
54
+ &[
55
+ StructureAdapter {
56
+ id: "rust",
57
+ detect: detects_rust_project,
58
+ source_roots: &["src/**", "crates/*/src/**", "**/crates/*/src/**"],
59
+ test_roots: &["tests/**", "crates/*/tests/**", "**/crates/*/tests/**"],
60
+ module_roots: &["src/**", "crates/*/src/**", "**/crates/*/src/**"],
61
+ allowed_root_files: &["Cargo.toml", "Cargo.lock"],
62
+ },
63
+ StructureAdapter {
64
+ id: "javascript-typescript",
65
+ detect: detects_javascript_typescript_project,
66
+ source_roots: &[
67
+ "src/**",
68
+ "app/**",
69
+ "pages/**",
70
+ "components/**",
71
+ "packages/*/src/**",
72
+ "**/packages/*/src/**",
73
+ ],
74
+ test_roots: &["test/**", "tests/**", "__tests__/**", "packages/*/tests/**"],
75
+ module_roots: &["src/**", "app/**", "packages/*/src/**"],
76
+ allowed_root_files: &[
77
+ "package.json",
78
+ "tsconfig.json",
79
+ "vite.config.ts",
80
+ "next.config.js",
81
+ ],
82
+ },
83
+ ]
84
+ }
@@ -0,0 +1,153 @@
1
+ use crate::paths;
2
+ use crate::quality::types::{QualityMode, QualityViolation};
3
+
4
+ use super::{applies, push};
5
+ use crate::quality::structure::model::{DirectoryRoleRule, RepositoryStructureModel};
6
+
7
+ pub(super) fn root_file_sprawl(
8
+ model: &RepositoryStructureModel,
9
+ mode: QualityMode,
10
+ violations: &mut Vec<QualityViolation>,
11
+ ) {
12
+ for path in model
13
+ .paths
14
+ .iter()
15
+ .filter(|path| applies(path, mode) && path.segments.len() == 1)
16
+ {
17
+ if paths::matches_any(&path.explanation.path, &model.config.allowed_root_files) {
18
+ continue;
19
+ }
20
+ push_single_path(
21
+ "root-file-sprawl",
22
+ &path.explanation.path,
23
+ format!(
24
+ "{} is a root-level file without a recognized root role; place new work inside a module, docs, config, or script directory.",
25
+ path.explanation.path
26
+ ),
27
+ violations,
28
+ );
29
+ }
30
+ }
31
+
32
+ pub(super) fn misplaced_files(
33
+ model: &RepositoryStructureModel,
34
+ mode: QualityMode,
35
+ violations: &mut Vec<QualityViolation>,
36
+ ) {
37
+ for path in model.paths.iter().filter(|path| applies(path, mode)) {
38
+ let role = path.explanation.role.as_str();
39
+ let misplaced_test =
40
+ role == "test" && !paths::matches_any(&path.explanation.path, &model.config.test_roots);
41
+ if misplaced_test {
42
+ push_single_path(
43
+ "misplaced-file-role",
44
+ &path.explanation.path,
45
+ format!(
46
+ "{} is test code outside a configured test root.",
47
+ path.explanation.path
48
+ ),
49
+ violations,
50
+ );
51
+ }
52
+ }
53
+ }
54
+
55
+ pub(super) fn directory_role_mixing(
56
+ model: &RepositoryStructureModel,
57
+ mode: QualityMode,
58
+ violations: &mut Vec<QualityViolation>,
59
+ ) {
60
+ for path in model.paths.iter().filter(|path| {
61
+ applies(path, mode)
62
+ && matches!(path.explanation.role.as_str(), "generated" | "artifact")
63
+ && paths::matches_any(&path.explanation.path, &model.config.source_roots)
64
+ }) {
65
+ if role_rule_allows_path(model, &path.explanation.path, &path.explanation.role) {
66
+ continue;
67
+ }
68
+ push_single_path(
69
+ "directory-role-mixing",
70
+ &path.explanation.path,
71
+ format!(
72
+ "{} is {} content under a source root.",
73
+ path.explanation.path, path.explanation.role
74
+ ),
75
+ violations,
76
+ );
77
+ }
78
+
79
+ for directory in &model.directories {
80
+ let rule = directory_role_rule_for(model, &directory.path);
81
+ let allowed_by_rule = rule
82
+ .filter(|rule| !rule.allowed_roles.is_empty())
83
+ .is_some_and(|rule| {
84
+ directory
85
+ .roles
86
+ .iter()
87
+ .all(|role| rule.allowed_roles.iter().any(|allowed| allowed == role))
88
+ });
89
+ let max_roles = rule
90
+ .and_then(|rule| rule.max_roles)
91
+ .unwrap_or(model.config.limits.max_directory_roles);
92
+ let incompatible = directory.roles.contains("source")
93
+ && (directory.roles.contains("generated")
94
+ || directory.roles.contains("artifact")
95
+ || directory.roles.contains("dependency/vendor"));
96
+ let too_many_roles = directory.roles.len() > max_roles
97
+ && (directory.roles.contains("source")
98
+ || directory.roles.contains("generated")
99
+ || directory.roles.contains("artifact"));
100
+ if (!incompatible || allowed_by_rule) && !too_many_roles {
101
+ continue;
102
+ }
103
+ for path in model.paths.iter().filter(|path| {
104
+ path.explanation.directory == directory.path
105
+ && applies(path, mode)
106
+ && path.explanation.role != "source"
107
+ }) {
108
+ push(
109
+ "directory-role-mixing",
110
+ &path.explanation.path,
111
+ format!(
112
+ "{} mixes incompatible directory roles: {}.",
113
+ directory.path,
114
+ directory
115
+ .roles
116
+ .iter()
117
+ .cloned()
118
+ .collect::<Vec<_>>()
119
+ .join(", ")
120
+ ),
121
+ vec![],
122
+ violations,
123
+ );
124
+ }
125
+ }
126
+ }
127
+
128
+ fn role_rule_allows_path(model: &RepositoryStructureModel, path: &str, role: &str) -> bool {
129
+ model.config.directory_role_rules.iter().any(|rule| {
130
+ !rule.allowed_roles.is_empty()
131
+ && paths::matches_any(path, &rule.paths)
132
+ && rule.allowed_roles.iter().any(|allowed| allowed == role)
133
+ })
134
+ }
135
+
136
+ fn directory_role_rule_for<'a>(
137
+ model: &'a RepositoryStructureModel,
138
+ directory: &str,
139
+ ) -> Option<&'a DirectoryRoleRule> {
140
+ model.config.directory_role_rules.iter().find(|rule| {
141
+ paths::matches_any(directory, &rule.paths)
142
+ || paths::matches_any(&format!("{directory}/_"), &rule.paths)
143
+ })
144
+ }
145
+
146
+ fn push_single_path(
147
+ check_id: &str,
148
+ path: &str,
149
+ message: String,
150
+ violations: &mut Vec<QualityViolation>,
151
+ ) {
152
+ push(check_id, path, message, vec![], violations);
153
+ }