@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,158 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use super::model::ObjectCandidate;
|
|
4
|
+
use crate::quality::scanner::{stable_fingerprint, FileAnalysis, SymbolAnalysis};
|
|
5
|
+
|
|
6
|
+
pub(super) fn extract_object_candidates(file: &FileAnalysis) -> Vec<ObjectCandidate> {
|
|
7
|
+
let mut candidates = Vec::new();
|
|
8
|
+
let mut start: Option<usize> = None;
|
|
9
|
+
let mut depth = 0isize;
|
|
10
|
+
|
|
11
|
+
for (index, line) in file.raw_lines.iter().enumerate() {
|
|
12
|
+
let delta = brace_delta(line);
|
|
13
|
+
if start.is_none() && line.contains('{') && looks_like_object_start(line) {
|
|
14
|
+
start = Some(index);
|
|
15
|
+
}
|
|
16
|
+
if start.is_some() {
|
|
17
|
+
depth += delta;
|
|
18
|
+
if depth <= 0 {
|
|
19
|
+
if let Some(start_index) = start.take() {
|
|
20
|
+
push_candidate(file, start_index, index, &mut candidates);
|
|
21
|
+
}
|
|
22
|
+
depth = 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
candidates
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn push_candidate(
|
|
31
|
+
file: &FileAnalysis,
|
|
32
|
+
start_index: usize,
|
|
33
|
+
end_index: usize,
|
|
34
|
+
candidates: &mut Vec<ObjectCandidate>,
|
|
35
|
+
) {
|
|
36
|
+
let line_count = end_index.saturating_sub(start_index) + 1;
|
|
37
|
+
if line_count < 8 {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let keys = extract_keys(&file.raw_lines[start_index..=end_index]);
|
|
41
|
+
if keys.len() < 3 {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let shape = keys.iter().cloned().collect::<Vec<_>>().join("|");
|
|
45
|
+
let shape_hash = stable_fingerprint(&["semantic-shape", &shape]);
|
|
46
|
+
let start_line = start_index + 1;
|
|
47
|
+
candidates.push(ObjectCandidate {
|
|
48
|
+
path: file.path.clone(),
|
|
49
|
+
start_line,
|
|
50
|
+
end_line: end_index + 1,
|
|
51
|
+
symbol: nearest_symbol(&file.symbols, start_line),
|
|
52
|
+
keys,
|
|
53
|
+
shape_hash,
|
|
54
|
+
line_count,
|
|
55
|
+
in_test_context: is_test_context(&file.path),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn extract_keys(lines: &[String]) -> BTreeSet<String> {
|
|
60
|
+
lines
|
|
61
|
+
.iter()
|
|
62
|
+
.filter_map(|line| key_from_line(line))
|
|
63
|
+
.collect::<BTreeSet<_>>()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn key_from_line(line: &str) -> Option<String> {
|
|
67
|
+
let trimmed = line.trim().trim_start_matches(['{', ',', '[']);
|
|
68
|
+
if trimmed.starts_with("//") || trimmed.starts_with('#') {
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
let (raw_key, _) = trimmed.split_once(':')?;
|
|
72
|
+
let key = raw_key.trim().trim_matches('"').trim_matches('\'').trim();
|
|
73
|
+
let valid = !key.is_empty()
|
|
74
|
+
&& key.len() <= 64
|
|
75
|
+
&& key.chars().all(|character| {
|
|
76
|
+
character.is_ascii_alphanumeric() || character == '_' || character == '-'
|
|
77
|
+
});
|
|
78
|
+
valid.then(|| key.to_ascii_lowercase())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn brace_delta(line: &str) -> isize {
|
|
82
|
+
let mut delta = 0isize;
|
|
83
|
+
let mut in_string = false;
|
|
84
|
+
let mut quote = '\0';
|
|
85
|
+
let mut escaped = false;
|
|
86
|
+
for character in line.chars() {
|
|
87
|
+
if in_string {
|
|
88
|
+
if escaped {
|
|
89
|
+
escaped = false;
|
|
90
|
+
} else if character == '\\' {
|
|
91
|
+
escaped = true;
|
|
92
|
+
} else if character == quote {
|
|
93
|
+
in_string = false;
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if character == '"' || character == '\'' || character == '`' {
|
|
98
|
+
in_string = true;
|
|
99
|
+
quote = character;
|
|
100
|
+
} else if character == '{' {
|
|
101
|
+
delta += 1;
|
|
102
|
+
} else if character == '}' {
|
|
103
|
+
delta -= 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
delta
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn looks_like_object_start(line: &str) -> bool {
|
|
110
|
+
let trimmed = line.trim();
|
|
111
|
+
if starts_code_block(trimmed) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
trimmed.starts_with('{')
|
|
115
|
+
|| trimmed.ends_with('{')
|
|
116
|
+
|| trimmed.contains("= {")
|
|
117
|
+
|| trimmed.contains(": {")
|
|
118
|
+
|| trimmed.contains("({")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn starts_code_block(trimmed: &str) -> bool {
|
|
122
|
+
[
|
|
123
|
+
"fn ",
|
|
124
|
+
"pub fn ",
|
|
125
|
+
"function ",
|
|
126
|
+
"async function ",
|
|
127
|
+
"if ",
|
|
128
|
+
"for ",
|
|
129
|
+
"while ",
|
|
130
|
+
"match ",
|
|
131
|
+
"impl ",
|
|
132
|
+
"mod ",
|
|
133
|
+
"class ",
|
|
134
|
+
"struct ",
|
|
135
|
+
"enum ",
|
|
136
|
+
]
|
|
137
|
+
.iter()
|
|
138
|
+
.any(|prefix| trimmed.starts_with(prefix))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn nearest_symbol(symbols: &[SymbolAnalysis], line: usize) -> Option<String> {
|
|
142
|
+
symbols
|
|
143
|
+
.iter()
|
|
144
|
+
.filter(|symbol| symbol.start_line <= line)
|
|
145
|
+
.max_by_key(|symbol| symbol.start_line)
|
|
146
|
+
.map(|symbol| symbol.name.clone())
|
|
147
|
+
.filter(|name| !name.is_empty())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn is_test_context(path: &str) -> bool {
|
|
151
|
+
let lower = path.to_ascii_lowercase();
|
|
152
|
+
lower.contains("/test")
|
|
153
|
+
|| lower.contains("test/")
|
|
154
|
+
|| lower.contains(".test.")
|
|
155
|
+
|| lower.contains(".spec.")
|
|
156
|
+
|| lower.contains("fixture")
|
|
157
|
+
|| lower.contains("support")
|
|
158
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
use std::collections::BTreeSet;
|
|
2
|
+
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone, Serialize)]
|
|
6
|
+
#[serde(rename_all = "camelCase")]
|
|
7
|
+
pub struct SemanticReport {
|
|
8
|
+
pub schema: String,
|
|
9
|
+
pub mode: String,
|
|
10
|
+
pub ok: bool,
|
|
11
|
+
pub changed_paths: Vec<String>,
|
|
12
|
+
pub summary: SemanticSummary,
|
|
13
|
+
pub findings: Vec<SemanticFinding>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone, Serialize)]
|
|
17
|
+
#[serde(rename_all = "camelCase")]
|
|
18
|
+
pub struct SemanticSummary {
|
|
19
|
+
pub scanned_files: usize,
|
|
20
|
+
pub scanned_path_count: usize,
|
|
21
|
+
pub finding_count: usize,
|
|
22
|
+
pub blocking_finding_count: usize,
|
|
23
|
+
pub truncated: bool,
|
|
24
|
+
pub reason_codes: Vec<String>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Debug, Clone, Serialize)]
|
|
28
|
+
#[serde(rename_all = "camelCase")]
|
|
29
|
+
pub struct SemanticFinding {
|
|
30
|
+
pub id: String,
|
|
31
|
+
pub kind: String,
|
|
32
|
+
pub confidence: f64,
|
|
33
|
+
pub severity: String,
|
|
34
|
+
pub mode: String,
|
|
35
|
+
pub summary: String,
|
|
36
|
+
pub occurrences: Vec<SemanticOccurrence>,
|
|
37
|
+
pub cleanup_route: SemanticCleanupRoute,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Serialize)]
|
|
41
|
+
#[serde(rename_all = "camelCase")]
|
|
42
|
+
pub struct SemanticOccurrence {
|
|
43
|
+
pub path: String,
|
|
44
|
+
pub start_line: usize,
|
|
45
|
+
pub end_line: usize,
|
|
46
|
+
pub symbol: Option<String>,
|
|
47
|
+
pub shape_hash: String,
|
|
48
|
+
pub key_count: usize,
|
|
49
|
+
pub line_count: usize,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Clone, Serialize)]
|
|
53
|
+
#[serde(rename_all = "camelCase")]
|
|
54
|
+
pub struct SemanticCleanupRoute {
|
|
55
|
+
pub intent: String,
|
|
56
|
+
pub target_suggestion: String,
|
|
57
|
+
pub agent_instructions: Vec<String>,
|
|
58
|
+
pub required_checks: Vec<String>,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[derive(Debug, Clone)]
|
|
62
|
+
pub(super) struct ObjectCandidate {
|
|
63
|
+
pub path: String,
|
|
64
|
+
pub start_line: usize,
|
|
65
|
+
pub end_line: usize,
|
|
66
|
+
pub symbol: Option<String>,
|
|
67
|
+
pub keys: BTreeSet<String>,
|
|
68
|
+
pub shape_hash: String,
|
|
69
|
+
pub line_count: usize,
|
|
70
|
+
pub in_test_context: bool,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl ObjectCandidate {
|
|
74
|
+
pub(super) fn occurrence(&self) -> SemanticOccurrence {
|
|
75
|
+
SemanticOccurrence {
|
|
76
|
+
path: self.path.clone(),
|
|
77
|
+
start_line: self.start_line,
|
|
78
|
+
end_line: self.end_line,
|
|
79
|
+
symbol: self.symbol.clone(),
|
|
80
|
+
shape_hash: self.shape_hash.clone(),
|
|
81
|
+
key_count: self.keys.len(),
|
|
82
|
+
line_count: self.line_count,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use super::model::{SemanticCleanupRoute, SemanticOccurrence};
|
|
2
|
+
use crate::quality::scanner::QualityContext;
|
|
3
|
+
use crate::quality::types::QualityMode;
|
|
4
|
+
|
|
5
|
+
pub(super) fn finding_mode(context: &QualityContext) -> &'static str {
|
|
6
|
+
match context.mode {
|
|
7
|
+
QualityMode::ChangedFast => "changed-blocking",
|
|
8
|
+
QualityMode::Report => "report",
|
|
9
|
+
QualityMode::DeepReport => "deep-report",
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub(super) fn cleanup_route(
|
|
14
|
+
intent: &str,
|
|
15
|
+
occurrences: &[SemanticOccurrence],
|
|
16
|
+
first_instruction: &str,
|
|
17
|
+
) -> SemanticCleanupRoute {
|
|
18
|
+
let target_suggestion = target_suggestion(occurrences);
|
|
19
|
+
SemanticCleanupRoute {
|
|
20
|
+
intent: intent.to_string(),
|
|
21
|
+
target_suggestion,
|
|
22
|
+
agent_instructions: vec![
|
|
23
|
+
first_instruction.to_string(),
|
|
24
|
+
"Update every occurrence listed in this finding group; do not leave parallel inline copies behind.".to_string(),
|
|
25
|
+
"Preserve behavior and keep version-specific or schema-specific differences explicit as parameters or named fixtures.".to_string(),
|
|
26
|
+
"Do not auto-remove compatibility coverage unless focused tests prove the behavior remains covered.".to_string(),
|
|
27
|
+
],
|
|
28
|
+
required_checks: vec![
|
|
29
|
+
"naome semantic check --changed".to_string(),
|
|
30
|
+
"naome quality check --changed".to_string(),
|
|
31
|
+
"git diff --check".to_string(),
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn target_suggestion(occurrences: &[SemanticOccurrence]) -> String {
|
|
37
|
+
let Some(first) = occurrences.first() else {
|
|
38
|
+
return "test-support".to_string();
|
|
39
|
+
};
|
|
40
|
+
let directory = first
|
|
41
|
+
.path
|
|
42
|
+
.rsplit_once('/')
|
|
43
|
+
.map(|(directory, _)| directory)
|
|
44
|
+
.unwrap_or(".");
|
|
45
|
+
if directory == "." {
|
|
46
|
+
"test-support".to_string()
|
|
47
|
+
} else if directory.contains("script") {
|
|
48
|
+
format!("{directory}/test-support")
|
|
49
|
+
} else {
|
|
50
|
+
format!("{directory}/fixtures")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
mod checks;
|
|
2
|
+
mod extract;
|
|
3
|
+
mod model;
|
|
4
|
+
mod route;
|
|
5
|
+
|
|
6
|
+
use checks::{copied_config_findings, inline_legacy_fixture_findings};
|
|
7
|
+
use extract::extract_object_candidates;
|
|
8
|
+
pub use model::{SemanticFinding, SemanticReport};
|
|
9
|
+
|
|
10
|
+
use super::scanner::QualityContext;
|
|
11
|
+
use model::SemanticSummary;
|
|
12
|
+
|
|
13
|
+
pub fn run_semantic_checks(context: &QualityContext) -> SemanticReport {
|
|
14
|
+
let mut candidates = context
|
|
15
|
+
.files
|
|
16
|
+
.iter()
|
|
17
|
+
.flat_map(extract_object_candidates)
|
|
18
|
+
.collect::<Vec<_>>();
|
|
19
|
+
candidates.sort_by(|left, right| {
|
|
20
|
+
left.path
|
|
21
|
+
.cmp(&right.path)
|
|
22
|
+
.then(left.start_line.cmp(&right.start_line))
|
|
23
|
+
.then(left.shape_hash.cmp(&right.shape_hash))
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let mut findings = Vec::new();
|
|
27
|
+
findings.extend(copied_config_findings(context, &candidates));
|
|
28
|
+
findings.extend(inline_legacy_fixture_findings(context, &candidates));
|
|
29
|
+
findings.sort_by(|left, right| {
|
|
30
|
+
left.kind
|
|
31
|
+
.cmp(&right.kind)
|
|
32
|
+
.then(left.occurrences[0].path.cmp(&right.occurrences[0].path))
|
|
33
|
+
.then(
|
|
34
|
+
left.occurrences[0]
|
|
35
|
+
.start_line
|
|
36
|
+
.cmp(&right.occurrences[0].start_line),
|
|
37
|
+
)
|
|
38
|
+
.then(left.id.cmp(&right.id))
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
let blocking_finding_count = findings.len();
|
|
42
|
+
SemanticReport {
|
|
43
|
+
schema: "naome.semantic-legacy-report.v1".to_string(),
|
|
44
|
+
mode: context.mode.as_str().to_string(),
|
|
45
|
+
ok: blocking_finding_count == 0,
|
|
46
|
+
changed_paths: context.changed_paths.clone(),
|
|
47
|
+
summary: SemanticSummary {
|
|
48
|
+
scanned_files: context.files.len(),
|
|
49
|
+
scanned_path_count: context.scanned_paths().len(),
|
|
50
|
+
finding_count: findings.len(),
|
|
51
|
+
blocking_finding_count,
|
|
52
|
+
truncated: context.truncated,
|
|
53
|
+
reason_codes: context.reason_codes.clone(),
|
|
54
|
+
},
|
|
55
|
+
findings,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn semantic_route_for_finding(
|
|
60
|
+
report: &SemanticReport,
|
|
61
|
+
finding_id: &str,
|
|
62
|
+
) -> Option<SemanticFinding> {
|
|
63
|
+
report
|
|
64
|
+
.findings
|
|
65
|
+
.iter()
|
|
66
|
+
.find(|finding| finding.id == finding_id)
|
|
67
|
+
.cloned()
|
|
68
|
+
}
|
|
@@ -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
|
+
}
|