@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.
- package/Cargo.lock +2 -2
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +19 -5
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +2 -1
- package/crates/naome-cli/src/main.rs +3 -0
- package/crates/naome-cli/src/quality_commands.rs +90 -2
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/decision/checks.rs +64 -0
- package/crates/naome-core/src/decision/idle.rs +67 -0
- package/crates/naome-core/src/decision/json.rs +36 -0
- package/crates/naome-core/src/decision/states.rs +165 -0
- package/crates/naome-core/src/decision.rs +131 -353
- package/crates/naome-core/src/install_plan.rs +2 -0
- package/crates/naome-core/src/lib.rs +5 -3
- package/crates/naome-core/src/paths.rs +3 -1
- package/crates/naome-core/src/quality/adapter_support.rs +89 -0
- package/crates/naome-core/src/quality/adapters.rs +20 -67
- package/crates/naome-core/src/quality/cleanup.rs +13 -1
- package/crates/naome-core/src/quality/config.rs +8 -15
- package/crates/naome-core/src/quality/config_support.rs +24 -0
- package/crates/naome-core/src/quality/mod.rs +18 -0
- package/crates/naome-core/src/quality/scanner.rs +20 -8
- package/crates/naome-core/src/quality/structure/adapters.rs +84 -0
- package/crates/naome-core/src/quality/structure/checks/basic.rs +153 -0
- package/crates/naome-core/src/quality/structure/checks/directory.rs +144 -0
- package/crates/naome-core/src/quality/structure/checks/pairing.rs +63 -0
- package/crates/naome-core/src/quality/structure/checks.rs +124 -0
- package/crates/naome-core/src/quality/structure/classify/roles.rs +188 -0
- package/crates/naome-core/src/quality/structure/classify.rs +94 -0
- package/crates/naome-core/src/quality/structure/config.rs +89 -0
- package/crates/naome-core/src/quality/structure/defaults.rs +107 -0
- package/crates/naome-core/src/quality/structure/mod.rs +77 -0
- package/crates/naome-core/src/quality/structure/model.rs +124 -0
- package/crates/naome-core/src/quality/types.rs +3 -0
- package/crates/naome-core/src/route/builtin_checks.rs +155 -0
- package/crates/naome-core/src/route/builtin_context.rs +73 -0
- package/crates/naome-core/src/route/builtin_integrity.rs +49 -0
- package/crates/naome-core/src/route/builtin_require.rs +40 -0
- package/crates/naome-core/src/route/context.rs +180 -0
- package/crates/naome-core/src/route/execution.rs +96 -0
- package/crates/naome-core/src/route/execution_baselines.rs +146 -0
- package/crates/naome-core/src/route/execution_support.rs +57 -0
- package/crates/naome-core/src/route/execution_tasks.rs +71 -0
- package/crates/naome-core/src/route/git_ops.rs +72 -0
- package/crates/naome-core/src/route/quality_gate.rs +73 -0
- package/crates/naome-core/src/route/quality_gate_config.rs +126 -0
- package/crates/naome-core/src/route/quality_gate_snapshot.rs +69 -0
- package/crates/naome-core/src/route/worktree.rs +75 -0
- package/crates/naome-core/src/route/worktree_files.rs +32 -0
- package/crates/naome-core/src/route/worktree_plan.rs +131 -0
- package/crates/naome-core/src/route.rs +44 -1217
- package/crates/naome-core/src/verification.rs +1 -0
- package/crates/naome-core/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +2 -0
- package/crates/naome-core/tests/quality.rs +12 -118
- package/crates/naome-core/tests/quality_structure.rs +116 -0
- package/crates/naome-core/tests/quality_structure_adapters.rs +98 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +125 -0
- package/crates/naome-core/tests/quality_structure_support/mod.rs +249 -0
- package/crates/naome-core/tests/repo_support/mod.rs +16 -0
- package/crates/naome-core/tests/repo_support/repo.rs +113 -0
- package/crates/naome-core/tests/repo_support/repo_factories.rs +99 -0
- package/crates/naome-core/tests/repo_support/repo_helpers.rs +123 -0
- package/crates/naome-core/tests/repo_support/routes.rs +81 -0
- package/crates/naome-core/tests/repo_support/verification.rs +168 -0
- package/crates/naome-core/tests/repo_support/verification_values.rs +135 -0
- package/crates/naome-core/tests/route.rs +1 -1376
- package/crates/naome-core/tests/route_baseline.rs +86 -0
- package/crates/naome-core/tests/route_completion.rs +141 -0
- package/crates/naome-core/tests/route_harness_refresh.rs +135 -0
- package/crates/naome-core/tests/route_user_diff.rs +198 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -0
- package/crates/naome-core/tests/task_state.rs +60 -432
- package/crates/naome-core/tests/task_state_compact_support/repo.rs +1 -1
- package/crates/naome-core/tests/task_state_support/mod.rs +163 -0
- package/crates/naome-core/tests/task_state_support/states.rs +84 -0
- package/crates/naome-core/tests/verification.rs +4 -45
- package/crates/naome-core/tests/verification_contract.rs +22 -78
- package/crates/naome-core/tests/workflow_support/mod.rs +1 -1
- package/installer/agents.js +90 -0
- package/installer/context.js +67 -0
- package/installer/filesystem.js +166 -0
- package/installer/flows.js +84 -0
- package/installer/git-boundary.js +170 -0
- package/installer/git-hook-content.js +36 -0
- package/installer/git-hooks.js +134 -0
- package/installer/git-local.js +2 -0
- package/installer/git-shared.js +35 -0
- package/installer/harness-file-ops.js +140 -0
- package/installer/harness-files.js +56 -0
- package/installer/harness-verification.js +123 -0
- package/installer/install-plan.js +66 -0
- package/installer/main.js +25 -0
- package/installer/manifest-state.js +167 -0
- package/installer/native-build.js +24 -0
- package/installer/native-format.js +6 -0
- package/installer/native.js +162 -0
- package/installer/output.js +131 -0
- package/installer/version.js +32 -0
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +2 -1
- package/templates/naome-root/.naome/bin/check-harness-health.js +2 -2
- package/templates/naome-root/.naome/bin/check-task-state.js +2 -2
- package/templates/naome-root/.naome/bin/naome.js +25 -21
- package/templates/naome-root/.naome/manifest.json +4 -2
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/verification.json +1 -0
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +3 -0
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- 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
|
-
|
|
15
|
-
|
|
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
|
|
26
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 =>
|
|
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(
|
|
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,
|
|
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()
|
|
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
|
+
}
|