@lamentis/naome 1.2.1 → 1.3.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/README.md +117 -47
- package/bin/naome.js +65 -12
- package/crates/naome-cli/Cargo.toml +1 -1
- package/crates/naome-cli/src/context_commands.rs +47 -0
- package/crates/naome-cli/src/dispatcher.rs +12 -2
- package/crates/naome-cli/src/main.rs +78 -29
- package/crates/naome-cli/src/quality_commands.rs +238 -34
- package/crates/naome-cli/src/quality_output.rs +34 -0
- package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
- package/crates/naome-cli/src/repository_model_commands.rs +84 -0
- package/crates/naome-cli/src/task_commands.rs +62 -0
- package/crates/naome-cli/src/workflow_commands.rs +120 -3
- package/crates/naome-core/Cargo.toml +1 -1
- package/crates/naome-core/src/context/helpers.rs +75 -0
- package/crates/naome-core/src/context/select.rs +134 -0
- package/crates/naome-core/src/context/types.rs +43 -0
- package/crates/naome-core/src/context.rs +6 -0
- package/crates/naome-core/src/decision/states.rs +1 -1
- package/crates/naome-core/src/decision.rs +4 -1
- package/crates/naome-core/src/git.rs +4 -2
- package/crates/naome-core/src/install_plan.rs +20 -0
- package/crates/naome-core/src/journal.rs +2 -7
- package/crates/naome-core/src/lib.rs +35 -8
- package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
- package/crates/naome-core/src/quality/adapter_support.rs +67 -0
- package/crates/naome-core/src/quality/adapters.rs +81 -18
- package/crates/naome-core/src/quality/baseline.rs +8 -0
- package/crates/naome-core/src/quality/cache.rs +151 -0
- package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +19 -8
- 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 +36 -3
- package/crates/naome-core/src/quality/config.rs +21 -3
- package/crates/naome-core/src/quality/mod.rs +189 -10
- package/crates/naome-core/src/quality/reconcile.rs +138 -0
- package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
- package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
- package/crates/naome-core/src/quality/scanner/analysis.rs +175 -0
- package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
- package/crates/naome-core/src/quality/scanner.rs +235 -217
- package/crates/naome-core/src/quality/semantic/checks.rs +151 -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/adapter_ios.rs +149 -0
- package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
- package/crates/naome-core/src/quality/structure/checks/directory.rs +13 -21
- package/crates/naome-core/src/quality/structure/checks.rs +1 -1
- package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
- package/crates/naome-core/src/quality/structure/classify.rs +52 -0
- package/crates/naome-core/src/quality/structure/config.rs +24 -3
- package/crates/naome-core/src/quality/structure/mod.rs +5 -2
- package/crates/naome-core/src/quality/structure/model.rs +8 -1
- package/crates/naome-core/src/quality/types.rs +59 -2
- package/crates/naome-core/src/repository_model/detect.rs +188 -0
- package/crates/naome-core/src/repository_model/explain.rs +121 -0
- package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
- package/crates/naome-core/src/repository_model/path_support.rs +59 -0
- package/crates/naome-core/src/repository_model/types.rs +152 -0
- package/crates/naome-core/src/repository_model/world.rs +48 -0
- package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
- package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
- package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
- package/crates/naome-core/src/repository_model.rs +164 -0
- package/crates/naome-core/src/route/builtin_checks.rs +41 -16
- package/crates/naome-core/src/task_ledger/import.rs +142 -0
- package/crates/naome-core/src/task_ledger/model.rs +13 -0
- package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
- package/crates/naome-core/src/task_ledger/read.rs +118 -0
- package/crates/naome-core/src/task_ledger/render.rs +55 -0
- package/crates/naome-core/src/task_ledger/write.rs +38 -0
- package/crates/naome-core/src/task_ledger.rs +48 -0
- package/crates/naome-core/src/task_state/api.rs +4 -2
- package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
- package/crates/naome-core/src/task_state/diff.rs +2 -2
- package/crates/naome-core/src/task_state/evidence.rs +8 -3
- package/crates/naome-core/src/task_state/mod.rs +1 -1
- package/crates/naome-core/src/task_state/progress.rs +13 -0
- package/crates/naome-core/src/task_state/proof_model.rs +8 -8
- package/crates/naome-core/src/task_state/repair.rs +2 -2
- package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
- package/crates/naome-core/src/task_state/types.rs +24 -0
- package/crates/naome-core/src/verification.rs +29 -18
- package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
- package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
- package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
- package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
- package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
- package/crates/naome-core/src/workflow/agent/support.rs +58 -0
- package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
- package/crates/naome-core/src/workflow/agent.rs +34 -0
- package/crates/naome-core/src/workflow/agent_types.rs +105 -0
- package/crates/naome-core/src/workflow/doctor.rs +183 -0
- package/crates/naome-core/src/workflow/mod.rs +13 -0
- package/crates/naome-core/src/workflow/mutation.rs +1 -2
- package/crates/naome-core/src/workflow/output.rs +8 -2
- package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
- package/crates/naome-core/tests/context.rs +99 -0
- package/crates/naome-core/tests/harness_health.rs +4 -0
- package/crates/naome-core/tests/install_plan.rs +14 -0
- package/crates/naome-core/tests/quality.rs +190 -5
- package/crates/naome-core/tests/quality_performance.rs +268 -0
- package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
- package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
- package/crates/naome-core/tests/repo_support/mod.rs +5 -1
- package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
- package/crates/naome-core/tests/repository_model.rs +281 -0
- package/crates/naome-core/tests/route_user_diff.rs +59 -7
- package/crates/naome-core/tests/semantic_legacy.rs +174 -0
- package/crates/naome-core/tests/task_ledger.rs +328 -0
- package/crates/naome-core/tests/task_state.rs +28 -0
- package/crates/naome-core/tests/verification.rs +29 -36
- package/crates/naome-core/tests/workflow_agent.rs +233 -0
- package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
- package/crates/naome-core/tests/workflow_doctor.rs +45 -0
- package/crates/naome-core/tests/workflow_policy.rs +6 -1
- package/installer/codex-hooks.js +121 -0
- package/installer/context.js +10 -0
- package/installer/filesystem.js +4 -0
- package/installer/flows.js +8 -4
- package/installer/git-boundary.js +1 -0
- package/installer/harness-files.js +6 -0
- package/installer/install-plan.js +4 -0
- package/installer/main.js +1 -1
- package/installer/native.js +1 -1
- package/native/darwin-arm64/naome +0 -0
- package/native/linux-x64/naome +0 -0
- package/package.json +1 -1
- package/templates/naome-root/.codex/config.toml +2 -0
- package/templates/naome-root/.codex/hooks.json +70 -0
- package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
- package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
- package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
- package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
- package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
- package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
- package/templates/naome-root/.naome/bin/naome.js +45 -7
- package/templates/naome-root/.naome/manifest.json +12 -6
- package/templates/naome-root/.naome/repository-model.json +6 -0
- package/templates/naome-root/.naome/repository-quality.json +3 -1
- package/templates/naome-root/.naome/verification.json +15 -1
- package/templates/naome-root/.naomeignore +1 -0
- package/templates/naome-root/AGENTS.md +38 -83
- package/templates/naome-root/docs/naome/agent-workflow.md +66 -28
- package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
- package/templates/naome-root/docs/naome/context-economy.md +73 -0
- package/templates/naome-root/docs/naome/first-run.md +25 -14
- package/templates/naome-root/docs/naome/index.md +18 -10
- package/templates/naome-root/docs/naome/repository-model.md +92 -0
- package/templates/naome-root/docs/naome/repository-quality.md +104 -5
- package/templates/naome-root/docs/naome/repository-structure.md +10 -3
- package/templates/naome-root/docs/naome/task-ledger.md +71 -0
- package/templates/naome-root/docs/naome/testing.md +16 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
|
|
3
|
+
use super::model::{ObjectCandidate, SemanticFinding};
|
|
4
|
+
use super::route::{cleanup_route, finding_mode};
|
|
5
|
+
use crate::quality::scanner::{stable_fingerprint, QualityContext};
|
|
6
|
+
|
|
7
|
+
pub(super) fn copied_config_findings(
|
|
8
|
+
context: &QualityContext,
|
|
9
|
+
candidates: &[ObjectCandidate],
|
|
10
|
+
) -> Vec<SemanticFinding> {
|
|
11
|
+
let mut by_shape: BTreeMap<&str, Vec<&ObjectCandidate>> = BTreeMap::new();
|
|
12
|
+
for candidate in candidates
|
|
13
|
+
.iter()
|
|
14
|
+
.filter(|candidate| candidate.keys.len() >= 4 && candidate.line_count >= 8)
|
|
15
|
+
.filter(|candidate| has_config_shape_signal(&candidate.keys))
|
|
16
|
+
{
|
|
17
|
+
by_shape
|
|
18
|
+
.entry(candidate.shape_hash.as_str())
|
|
19
|
+
.or_default()
|
|
20
|
+
.push(candidate);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
by_shape
|
|
24
|
+
.into_values()
|
|
25
|
+
.filter(|group| group.len() >= 2)
|
|
26
|
+
.filter(|group| group_applies_to_mode(context, group))
|
|
27
|
+
.map(|group| {
|
|
28
|
+
let occurrences = group
|
|
29
|
+
.iter()
|
|
30
|
+
.map(|candidate| candidate.occurrence())
|
|
31
|
+
.collect::<Vec<_>>();
|
|
32
|
+
let primary = &occurrences[0];
|
|
33
|
+
let id = stable_fingerprint(&[
|
|
34
|
+
"semantic",
|
|
35
|
+
"copied-config-object",
|
|
36
|
+
&primary.shape_hash,
|
|
37
|
+
&primary.path,
|
|
38
|
+
]);
|
|
39
|
+
SemanticFinding {
|
|
40
|
+
id,
|
|
41
|
+
kind: "copied-config-object".to_string(),
|
|
42
|
+
confidence: 0.92,
|
|
43
|
+
severity: "medium".to_string(),
|
|
44
|
+
mode: finding_mode(context).to_string(),
|
|
45
|
+
summary: format!(
|
|
46
|
+
"Same config-like object shape appears in {} locations.",
|
|
47
|
+
occurrences.len()
|
|
48
|
+
),
|
|
49
|
+
cleanup_route: cleanup_route(
|
|
50
|
+
"Extract shared fixture builder",
|
|
51
|
+
&occurrences,
|
|
52
|
+
"Create a shared fixture or builder for the repeated object shape.",
|
|
53
|
+
),
|
|
54
|
+
occurrences,
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
.collect()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub(super) fn inline_legacy_fixture_findings(
|
|
61
|
+
context: &QualityContext,
|
|
62
|
+
candidates: &[ObjectCandidate],
|
|
63
|
+
) -> Vec<SemanticFinding> {
|
|
64
|
+
candidates
|
|
65
|
+
.iter()
|
|
66
|
+
.filter(|candidate| context.applies_to(&candidate.path))
|
|
67
|
+
.filter(|candidate| candidate.in_test_context)
|
|
68
|
+
.filter(|candidate| !is_shared_fixture_factory(candidate))
|
|
69
|
+
.filter(|candidate| candidate.line_count >= 8)
|
|
70
|
+
.filter(|candidate| has_legacy_fixture_signal(&candidate.keys))
|
|
71
|
+
.map(|candidate| {
|
|
72
|
+
let occurrence = candidate.occurrence();
|
|
73
|
+
let id = stable_fingerprint(&[
|
|
74
|
+
"semantic",
|
|
75
|
+
"inline-legacy-fixture",
|
|
76
|
+
&occurrence.shape_hash,
|
|
77
|
+
&occurrence.path,
|
|
78
|
+
&occurrence.start_line.to_string(),
|
|
79
|
+
]);
|
|
80
|
+
SemanticFinding {
|
|
81
|
+
id,
|
|
82
|
+
kind: "inline-legacy-fixture".to_string(),
|
|
83
|
+
confidence: 0.86,
|
|
84
|
+
severity: "medium".to_string(),
|
|
85
|
+
mode: finding_mode(context).to_string(),
|
|
86
|
+
summary: "Large schema/version fixture is inline in a test or support context."
|
|
87
|
+
.to_string(),
|
|
88
|
+
cleanup_route: cleanup_route(
|
|
89
|
+
"Extract compatibility fixture",
|
|
90
|
+
std::slice::from_ref(&occurrence),
|
|
91
|
+
"Move the inline legacy fixture into a shared test fixture factory and keep version-specific differences explicit.",
|
|
92
|
+
),
|
|
93
|
+
occurrences: vec![occurrence],
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.collect()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn has_config_shape_signal(keys: &BTreeSet<String>) -> bool {
|
|
100
|
+
keys.contains("schema")
|
|
101
|
+
|| keys.contains("version")
|
|
102
|
+
|| keys.contains("status")
|
|
103
|
+
|| keys.contains("enabledadapters")
|
|
104
|
+
|| keys.contains("disabledchecks")
|
|
105
|
+
|| keys.contains("limits")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn has_legacy_fixture_signal(keys: &BTreeSet<String>) -> bool {
|
|
109
|
+
(keys.contains("schema") && keys.contains("version"))
|
|
110
|
+
|| (keys.contains("version") && keys.contains("status"))
|
|
111
|
+
|| (keys.contains("fromversion") && keys.contains("toversion"))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn is_shared_fixture_factory(candidate: &ObjectCandidate) -> bool {
|
|
115
|
+
if is_shared_fixture_path(&candidate.path) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
let Some(symbol) = &candidate.symbol else {
|
|
119
|
+
return false;
|
|
120
|
+
};
|
|
121
|
+
let normalized = symbol.to_ascii_lowercase();
|
|
122
|
+
[
|
|
123
|
+
"builder", "contract", "factory", "fixture", "make", "template",
|
|
124
|
+
]
|
|
125
|
+
.iter()
|
|
126
|
+
.any(|marker| normalized.contains(marker))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn is_shared_fixture_path(path: &str) -> bool {
|
|
130
|
+
let normalized = path.replace('\\', "/").to_ascii_lowercase();
|
|
131
|
+
[
|
|
132
|
+
"/fixtures/",
|
|
133
|
+
"/fixture/",
|
|
134
|
+
"/support/",
|
|
135
|
+
"_support/",
|
|
136
|
+
"-support/",
|
|
137
|
+
"/test-support.",
|
|
138
|
+
]
|
|
139
|
+
.iter()
|
|
140
|
+
.any(|marker| normalized.contains(marker))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn group_applies_to_mode(context: &QualityContext, group: &[&ObjectCandidate]) -> bool {
|
|
144
|
+
if context.mode.is_deep() {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
context.mode.is_changed()
|
|
148
|
+
&& group
|
|
149
|
+
.iter()
|
|
150
|
+
.any(|candidate| context.applies_to(&candidate.path))
|
|
151
|
+
}
|
|
@@ -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 | QualityMode::PathScoped => "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,149 @@
|
|
|
1
|
+
use crate::quality::adapter_support::{
|
|
2
|
+
detects_generated_ios_project, detects_ios_app_structure_project,
|
|
3
|
+
detects_ios_resources_project, detects_swift_package_project, detects_swift_project,
|
|
4
|
+
detects_swiftui_project, detects_xcode_project, detects_xctest_project,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use super::adapters::StructureAdapter;
|
|
8
|
+
|
|
9
|
+
pub(super) fn adapters() -> Vec<StructureAdapter> {
|
|
10
|
+
let mut adapters = Vec::new();
|
|
11
|
+
adapters.extend_from_slice(IOS_CORE_STRUCTURE_ADAPTERS);
|
|
12
|
+
adapters.extend_from_slice(IOS_APP_STRUCTURE_ADAPTERS);
|
|
13
|
+
adapters
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const IOS_CORE_STRUCTURE_ADAPTERS: &[StructureAdapter] = &[
|
|
17
|
+
StructureAdapter {
|
|
18
|
+
id: "swift",
|
|
19
|
+
detect: detects_swift_project,
|
|
20
|
+
source_roots: &[
|
|
21
|
+
"Sources/**",
|
|
22
|
+
"**/Sources/**",
|
|
23
|
+
"Source/**",
|
|
24
|
+
"App/**",
|
|
25
|
+
"Features/**",
|
|
26
|
+
"Modules/**",
|
|
27
|
+
"Shared/**",
|
|
28
|
+
],
|
|
29
|
+
test_roots: &[],
|
|
30
|
+
generated_roots: &[],
|
|
31
|
+
artifact_roots: &[],
|
|
32
|
+
module_roots: &[
|
|
33
|
+
"Sources/*/**",
|
|
34
|
+
"**/Sources/*/**",
|
|
35
|
+
"App/**",
|
|
36
|
+
"Features/*/**",
|
|
37
|
+
"Modules/*/**",
|
|
38
|
+
],
|
|
39
|
+
allowed_root_files: &[],
|
|
40
|
+
},
|
|
41
|
+
StructureAdapter {
|
|
42
|
+
id: "xcode",
|
|
43
|
+
detect: detects_xcode_project,
|
|
44
|
+
source_roots: &[],
|
|
45
|
+
test_roots: &[],
|
|
46
|
+
generated_roots: &[],
|
|
47
|
+
artifact_roots: &[
|
|
48
|
+
"DerivedData/**",
|
|
49
|
+
"**/DerivedData/**",
|
|
50
|
+
"Build/**",
|
|
51
|
+
"**/Build/**",
|
|
52
|
+
"**/*.xcarchive/**",
|
|
53
|
+
],
|
|
54
|
+
module_roots: &[],
|
|
55
|
+
allowed_root_files: &["*.xcodeproj", "*.xcworkspace"],
|
|
56
|
+
},
|
|
57
|
+
StructureAdapter {
|
|
58
|
+
id: "xctest",
|
|
59
|
+
detect: detects_xctest_project,
|
|
60
|
+
source_roots: &[],
|
|
61
|
+
test_roots: &[
|
|
62
|
+
"Tests/**",
|
|
63
|
+
"**/Tests/**",
|
|
64
|
+
"*Tests/**",
|
|
65
|
+
"**/*Tests/**",
|
|
66
|
+
"*UITests/**",
|
|
67
|
+
"**/*UITests/**",
|
|
68
|
+
],
|
|
69
|
+
generated_roots: &[],
|
|
70
|
+
artifact_roots: &[],
|
|
71
|
+
module_roots: &[],
|
|
72
|
+
allowed_root_files: &[],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const IOS_APP_STRUCTURE_ADAPTERS: &[StructureAdapter] = &[
|
|
77
|
+
StructureAdapter {
|
|
78
|
+
id: "swiftui",
|
|
79
|
+
detect: detects_swiftui_project,
|
|
80
|
+
source_roots: &[
|
|
81
|
+
"Views/**",
|
|
82
|
+
"**/Views/**",
|
|
83
|
+
"Preview Content/**",
|
|
84
|
+
"**/Preview Content/**",
|
|
85
|
+
],
|
|
86
|
+
test_roots: &[],
|
|
87
|
+
generated_roots: &[],
|
|
88
|
+
artifact_roots: &[],
|
|
89
|
+
module_roots: &["Views/*/**", "**/Views/*/**"],
|
|
90
|
+
allowed_root_files: &[],
|
|
91
|
+
},
|
|
92
|
+
StructureAdapter {
|
|
93
|
+
id: "ios-app-structure",
|
|
94
|
+
detect: detects_ios_app_structure_project,
|
|
95
|
+
source_roots: &[
|
|
96
|
+
"App/**",
|
|
97
|
+
"Application/**",
|
|
98
|
+
"Features/**",
|
|
99
|
+
"Modules/**",
|
|
100
|
+
"Shared/**",
|
|
101
|
+
],
|
|
102
|
+
test_roots: &[],
|
|
103
|
+
generated_roots: &[],
|
|
104
|
+
artifact_roots: &[],
|
|
105
|
+
module_roots: &["Features/*/**", "Modules/*/**", "App/**"],
|
|
106
|
+
allowed_root_files: &["Info.plist", "*.entitlements", "Podfile", "Cartfile"],
|
|
107
|
+
},
|
|
108
|
+
StructureAdapter {
|
|
109
|
+
id: "swift-package",
|
|
110
|
+
detect: detects_swift_package_project,
|
|
111
|
+
source_roots: &["Sources/**", "**/Sources/**"],
|
|
112
|
+
test_roots: &["Tests/**", "**/Tests/**"],
|
|
113
|
+
generated_roots: &[],
|
|
114
|
+
artifact_roots: &[],
|
|
115
|
+
module_roots: &["Sources/*/**", "**/Sources/*/**"],
|
|
116
|
+
allowed_root_files: &["Package.swift", "Package.resolved"],
|
|
117
|
+
},
|
|
118
|
+
StructureAdapter {
|
|
119
|
+
id: "ios-resources",
|
|
120
|
+
detect: detects_ios_resources_project,
|
|
121
|
+
source_roots: &["Resources/**", "**/Resources/**"],
|
|
122
|
+
test_roots: &[],
|
|
123
|
+
generated_roots: &[],
|
|
124
|
+
artifact_roots: &[],
|
|
125
|
+
module_roots: &["Resources/**", "**/Resources/**"],
|
|
126
|
+
allowed_root_files: &["Info.plist", "*.entitlements"],
|
|
127
|
+
},
|
|
128
|
+
StructureAdapter {
|
|
129
|
+
id: "generated-ios",
|
|
130
|
+
detect: detects_generated_ios_project,
|
|
131
|
+
source_roots: &[],
|
|
132
|
+
test_roots: &[],
|
|
133
|
+
generated_roots: &[
|
|
134
|
+
"Generated/**",
|
|
135
|
+
"**/Generated/**",
|
|
136
|
+
"SwiftGen/**",
|
|
137
|
+
"**/SwiftGen/**",
|
|
138
|
+
"Sourcery/**",
|
|
139
|
+
"**/Sourcery/**",
|
|
140
|
+
"**/*.generated.swift",
|
|
141
|
+
"**/*.pb.swift",
|
|
142
|
+
"**/*.grpc.swift",
|
|
143
|
+
"**/R.generated.swift",
|
|
144
|
+
],
|
|
145
|
+
artifact_roots: &[],
|
|
146
|
+
module_roots: &[],
|
|
147
|
+
allowed_root_files: &[],
|
|
148
|
+
},
|
|
149
|
+
];
|