@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.
Files changed (155) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +117 -47
  3. package/bin/naome.js +65 -12
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/context_commands.rs +47 -0
  6. package/crates/naome-cli/src/dispatcher.rs +12 -2
  7. package/crates/naome-cli/src/main.rs +78 -29
  8. package/crates/naome-cli/src/quality_commands.rs +238 -34
  9. package/crates/naome-cli/src/quality_output.rs +34 -0
  10. package/crates/naome-cli/src/quality_reconcile_command.rs +45 -0
  11. package/crates/naome-cli/src/repository_model_commands.rs +84 -0
  12. package/crates/naome-cli/src/task_commands.rs +62 -0
  13. package/crates/naome-cli/src/workflow_commands.rs +120 -3
  14. package/crates/naome-core/Cargo.toml +1 -1
  15. package/crates/naome-core/src/context/helpers.rs +75 -0
  16. package/crates/naome-core/src/context/select.rs +134 -0
  17. package/crates/naome-core/src/context/types.rs +43 -0
  18. package/crates/naome-core/src/context.rs +6 -0
  19. package/crates/naome-core/src/decision/states.rs +1 -1
  20. package/crates/naome-core/src/decision.rs +4 -1
  21. package/crates/naome-core/src/git.rs +4 -2
  22. package/crates/naome-core/src/install_plan.rs +20 -0
  23. package/crates/naome-core/src/journal.rs +2 -7
  24. package/crates/naome-core/src/lib.rs +35 -8
  25. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  26. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  27. package/crates/naome-core/src/quality/adapters.rs +81 -18
  28. package/crates/naome-core/src/quality/baseline.rs +8 -0
  29. package/crates/naome-core/src/quality/cache.rs +151 -0
  30. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +19 -8
  31. package/crates/naome-core/src/quality/checks/near_duplicates.rs +4 -2
  32. package/crates/naome-core/src/quality/checks.rs +7 -8
  33. package/crates/naome-core/src/quality/cleanup.rs +36 -3
  34. package/crates/naome-core/src/quality/config.rs +21 -3
  35. package/crates/naome-core/src/quality/mod.rs +189 -10
  36. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  37. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  38. package/crates/naome-core/src/quality/scanner/analysis/normalize.rs +78 -0
  39. package/crates/naome-core/src/quality/scanner/analysis.rs +175 -0
  40. package/crates/naome-core/src/quality/scanner/repo_paths.rs +39 -3
  41. package/crates/naome-core/src/quality/scanner.rs +235 -217
  42. package/crates/naome-core/src/quality/semantic/checks.rs +151 -0
  43. package/crates/naome-core/src/quality/semantic/extract.rs +158 -0
  44. package/crates/naome-core/src/quality/semantic/model.rs +85 -0
  45. package/crates/naome-core/src/quality/semantic/route.rs +52 -0
  46. package/crates/naome-core/src/quality/semantic.rs +68 -0
  47. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  48. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  49. package/crates/naome-core/src/quality/structure/checks/directory.rs +13 -21
  50. package/crates/naome-core/src/quality/structure/checks.rs +1 -1
  51. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  52. package/crates/naome-core/src/quality/structure/classify.rs +52 -0
  53. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  54. package/crates/naome-core/src/quality/structure/mod.rs +5 -2
  55. package/crates/naome-core/src/quality/structure/model.rs +8 -1
  56. package/crates/naome-core/src/quality/types.rs +59 -2
  57. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  58. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  59. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  60. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  61. package/crates/naome-core/src/repository_model/types.rs +152 -0
  62. package/crates/naome-core/src/repository_model/world.rs +48 -0
  63. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  64. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  65. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  66. package/crates/naome-core/src/repository_model.rs +164 -0
  67. package/crates/naome-core/src/route/builtin_checks.rs +41 -16
  68. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  69. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  70. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  71. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  72. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  73. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  74. package/crates/naome-core/src/task_ledger.rs +48 -0
  75. package/crates/naome-core/src/task_state/api.rs +4 -2
  76. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  77. package/crates/naome-core/src/task_state/diff.rs +2 -2
  78. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  79. package/crates/naome-core/src/task_state/mod.rs +1 -1
  80. package/crates/naome-core/src/task_state/progress.rs +13 -0
  81. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  82. package/crates/naome-core/src/task_state/repair.rs +2 -2
  83. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  84. package/crates/naome-core/src/task_state/types.rs +24 -0
  85. package/crates/naome-core/src/verification.rs +29 -18
  86. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  87. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  88. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  89. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  90. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  91. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  92. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  93. package/crates/naome-core/src/workflow/agent.rs +34 -0
  94. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  95. package/crates/naome-core/src/workflow/doctor.rs +183 -0
  96. package/crates/naome-core/src/workflow/mod.rs +13 -0
  97. package/crates/naome-core/src/workflow/mutation.rs +1 -2
  98. package/crates/naome-core/src/workflow/output.rs +8 -2
  99. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  100. package/crates/naome-core/tests/context.rs +99 -0
  101. package/crates/naome-core/tests/harness_health.rs +4 -0
  102. package/crates/naome-core/tests/install_plan.rs +14 -0
  103. package/crates/naome-core/tests/quality.rs +190 -5
  104. package/crates/naome-core/tests/quality_performance.rs +268 -0
  105. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  106. package/crates/naome-core/tests/quality_structure_policy.rs +19 -0
  107. package/crates/naome-core/tests/repo_support/mod.rs +5 -1
  108. package/crates/naome-core/tests/repo_support/verification_values.rs +55 -0
  109. package/crates/naome-core/tests/repository_model.rs +281 -0
  110. package/crates/naome-core/tests/route_user_diff.rs +59 -7
  111. package/crates/naome-core/tests/semantic_legacy.rs +174 -0
  112. package/crates/naome-core/tests/task_ledger.rs +328 -0
  113. package/crates/naome-core/tests/task_state.rs +28 -0
  114. package/crates/naome-core/tests/verification.rs +29 -36
  115. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  116. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  117. package/crates/naome-core/tests/workflow_doctor.rs +45 -0
  118. package/crates/naome-core/tests/workflow_policy.rs +6 -1
  119. package/installer/codex-hooks.js +121 -0
  120. package/installer/context.js +10 -0
  121. package/installer/filesystem.js +4 -0
  122. package/installer/flows.js +8 -4
  123. package/installer/git-boundary.js +1 -0
  124. package/installer/harness-files.js +6 -0
  125. package/installer/install-plan.js +4 -0
  126. package/installer/main.js +1 -1
  127. package/installer/native.js +1 -1
  128. package/native/darwin-arm64/naome +0 -0
  129. package/native/linux-x64/naome +0 -0
  130. package/package.json +1 -1
  131. package/templates/naome-root/.codex/config.toml +2 -0
  132. package/templates/naome-root/.codex/hooks.json +70 -0
  133. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  134. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  135. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  136. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  137. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  138. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  139. package/templates/naome-root/.naome/bin/naome.js +45 -7
  140. package/templates/naome-root/.naome/manifest.json +12 -6
  141. package/templates/naome-root/.naome/repository-model.json +6 -0
  142. package/templates/naome-root/.naome/repository-quality.json +3 -1
  143. package/templates/naome-root/.naome/verification.json +15 -1
  144. package/templates/naome-root/.naomeignore +1 -0
  145. package/templates/naome-root/AGENTS.md +38 -83
  146. package/templates/naome-root/docs/naome/agent-workflow.md +66 -28
  147. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  148. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  149. package/templates/naome-root/docs/naome/first-run.md +25 -14
  150. package/templates/naome-root/docs/naome/index.md +18 -10
  151. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  152. package/templates/naome-root/docs/naome/repository-quality.md +104 -5
  153. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  154. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  155. 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
+ ];