@lamentis/naome 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -2
- package/README.md +108 -47
- package/bin/naome-node.js +2 -1579
- package/bin/naome.js +34 -5
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/dispatcher.rs +7 -2
- package/crates/naome-cli/src/main.rs +37 -22
- package/crates/naome-cli/src/quality_commands.rs +317 -10
- package/crates/naome-cli/src/workflow_commands.rs +21 -1
- 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/git.rs +4 -2
- package/crates/naome-core/src/install_plan.rs +4 -0
- package/crates/naome-core/src/lib.rs +12 -6
- 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/baseline.rs +8 -0
- package/crates/naome-core/src/quality/cache.rs +153 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +25 -11
- package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
- package/crates/naome-core/src/quality/checks.rs +7 -8
- package/crates/naome-core/src/quality/cleanup.rs +48 -3
- 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 +72 -6
- package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
- package/crates/naome-core/src/quality/scanner/analysis.rs +160 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
- package/crates/naome-core/src/quality/scanner.rs +200 -215
- package/crates/naome-core/src/quality/semantic/checks.rs +134 -0
- package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
- package/crates/naome-core/src/quality/semantic/model.rs +85 -0
- package/crates/naome-core/src/quality/semantic/route.rs +52 -0
- package/crates/naome-core/src/quality/semantic.rs +68 -0
- 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 +134 -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 +146 -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 +131 -0
- package/crates/naome-core/src/quality/types.rs +43 -2
- package/crates/naome-core/src/route/builtin_checks.rs +141 -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/src/workflow/doctor.rs +144 -0
- package/crates/naome-core/src/workflow/mod.rs +2 -0
- package/crates/naome-core/src/workflow/mutation.rs +1 -2
- package/crates/naome-core/tests/decision.rs +24 -118
- package/crates/naome-core/tests/harness_health.rs +2 -0
- package/crates/naome-core/tests/install_plan.rs +2 -0
- package/crates/naome-core/tests/quality.rs +26 -123
- package/crates/naome-core/tests/quality_performance.rs +231 -0
- 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 +144 -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 +202 -0
- package/crates/naome-core/tests/route_worktree.rs +54 -0
- package/crates/naome-core/tests/semantic_legacy.rs +140 -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_doctor.rs +24 -0
- package/crates/naome-core/tests/workflow_policy.rs +6 -1
- 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 +171 -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 +3 -3
- package/templates/naome-root/.naome/bin/check-task-state.js +3 -3
- package/templates/naome-root/.naome/bin/naome.js +32 -21
- package/templates/naome-root/.naome/manifest.json +5 -3
- package/templates/naome-root/.naome/repository-structure.json +90 -0
- package/templates/naome-root/.naome/verification.json +1 -0
- package/templates/naome-root/.naomeignore +1 -0
- package/templates/naome-root/docs/naome/agent-workflow.md +16 -14
- package/templates/naome-root/docs/naome/index.md +4 -3
- package/templates/naome-root/docs/naome/repository-quality.md +66 -4
- package/templates/naome-root/docs/naome/repository-structure.md +51 -0
- package/templates/naome-root/docs/naome/testing.md +2 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
use std::collections::HashSet;
|
|
2
|
+
|
|
3
|
+
use crate::models::NaomeError;
|
|
4
|
+
|
|
5
|
+
pub(crate) struct RepoSignals<'a> {
|
|
6
|
+
paths: &'a [String],
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl<'a> RepoSignals<'a> {
|
|
10
|
+
pub(crate) fn new(paths: &'a [String]) -> Self {
|
|
11
|
+
Self { paths }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub(crate) fn has_manifest(&self, expected: &str) -> bool {
|
|
15
|
+
let nested_suffix = format!("/{expected}");
|
|
16
|
+
self.paths
|
|
17
|
+
.iter()
|
|
18
|
+
.any(|path| path == expected || path.ends_with(&nested_suffix))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub(crate) fn has_extension(&self, extensions: &[&str]) -> bool {
|
|
22
|
+
self.paths
|
|
23
|
+
.iter()
|
|
24
|
+
.any(|path| extensions.iter().any(|extension| path.ends_with(extension)))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub(crate) trait AdapterDescriptor {
|
|
29
|
+
fn id(&self) -> &'static str;
|
|
30
|
+
fn detects(&self, signals: &RepoSignals<'_>) -> bool;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(crate) fn detected_ids<T: AdapterDescriptor>(paths: &[String], registry: &[T]) -> Vec<String> {
|
|
34
|
+
let signals = RepoSignals::new(paths);
|
|
35
|
+
registry
|
|
36
|
+
.iter()
|
|
37
|
+
.filter(|adapter| adapter.detects(&signals))
|
|
38
|
+
.map(|adapter| adapter.id().to_string())
|
|
39
|
+
.collect()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(crate) fn find_adapter_by_id<'a, T: AdapterDescriptor>(
|
|
43
|
+
registry: &'a [T],
|
|
44
|
+
id: &str,
|
|
45
|
+
config_path: &str,
|
|
46
|
+
) -> Result<&'a T, NaomeError> {
|
|
47
|
+
registry
|
|
48
|
+
.iter()
|
|
49
|
+
.find(|adapter| adapter.id() == id)
|
|
50
|
+
.ok_or_else(|| {
|
|
51
|
+
NaomeError::new(format!(
|
|
52
|
+
"{config_path} enabledAdapters contains unknown adapter '{id}'."
|
|
53
|
+
))
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(crate) fn validate_ids<T: AdapterDescriptor>(
|
|
58
|
+
ids: &[String],
|
|
59
|
+
registry: &[T],
|
|
60
|
+
config_path: &str,
|
|
61
|
+
) -> Result<(), NaomeError> {
|
|
62
|
+
let mut seen = HashSet::new();
|
|
63
|
+
for adapter_id in ids {
|
|
64
|
+
if !seen.insert(adapter_id) {
|
|
65
|
+
return Err(NaomeError::new(format!(
|
|
66
|
+
"{config_path} enabledAdapters contains duplicate adapter '{adapter_id}'."
|
|
67
|
+
)));
|
|
68
|
+
}
|
|
69
|
+
find_adapter_by_id(registry, adapter_id, config_path)?;
|
|
70
|
+
}
|
|
71
|
+
Ok(())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub(crate) fn extend_unique(target: &mut Vec<String>, values: &[&str]) {
|
|
75
|
+
for value in values {
|
|
76
|
+
if !target.iter().any(|existing| existing == value) {
|
|
77
|
+
target.push((*value).to_string());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub(crate) fn detects_rust_project(signals: &RepoSignals<'_>) -> bool {
|
|
83
|
+
signals.has_manifest("Cargo.toml") || signals.has_extension(&[".rs"])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub(crate) fn detects_javascript_typescript_project(signals: &RepoSignals<'_>) -> bool {
|
|
87
|
+
signals.has_manifest("package.json")
|
|
88
|
+
|| signals.has_extension(&[".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"])
|
|
89
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
use std::collections::HashSet;
|
|
2
|
-
|
|
3
1
|
use crate::models::NaomeError;
|
|
4
2
|
|
|
3
|
+
use super::adapter_support::{
|
|
4
|
+
detected_ids, detects_javascript_typescript_project, detects_rust_project, extend_unique,
|
|
5
|
+
find_adapter_by_id, validate_ids, AdapterDescriptor, RepoSignals,
|
|
6
|
+
};
|
|
5
7
|
use super::types::{QualityLimitOverrides, QualityPathRule, RepositoryQualityConfig};
|
|
6
8
|
|
|
9
|
+
const CONFIG_PATH: &str = ".naome/repository-quality.json";
|
|
10
|
+
|
|
7
11
|
pub(crate) struct QualityAdapter {
|
|
8
12
|
pub id: &'static str,
|
|
9
13
|
pub generated_paths: &'static [&'static str],
|
|
@@ -11,44 +15,28 @@ pub(crate) struct QualityAdapter {
|
|
|
11
15
|
path_rules: fn() -> Vec<QualityPathRule>,
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
|
|
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
|
-
}
|
|
@@ -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
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
.
|
|
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::
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
98
|
+
if !context.mode.is_changed() {
|
|
100
99
|
return;
|
|
101
100
|
}
|
|
102
101
|
for file in context
|
|
@@ -56,17 +56,62 @@ pub fn cleanup_route_for_path(
|
|
|
56
56
|
.into_iter()
|
|
57
57
|
.collect::<Vec<_>>();
|
|
58
58
|
related_paths.retain(|related| related != path);
|
|
59
|
+
let topic = if violations.iter().any(|violation| {
|
|
60
|
+
violation.check_id.starts_with("directory-")
|
|
61
|
+
|| violation.check_id == "root-file-sprawl"
|
|
62
|
+
|| violation.check_id == "dumping-ground-directory"
|
|
63
|
+
|| violation.check_id == "path-depth"
|
|
64
|
+
|| violation.check_id == "case-collision"
|
|
65
|
+
|| violation.check_id == "test-source-pairing"
|
|
66
|
+
}) {
|
|
67
|
+
"repository structure"
|
|
68
|
+
} else {
|
|
69
|
+
"repository quality"
|
|
70
|
+
};
|
|
71
|
+
let agent_instructions = cleanup_instructions(path, topic, &violations);
|
|
59
72
|
QualityCleanupRoute {
|
|
60
73
|
schema: "naome.quality-cleanup-route.v1".to_string(),
|
|
61
74
|
path: path.to_string(),
|
|
62
75
|
violations,
|
|
63
76
|
related_paths,
|
|
64
|
-
agent_instructions
|
|
65
|
-
"Reduce or split {path} until every repository-quality violation is gone. Prefer extracting reusable helpers/components over copying code. Keep behavior unchanged, add or preserve focused tests, then run naome quality check --changed before task completion."
|
|
66
|
-
),
|
|
77
|
+
agent_instructions,
|
|
67
78
|
required_checks: vec![
|
|
68
79
|
"naome quality check --changed".to_string(),
|
|
69
80
|
"git diff --check".to_string(),
|
|
70
81
|
],
|
|
71
82
|
}
|
|
72
83
|
}
|
|
84
|
+
|
|
85
|
+
fn cleanup_instructions(path: &str, topic: &str, violations: &[QualityViolation]) -> String {
|
|
86
|
+
let mut instructions = Vec::new();
|
|
87
|
+
instructions.push(format!(
|
|
88
|
+
"Clean up {path} until every {topic} violation is gone while preserving behavior."
|
|
89
|
+
));
|
|
90
|
+
for check_id in violations
|
|
91
|
+
.iter()
|
|
92
|
+
.map(|violation| violation.check_id.as_str())
|
|
93
|
+
.collect::<BTreeSet<_>>()
|
|
94
|
+
{
|
|
95
|
+
instructions.push(match check_id {
|
|
96
|
+
"test-source-pairing" => "Create or update a nearby or module-matched test that exercises the changed source behavior.".to_string(),
|
|
97
|
+
"dumping-ground-directory" => "Move feature logic out of generic utils/helpers/common/shared directories into a named module, or extract a reusable helper with a clear owner.".to_string(),
|
|
98
|
+
"directory-role-mixing" => "Separate source, generated, artifact, and dependency/vendor roles into directories that match repository policy.".to_string(),
|
|
99
|
+
"misplaced-file-role" => "Move the file to a configured root for its role, such as source, test, docs, config, or script.".to_string(),
|
|
100
|
+
"root-file-sprawl" => "Move new root-level work into an existing module, docs, config, or script directory unless it is a recognized root manifest.".to_string(),
|
|
101
|
+
"directory-size" => "Split the directory by module or responsibility until direct file count is below the configured limit.".to_string(),
|
|
102
|
+
"path-depth" => "Shorten the path by moving the file closer to its owning module boundary.".to_string(),
|
|
103
|
+
"case-collision" => "Rename colliding paths so they are distinct on case-insensitive filesystems.".to_string(),
|
|
104
|
+
"file-length" => "Reduce or split the file into focused modules with stable public behavior.".to_string(),
|
|
105
|
+
"function-length" => "Extract cohesive helper functions or components until each function fits the configured limit.".to_string(),
|
|
106
|
+
"top-level-symbols" => "Group related symbols into smaller modules and export only the intended public surface.".to_string(),
|
|
107
|
+
"duplicate-block" | "near-duplicate-function" => "Extract shared behavior into a reusable helper, fixture, builder, or component used by all duplicate call sites.".to_string(),
|
|
108
|
+
"diff-growth" => "Reduce the diff by narrowing scope, splitting generated changes, or moving unrelated cleanup to a separate task.".to_string(),
|
|
109
|
+
_ => format!("Resolve the {check_id} finding without weakening generic policy."),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
instructions.push(
|
|
113
|
+
"Run naome quality check --changed and git diff --check before task completion."
|
|
114
|
+
.to_string(),
|
|
115
|
+
);
|
|
116
|
+
instructions.join(" ")
|
|
117
|
+
}
|
|
@@ -4,6 +4,7 @@ use std::path::Path;
|
|
|
4
4
|
use crate::models::NaomeError;
|
|
5
5
|
|
|
6
6
|
use super::adapters::{apply_enabled_adapters, detected_adapter_ids, validate_adapter_ids};
|
|
7
|
+
use super::config_support::validate_ready_schema;
|
|
7
8
|
use super::scanner::collect_repo_paths;
|
|
8
9
|
use super::types::RepositoryQualityConfig;
|
|
9
10
|
|
|
@@ -46,21 +47,13 @@ fn generated_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError>
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
fn validate_config(config: &RepositoryQualityConfig) -> Result<(), NaomeError> {
|
|
49
|
-
|
|
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
|
+
}
|