@lamentis/naome 1.3.0 → 1.3.2

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 (149) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +11 -2
  3. package/bin/naome.js +62 -24
  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 +6 -0
  7. package/crates/naome-cli/src/main.rs +43 -6
  8. package/crates/naome-cli/src/quality_commands.rs +31 -46
  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 +100 -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/install_plan.rs +18 -0
  22. package/crates/naome-core/src/intent/resolver_catalog/active.rs +38 -0
  23. package/crates/naome-core/src/intent/resolver_catalog/baseline.rs +44 -0
  24. package/crates/naome-core/src/intent/resolver_catalog/completed.rs +56 -0
  25. package/crates/naome-core/src/intent/resolver_catalog/dirty.rs +32 -0
  26. package/crates/naome-core/src/intent/resolver_catalog/ready.rs +32 -0
  27. package/crates/naome-core/src/intent/resolver_catalog/system.rs +20 -0
  28. package/crates/naome-core/src/intent/resolver_catalog.rs +12 -166
  29. package/crates/naome-core/src/journal.rs +2 -7
  30. package/crates/naome-core/src/lib.rs +33 -10
  31. package/crates/naome-core/src/quality/adapter_ios.rs +131 -0
  32. package/crates/naome-core/src/quality/adapter_support.rs +67 -0
  33. package/crates/naome-core/src/quality/adapters.rs +81 -18
  34. package/crates/naome-core/src/quality/cache.rs +7 -9
  35. package/crates/naome-core/src/quality/checks/duplicate_blocks.rs +4 -7
  36. package/crates/naome-core/src/quality/config.rs +21 -3
  37. package/crates/naome-core/src/quality/mod.rs +138 -7
  38. package/crates/naome-core/src/quality/reconcile.rs +138 -0
  39. package/crates/naome-core/src/quality/reconcile_anchors.rs +64 -0
  40. package/crates/naome-core/src/quality/scanner/analysis.rs +20 -5
  41. package/crates/naome-core/src/quality/scanner.rs +62 -17
  42. package/crates/naome-core/src/quality/semantic/checks.rs +17 -0
  43. package/crates/naome-core/src/quality/semantic/route.rs +1 -1
  44. package/crates/naome-core/src/quality/structure/adapter_ios.rs +149 -0
  45. package/crates/naome-core/src/quality/structure/adapters.rs +60 -42
  46. package/crates/naome-core/src/quality/structure/checks/directory.rs +6 -4
  47. package/crates/naome-core/src/quality/structure/classify/roles.rs +51 -5
  48. package/crates/naome-core/src/quality/structure/config.rs +24 -3
  49. package/crates/naome-core/src/quality/structure/mod.rs +3 -0
  50. package/crates/naome-core/src/quality/types.rs +20 -1
  51. package/crates/naome-core/src/repository_model/detect.rs +188 -0
  52. package/crates/naome-core/src/repository_model/explain.rs +121 -0
  53. package/crates/naome-core/src/repository_model/path_scan.rs +67 -0
  54. package/crates/naome-core/src/repository_model/path_support.rs +59 -0
  55. package/crates/naome-core/src/repository_model/types.rs +152 -0
  56. package/crates/naome-core/src/repository_model/world.rs +48 -0
  57. package/crates/naome-core/src/repository_model/world_adapters.rs +145 -0
  58. package/crates/naome-core/src/repository_model/world_path_facts.rs +55 -0
  59. package/crates/naome-core/src/repository_model/world_paths.rs +168 -0
  60. package/crates/naome-core/src/repository_model.rs +164 -0
  61. package/crates/naome-core/src/route/builtin_checks.rs +40 -1
  62. package/crates/naome-core/src/task_ledger/import.rs +142 -0
  63. package/crates/naome-core/src/task_ledger/model.rs +13 -0
  64. package/crates/naome-core/src/task_ledger/proof_record.rs +52 -0
  65. package/crates/naome-core/src/task_ledger/read.rs +118 -0
  66. package/crates/naome-core/src/task_ledger/render.rs +55 -0
  67. package/crates/naome-core/src/task_ledger/write.rs +38 -0
  68. package/crates/naome-core/src/task_ledger.rs +48 -0
  69. package/crates/naome-core/src/task_state/api.rs +4 -2
  70. package/crates/naome-core/src/task_state/completed_refresh.rs +5 -16
  71. package/crates/naome-core/src/task_state/diff.rs +2 -2
  72. package/crates/naome-core/src/task_state/evidence.rs +8 -3
  73. package/crates/naome-core/src/task_state/mod.rs +1 -1
  74. package/crates/naome-core/src/task_state/progress.rs +13 -0
  75. package/crates/naome-core/src/task_state/proof_model.rs +8 -8
  76. package/crates/naome-core/src/task_state/repair.rs +2 -2
  77. package/crates/naome-core/src/task_state/task_diff_api.rs +9 -18
  78. package/crates/naome-core/src/task_state/types.rs +24 -0
  79. package/crates/naome-core/src/verification.rs +29 -18
  80. package/crates/naome-core/src/workflow/agent/capability.rs +194 -0
  81. package/crates/naome-core/src/workflow/agent/context_delta.rs +42 -0
  82. package/crates/naome-core/src/workflow/agent/decision.rs +32 -0
  83. package/crates/naome-core/src/workflow/agent/execution.rs +80 -0
  84. package/crates/naome-core/src/workflow/agent/proof.rs +24 -0
  85. package/crates/naome-core/src/workflow/agent/support.rs +58 -0
  86. package/crates/naome-core/src/workflow/agent/watchdog.rs +47 -0
  87. package/crates/naome-core/src/workflow/agent.rs +34 -0
  88. package/crates/naome-core/src/workflow/agent_types.rs +105 -0
  89. package/crates/naome-core/src/workflow/doctor.rs +39 -0
  90. package/crates/naome-core/src/workflow/mod.rs +11 -0
  91. package/crates/naome-core/src/workflow/output.rs +8 -2
  92. package/crates/naome-core/src/workflow/phase_inference.rs +1 -1
  93. package/crates/naome-core/tests/context.rs +99 -0
  94. package/crates/naome-core/tests/harness_health.rs +33 -40
  95. package/crates/naome-core/tests/install_plan.rs +12 -0
  96. package/crates/naome-core/tests/quality.rs +178 -2
  97. package/crates/naome-core/tests/quality_performance.rs +39 -2
  98. package/crates/naome-core/tests/quality_structure_adapters.rs +39 -0
  99. package/crates/naome-core/tests/repo_support/mod.rs +7 -1
  100. package/crates/naome-core/tests/repo_support/verification_values.rs +148 -1
  101. package/crates/naome-core/tests/repository_model.rs +281 -0
  102. package/crates/naome-core/tests/route_user_diff.rs +49 -1
  103. package/crates/naome-core/tests/semantic_legacy.rs +72 -38
  104. package/crates/naome-core/tests/task_ledger.rs +328 -0
  105. package/crates/naome-core/tests/task_state.rs +34 -14
  106. package/crates/naome-core/tests/task_state_support/mod.rs +2 -1
  107. package/crates/naome-core/tests/task_state_support/states.rs +28 -11
  108. package/crates/naome-core/tests/verification.rs +14 -39
  109. package/crates/naome-core/tests/verification_contract.rs +6 -52
  110. package/crates/naome-core/tests/workflow_agent.rs +233 -0
  111. package/crates/naome-core/tests/workflow_agent_support/mod.rs +159 -0
  112. package/crates/naome-core/tests/workflow_doctor.rs +21 -0
  113. package/crates/naome-core/tests/workflow_integrity.rs +2 -20
  114. package/crates/naome-core/tests/workflow_support/mod.rs +59 -20
  115. package/installer/codex-hooks.js +121 -0
  116. package/installer/context.js +10 -0
  117. package/installer/filesystem.js +4 -0
  118. package/installer/flows.js +8 -4
  119. package/installer/harness-files.js +6 -0
  120. package/installer/install-plan.js +4 -0
  121. package/installer/main.js +1 -1
  122. package/installer/native.js +1 -1
  123. package/native/darwin-arm64/naome +0 -0
  124. package/native/linux-x64/naome +0 -0
  125. package/package.json +1 -1
  126. package/templates/naome-root/.codex/config.toml +2 -0
  127. package/templates/naome-root/.codex/hooks.json +70 -0
  128. package/templates/naome-root/.naome/bin/check-harness-health.js +8 -6
  129. package/templates/naome-root/.naome/bin/check-task-state.js +12 -7
  130. package/templates/naome-root/.naome/bin/codex-hook-io.js +122 -0
  131. package/templates/naome-root/.naome/bin/codex-hook-policy.js +180 -0
  132. package/templates/naome-root/.naome/bin/codex-hook-runtime.js +174 -0
  133. package/templates/naome-root/.naome/bin/codex-hook.js +6 -0
  134. package/templates/naome-root/.naome/bin/naome.js +35 -4
  135. package/templates/naome-root/.naome/manifest.json +12 -6
  136. package/templates/naome-root/.naome/repository-model.json +6 -0
  137. package/templates/naome-root/.naome/repository-quality.json +3 -1
  138. package/templates/naome-root/.naome/verification.json +15 -1
  139. package/templates/naome-root/AGENTS.md +38 -83
  140. package/templates/naome-root/docs/naome/agent-workflow.md +54 -18
  141. package/templates/naome-root/docs/naome/codex-hooks.md +82 -0
  142. package/templates/naome-root/docs/naome/context-economy.md +73 -0
  143. package/templates/naome-root/docs/naome/first-run.md +25 -14
  144. package/templates/naome-root/docs/naome/index.md +18 -10
  145. package/templates/naome-root/docs/naome/repository-model.md +92 -0
  146. package/templates/naome-root/docs/naome/repository-quality.md +47 -7
  147. package/templates/naome-root/docs/naome/repository-structure.md +10 -3
  148. package/templates/naome-root/docs/naome/task-ledger.md +71 -0
  149. package/templates/naome-root/docs/naome/testing.md +16 -3
@@ -23,6 +23,25 @@ impl<'a> RepoSignals<'a> {
23
23
  .iter()
24
24
  .any(|path| extensions.iter().any(|extension| path.ends_with(extension)))
25
25
  }
26
+
27
+ pub(crate) fn has_path_containing(&self, fragments: &[&str]) -> bool {
28
+ self.paths.iter().any(|path| {
29
+ let lower = path.to_ascii_lowercase();
30
+ fragments.iter().any(|fragment| lower.contains(fragment))
31
+ })
32
+ }
33
+
34
+ pub(crate) fn has_file_name(&self, names: &[&str]) -> bool {
35
+ self.paths.iter().any(|path| {
36
+ let lower = path.to_ascii_lowercase();
37
+ path.rsplit('/').next().is_some_and(|file_name| {
38
+ names
39
+ .iter()
40
+ .any(|name| file_name.eq_ignore_ascii_case(name))
41
+ || names.iter().any(|name| lower.ends_with(name))
42
+ })
43
+ })
44
+ }
26
45
  }
27
46
 
28
47
  pub(crate) trait AdapterDescriptor {
@@ -87,3 +106,51 @@ pub(crate) fn detects_javascript_typescript_project(signals: &RepoSignals<'_>) -
87
106
  signals.has_manifest("package.json")
88
107
  || signals.has_extension(&[".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"])
89
108
  }
109
+
110
+ pub(crate) fn detects_swift_project(signals: &RepoSignals<'_>) -> bool {
111
+ signals.has_manifest("Package.swift") || signals.has_extension(&[".swift"])
112
+ }
113
+
114
+ pub(crate) fn detects_xcode_project(signals: &RepoSignals<'_>) -> bool {
115
+ signals.has_path_containing(&[".xcodeproj/", ".xcworkspace/"])
116
+ }
117
+
118
+ pub(crate) fn detects_xctest_project(signals: &RepoSignals<'_>) -> bool {
119
+ signals.has_path_containing(&["tests/"])
120
+ && signals.has_file_name(&["tests.swift", "uitests.swift"])
121
+ }
122
+
123
+ pub(crate) fn detects_swiftui_project(signals: &RepoSignals<'_>) -> bool {
124
+ signals.has_file_name(&["view.swift", "app.swift"])
125
+ || signals.has_path_containing(&["preview content/"])
126
+ }
127
+
128
+ pub(crate) fn detects_ios_app_structure_project(signals: &RepoSignals<'_>) -> bool {
129
+ detects_xcode_project(signals)
130
+ || signals.has_file_name(&["appdelegate.swift", "scenedelegate.swift", "info.plist"])
131
+ || signals.has_path_containing(&[".xcassets/"])
132
+ }
133
+
134
+ pub(crate) fn detects_swift_package_project(signals: &RepoSignals<'_>) -> bool {
135
+ signals.has_manifest("Package.swift")
136
+ || signals.has_path_containing(&["sources/", "tests/"])
137
+ && signals.has_extension(&[".swift"])
138
+ }
139
+
140
+ pub(crate) fn detects_ios_resources_project(signals: &RepoSignals<'_>) -> bool {
141
+ signals.has_path_containing(&[".xcassets/"])
142
+ || signals.has_extension(&[
143
+ ".strings",
144
+ ".stringsdict",
145
+ ".plist",
146
+ ".entitlements",
147
+ ".storyboard",
148
+ ".xib",
149
+ ])
150
+ }
151
+
152
+ pub(crate) fn detects_generated_ios_project(signals: &RepoSignals<'_>) -> bool {
153
+ signals.has_path_containing(&["generated/", "swiftgen/", "sourcery/"])
154
+ || signals.has_file_name(&["r.generated.swift"])
155
+ || signals.has_extension(&[".generated.swift", ".pb.swift", ".grpc.swift"])
156
+ }
@@ -1,8 +1,12 @@
1
1
  use crate::models::NaomeError;
2
2
 
3
+ use super::adapter_ios;
3
4
  use super::adapter_support::{
4
- detected_ids, detects_javascript_typescript_project, detects_rust_project, extend_unique,
5
- find_adapter_by_id, validate_ids, AdapterDescriptor, RepoSignals,
5
+ detected_ids, detects_generated_ios_project, detects_ios_app_structure_project,
6
+ detects_ios_resources_project, detects_javascript_typescript_project, detects_rust_project,
7
+ detects_swift_package_project, detects_swift_project, detects_swiftui_project,
8
+ detects_xcode_project, detects_xctest_project, extend_unique, find_adapter_by_id, validate_ids,
9
+ AdapterDescriptor, RepoSignals,
6
10
  };
7
11
  use super::types::{QualityLimitOverrides, QualityPathRule, RepositoryQualityConfig};
8
12
 
@@ -49,21 +53,76 @@ pub(crate) fn validate_adapter_ids(ids: &[String]) -> Result<(), NaomeError> {
49
53
  validate_ids(ids, registry(), CONFIG_PATH)
50
54
  }
51
55
 
56
+ const QUALITY_ADAPTERS: &[QualityAdapter] = &[
57
+ QualityAdapter {
58
+ id: "rust",
59
+ generated_paths: &[],
60
+ detect: detects_rust_project,
61
+ path_rules: rust_path_rules,
62
+ },
63
+ QualityAdapter {
64
+ id: "javascript-typescript",
65
+ generated_paths: &["coverage/**", "**/coverage/**", ".next/**", "**/.next/**"],
66
+ detect: detects_javascript_typescript_project,
67
+ path_rules: javascript_typescript_path_rules,
68
+ },
69
+ QualityAdapter {
70
+ id: "swift",
71
+ generated_paths: &[],
72
+ detect: detects_swift_project,
73
+ path_rules: adapter_ios::swift_path_rules,
74
+ },
75
+ QualityAdapter {
76
+ id: "xcode",
77
+ generated_paths: &[
78
+ "DerivedData/**",
79
+ "**/DerivedData/**",
80
+ "**/xcuserdata/**",
81
+ "**/*.xcuserstate",
82
+ ],
83
+ detect: detects_xcode_project,
84
+ path_rules: empty_path_rules,
85
+ },
86
+ QualityAdapter {
87
+ id: "xctest",
88
+ generated_paths: &[],
89
+ detect: detects_xctest_project,
90
+ path_rules: adapter_ios::xctest_path_rules,
91
+ },
92
+ QualityAdapter {
93
+ id: "swiftui",
94
+ generated_paths: &["**/Preview Content/**"],
95
+ detect: detects_swiftui_project,
96
+ path_rules: adapter_ios::swiftui_path_rules,
97
+ },
98
+ QualityAdapter {
99
+ id: "ios-app-structure",
100
+ generated_paths: &[],
101
+ detect: detects_ios_app_structure_project,
102
+ path_rules: adapter_ios::ios_app_structure_path_rules,
103
+ },
104
+ QualityAdapter {
105
+ id: "swift-package",
106
+ generated_paths: &[],
107
+ detect: detects_swift_package_project,
108
+ path_rules: adapter_ios::swift_package_path_rules,
109
+ },
110
+ QualityAdapter {
111
+ id: "ios-resources",
112
+ generated_paths: &[],
113
+ detect: detects_ios_resources_project,
114
+ path_rules: adapter_ios::ios_resource_path_rules,
115
+ },
116
+ QualityAdapter {
117
+ id: "generated-ios",
118
+ generated_paths: adapter_ios::GENERATED_IOS_PATHS,
119
+ detect: detects_generated_ios_project,
120
+ path_rules: adapter_ios::generated_ios_path_rules,
121
+ },
122
+ ];
123
+
52
124
  fn registry() -> &'static [QualityAdapter] {
53
- &[
54
- QualityAdapter {
55
- id: "rust",
56
- generated_paths: &[],
57
- detect: detects_rust_project,
58
- path_rules: rust_path_rules,
59
- },
60
- QualityAdapter {
61
- id: "javascript-typescript",
62
- generated_paths: &["coverage/**", "**/coverage/**", ".next/**", "**/.next/**"],
63
- detect: detects_javascript_typescript_project,
64
- path_rules: javascript_typescript_path_rules,
65
- },
66
- ]
125
+ QUALITY_ADAPTERS
67
126
  }
68
127
 
69
128
  fn rust_path_rules() -> Vec<QualityPathRule> {
@@ -84,6 +143,10 @@ fn javascript_typescript_path_rules() -> Vec<QualityPathRule> {
84
143
  }]
85
144
  }
86
145
 
146
+ fn empty_path_rules() -> Vec<QualityPathRule> {
147
+ Vec::new()
148
+ }
149
+
87
150
  fn javascript_typescript_test_paths() -> Vec<String> {
88
151
  let mut paths = Vec::new();
89
152
  for marker in ["test", "spec"] {
@@ -100,7 +163,7 @@ fn javascript_typescript_test_paths() -> Vec<String> {
100
163
  paths
101
164
  }
102
165
 
103
- fn test_file_limits(max_top_level_symbols: usize) -> QualityLimitOverrides {
166
+ pub(super) fn test_file_limits(max_top_level_symbols: usize) -> QualityLimitOverrides {
104
167
  QualityLimitOverrides {
105
168
  max_file_lines: Some(650),
106
169
  max_diff_added_lines: Some(220),
@@ -112,7 +175,7 @@ fn test_file_limits(max_top_level_symbols: usize) -> QualityLimitOverrides {
112
175
  }
113
176
  }
114
177
 
115
- fn path_rule(
178
+ pub(super) fn path_rule(
116
179
  id: &str,
117
180
  paths: &[&str],
118
181
  limits: QualityLimitOverrides,
@@ -91,15 +91,13 @@ impl QualityCache {
91
91
  }
92
92
 
93
93
  fn entry_path(&self, path: &str, content_hash: &str) -> PathBuf {
94
- self.root
95
- .join(CACHE_RELATIVE_PATH)
96
- .join(stable_key(&[
97
- env!("CARGO_PKG_VERSION"),
98
- &self.config_hash,
99
- ADAPTER_VERSION,
100
- path,
101
- content_hash,
102
- ]))
94
+ self.root.join(CACHE_RELATIVE_PATH).join(stable_key(&[
95
+ env!("CARGO_PKG_VERSION"),
96
+ &self.config_hash,
97
+ ADAPTER_VERSION,
98
+ path,
99
+ content_hash,
100
+ ]))
103
101
  }
104
102
  }
105
103
 
@@ -18,13 +18,10 @@ impl QualityCheck for DuplicateBlockCheck {
18
18
  return;
19
19
  }
20
20
  let mut occurrences: HashMap<String, Vec<DuplicateOccurrence>> = HashMap::new();
21
- for file in context
22
- .comparison_candidate_files()
23
- .filter(|file| {
24
- is_code_like_path(&file.path)
25
- && context.config.check_enabled_for_path(self.id(), &file.path)
26
- })
27
- {
21
+ for file in context.comparison_candidate_files().filter(|file| {
22
+ is_code_like_path(&file.path)
23
+ && context.config.check_enabled_for_path(self.id(), &file.path)
24
+ }) {
28
25
  let window = context.limits_for(&file.path).duplicate_block_lines;
29
26
  if file.normalized_lines.len() < window {
30
27
  continue;
@@ -15,6 +15,12 @@ pub fn config_relative_path() -> &'static str {
15
15
  }
16
16
 
17
17
  pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
18
+ let config = read_policy_config(root)?;
19
+ validate_config(&config)?;
20
+ apply_enabled_adapters(config)
21
+ }
22
+
23
+ pub(super) fn read_policy_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
18
24
  let path = root.join(CONFIG_RELATIVE_PATH);
19
25
  let config = if path.is_file() {
20
26
  serde_json::from_str(&fs::read_to_string(path)?)?
@@ -22,7 +28,20 @@ pub fn read_config(root: &Path) -> Result<RepositoryQualityConfig, NaomeError> {
22
28
  generated_config(root)?
23
29
  };
24
30
  validate_config(&config)?;
25
- apply_enabled_adapters(config)
31
+ Ok(config)
32
+ }
33
+
34
+ pub(super) fn write_policy_config(
35
+ root: &Path,
36
+ config: &RepositoryQualityConfig,
37
+ ) -> Result<(), NaomeError> {
38
+ let path = root.join(CONFIG_RELATIVE_PATH);
39
+ if let Some(parent) = path.parent() {
40
+ fs::create_dir_all(parent)?;
41
+ }
42
+ let content = serde_json::to_string_pretty(config)?;
43
+ fs::write(path, format!("{content}\n"))?;
44
+ Ok(())
26
45
  }
27
46
 
28
47
  pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError> {
@@ -34,8 +53,7 @@ pub fn write_default_config_if_missing(root: &Path) -> Result<bool, NaomeError>
34
53
  if let Some(parent) = path.parent() {
35
54
  fs::create_dir_all(parent)?;
36
55
  }
37
- let content = serde_json::to_string_pretty(&generated_config(root)?)?;
38
- fs::write(path, format!("{content}\n"))?;
56
+ write_policy_config(root, &generated_config(root)?)?;
39
57
  Ok(true)
40
58
  }
41
59
 
@@ -1,3 +1,4 @@
1
+ mod adapter_ios;
1
2
  mod adapter_support;
2
3
  mod adapters;
3
4
  mod baseline;
@@ -6,6 +7,8 @@ mod checks;
6
7
  mod cleanup;
7
8
  mod config;
8
9
  mod config_support;
10
+ mod reconcile;
11
+ mod reconcile_anchors;
9
12
  mod scanner;
10
13
  mod semantic;
11
14
  mod structure;
@@ -14,17 +17,19 @@ mod types;
14
17
  use std::path::Path;
15
18
 
16
19
  use crate::models::NaomeError;
20
+ use crate::repository_model_drift;
17
21
 
18
- pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
19
22
  pub use cache::{clear_quality_cache, quality_cache_status, QualityCacheStatus};
23
+ pub use cleanup::{cleanup_plan_from_violations, cleanup_route_for_path};
24
+ pub use reconcile::reconcile_repository_quality;
20
25
  pub use semantic::{semantic_route_for_finding, SemanticFinding, SemanticReport};
21
26
  pub use structure::{
22
27
  explain_repository_structure, RepositoryStructureConfig, StructurePathExplanation,
23
28
  };
24
29
  pub use types::{
25
30
  QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask, QualityInitMode,
26
- QualityInitResult, QualityMode, QualityReport, QualitySummary, QualityViolation,
27
- RepositoryQualityConfig,
31
+ QualityInitResult, QualityMode, QualityReconcileReport, QualityReport, QualitySummary,
32
+ QualityViolation, RepositoryQualityConfig,
28
33
  };
29
34
 
30
35
  use self::baseline::{
@@ -33,8 +38,9 @@ use self::baseline::{
33
38
  };
34
39
  use self::checks::run_quality_checks;
35
40
  use self::config::{config_relative_path, read_config, write_default_config_if_missing};
36
- use self::scanner::collect_repo_paths;
37
- use self::scanner::scan_repository;
41
+ use self::reconcile::stale_adapter_policy_violations;
42
+ use self::scanner::{collect_repo_paths, stable_fingerprint};
43
+ use self::scanner::{scan_repository, scan_repository_paths};
38
44
  use self::semantic::run_semantic_checks;
39
45
  use self::structure::{
40
46
  run_repository_structure_checks, structure_config_relative_path,
@@ -47,9 +53,37 @@ pub fn check_repository_quality(
47
53
  ) -> Result<QualityReport, NaomeError> {
48
54
  let config = read_config(root)?;
49
55
  let context = scan_repository(root, mode, config)?;
56
+ quality_report_from_context(root, context)
57
+ }
58
+
59
+ pub fn check_repository_quality_paths(
60
+ root: &Path,
61
+ paths: &[impl AsRef<str>],
62
+ ) -> Result<QualityReport, NaomeError> {
63
+ let config = read_config(root)?;
64
+ let paths = paths
65
+ .iter()
66
+ .map(|path| path.as_ref().to_string())
67
+ .collect::<Vec<_>>();
68
+ let context = scan_repository_paths(root, config, &paths)?;
69
+ quality_report_from_context(root, context)
70
+ }
71
+
72
+ fn quality_report_from_context(
73
+ root: &Path,
74
+ context: self::scanner::QualityContext,
75
+ ) -> Result<QualityReport, NaomeError> {
76
+ let mode = context.mode;
50
77
  let baseline = read_baseline_fingerprints(root)?;
51
78
  let mut violations = run_quality_checks(&context);
52
79
  violations.extend(run_repository_structure_checks(root, &context, &baseline)?);
80
+ violations.extend(stale_adapter_policy_violations(
81
+ root,
82
+ mode,
83
+ &context.changed_paths,
84
+ )?);
85
+ violations.extend(stale_repository_model_violations(root)?);
86
+ violations.extend(semantic_changed_violations(&context));
53
87
  for violation in &mut violations {
54
88
  violation.baseline = baseline.contains(&violation.fingerprint);
55
89
  }
@@ -77,7 +111,7 @@ pub fn check_repository_quality(
77
111
  baseline_violation_count,
78
112
  blocking_violation_count,
79
113
  truncated: context.truncated,
80
- reason_codes,
114
+ reason_codes: with_repository_model_reason_codes(reason_codes, &violations),
81
115
  cache_hits: context.cache_hits,
82
116
  cache_misses: context.cache_misses,
83
117
  },
@@ -85,6 +119,86 @@ pub fn check_repository_quality(
85
119
  })
86
120
  }
87
121
 
122
+ fn semantic_changed_violations(context: &self::scanner::QualityContext) -> Vec<QualityViolation> {
123
+ if !context.mode.is_changed() {
124
+ return Vec::new();
125
+ }
126
+ run_semantic_checks(context)
127
+ .findings
128
+ .iter()
129
+ .filter_map(|finding| semantic_finding_violation(context, finding))
130
+ .collect()
131
+ }
132
+
133
+ fn semantic_finding_violation(
134
+ context: &self::scanner::QualityContext,
135
+ finding: &SemanticFinding,
136
+ ) -> Option<QualityViolation> {
137
+ let primary = finding
138
+ .occurrences
139
+ .iter()
140
+ .find(|occurrence| context.applies_to(&occurrence.path))
141
+ .or_else(|| finding.occurrences.first())?;
142
+ let related_paths = finding
143
+ .occurrences
144
+ .iter()
145
+ .map(|occurrence| occurrence.path.clone())
146
+ .filter(|path| path != &primary.path)
147
+ .collect::<Vec<_>>();
148
+ Some(QualityViolation {
149
+ check_id: format!("semantic-{}", finding.kind),
150
+ severity: "blocking".to_string(),
151
+ path: primary.path.clone(),
152
+ line: Some(primary.start_line),
153
+ message: finding.summary.clone(),
154
+ value: Some(finding.confidence),
155
+ limit: None,
156
+ fingerprint: finding.id.clone(),
157
+ related_paths,
158
+ baseline: false,
159
+ })
160
+ }
161
+
162
+ fn stale_repository_model_violations(root: &Path) -> Result<Vec<QualityViolation>, NaomeError> {
163
+ let drift = repository_model_drift(root)?;
164
+ if !drift.model_present || !drift.stale {
165
+ return Ok(Vec::new());
166
+ }
167
+ let message = drift
168
+ .messages
169
+ .first()
170
+ .cloned()
171
+ .unwrap_or_else(|| "NAOME repository model is stale.".to_string());
172
+ Ok(vec![QualityViolation {
173
+ check_id: "repository-model-stale".to_string(),
174
+ severity: "blocking".to_string(),
175
+ path: drift.model_path,
176
+ line: None,
177
+ message: message.clone(),
178
+ value: None,
179
+ limit: None,
180
+ fingerprint: stable_fingerprint(&["repository-model-stale", &message]),
181
+ related_paths: drift.related_paths,
182
+ baseline: false,
183
+ }])
184
+ }
185
+
186
+ fn with_repository_model_reason_codes(
187
+ mut reason_codes: Vec<String>,
188
+ violations: &[QualityViolation],
189
+ ) -> Vec<String> {
190
+ if violations
191
+ .iter()
192
+ .any(|violation| violation.check_id == "repository-model-stale")
193
+ && !reason_codes
194
+ .iter()
195
+ .any(|code| code == "repository_model_stale")
196
+ {
197
+ reason_codes.push("repository_model_stale".to_string());
198
+ }
199
+ reason_codes
200
+ }
201
+
88
202
  pub fn init_repository_quality(root: &Path) -> Result<QualityInitResult, NaomeError> {
89
203
  init_repository_quality_with_mode(root, QualityInitMode::SeedOnly)
90
204
  }
@@ -112,7 +226,11 @@ pub fn init_repository_quality_with_mode(
112
226
  QualityMode::Report
113
227
  },
114
228
  )?;
115
- (write_baseline(root, &report.violations)?, report.violations.len(), false)
229
+ (
230
+ write_baseline(root, &report.violations)?,
231
+ report.violations.len(),
232
+ false,
233
+ )
116
234
  }
117
235
  };
118
236
 
@@ -154,3 +272,16 @@ pub fn check_semantic_legacy(root: &Path, mode: QualityMode) -> Result<SemanticR
154
272
  let context = scan_repository(root, mode, config)?;
155
273
  Ok(run_semantic_checks(&context))
156
274
  }
275
+
276
+ pub fn check_semantic_legacy_paths(
277
+ root: &Path,
278
+ paths: &[impl AsRef<str>],
279
+ ) -> Result<SemanticReport, NaomeError> {
280
+ let config = read_config(root)?;
281
+ let paths = paths
282
+ .iter()
283
+ .map(|path| path.as_ref().to_string())
284
+ .collect::<Vec<_>>();
285
+ let context = scan_repository_paths(root, config, &paths)?;
286
+ Ok(run_semantic_checks(&context))
287
+ }
@@ -0,0 +1,138 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ use super::adapters::detected_adapter_ids;
6
+ use super::config::{config_relative_path, read_policy_config, write_policy_config};
7
+ use super::reconcile_anchors::changed_paths_introduce_adapter;
8
+ use super::scanner::{collect_repo_paths, stable_fingerprint};
9
+ use super::structure::{
10
+ detected_structure_adapter_ids, read_policy_structure_config, structure_config_relative_path,
11
+ write_policy_structure_config,
12
+ };
13
+ use super::types::{QualityMode, QualityReconcileReport, QualityViolation};
14
+
15
+ pub fn reconcile_repository_quality(
16
+ root: &Path,
17
+ write: bool,
18
+ ) -> Result<QualityReconcileReport, NaomeError> {
19
+ let paths = collect_repo_paths(root)?;
20
+ let mut quality_config = read_policy_config(root)?;
21
+ let mut structure_config = read_policy_structure_config(root, &paths)?;
22
+ let detected_quality_adapters = detected_adapter_ids(&paths);
23
+ let detected_structure_adapters = detected_structure_adapter_ids(&paths);
24
+ let missing_quality_adapters =
25
+ missing_adapters(&detected_quality_adapters, &quality_config.enabled_adapters);
26
+ let missing_structure_adapters = missing_adapters(
27
+ &detected_structure_adapters,
28
+ &structure_config.enabled_adapters,
29
+ );
30
+ let stale = !missing_quality_adapters.is_empty() || !missing_structure_adapters.is_empty();
31
+ let mut updated_paths = Vec::new();
32
+
33
+ if write && stale {
34
+ for adapter in &missing_quality_adapters {
35
+ quality_config.enabled_adapters.push(adapter.clone());
36
+ }
37
+ for adapter in &missing_structure_adapters {
38
+ structure_config.enabled_adapters.push(adapter.clone());
39
+ }
40
+ quality_config.enabled_adapters.sort();
41
+ quality_config.enabled_adapters.dedup();
42
+ structure_config.enabled_adapters.sort();
43
+ structure_config.enabled_adapters.dedup();
44
+
45
+ if !missing_quality_adapters.is_empty() {
46
+ write_policy_config(root, &quality_config)?;
47
+ updated_paths.push(config_relative_path().to_string());
48
+ }
49
+ if !missing_structure_adapters.is_empty() {
50
+ write_policy_structure_config(root, &structure_config)?;
51
+ updated_paths.push(structure_config_relative_path().to_string());
52
+ }
53
+ }
54
+
55
+ let stale_after_write = stale && !write;
56
+ let mut reason_codes = Vec::new();
57
+ if stale {
58
+ reason_codes.push("adapter_policy_stale".to_string());
59
+ }
60
+ if write && !updated_paths.is_empty() {
61
+ reason_codes.push("adapter_policy_updated".to_string());
62
+ }
63
+
64
+ Ok(QualityReconcileReport {
65
+ schema: "naome.repository-quality-reconcile.v1".to_string(),
66
+ ok: !stale_after_write,
67
+ stale: stale_after_write,
68
+ write,
69
+ detected_quality_adapters,
70
+ enabled_quality_adapters: quality_config.enabled_adapters,
71
+ missing_quality_adapters: if write {
72
+ Vec::new()
73
+ } else {
74
+ missing_quality_adapters
75
+ },
76
+ detected_structure_adapters,
77
+ enabled_structure_adapters: structure_config.enabled_adapters,
78
+ missing_structure_adapters: if write {
79
+ Vec::new()
80
+ } else {
81
+ missing_structure_adapters
82
+ },
83
+ updated_paths,
84
+ reason_codes,
85
+ })
86
+ }
87
+
88
+ pub(crate) fn stale_adapter_policy_violations(
89
+ root: &Path,
90
+ mode: QualityMode,
91
+ changed_paths: &[String],
92
+ ) -> Result<Vec<QualityViolation>, NaomeError> {
93
+ let report = reconcile_repository_quality(root, false)?;
94
+ if report.ok {
95
+ return Ok(Vec::new());
96
+ }
97
+
98
+ let mut missing = report.missing_quality_adapters.clone();
99
+ for adapter in &report.missing_structure_adapters {
100
+ if !missing.contains(adapter) {
101
+ missing.push(adapter.clone());
102
+ }
103
+ }
104
+ if mode.is_changed() {
105
+ missing.retain(|adapter| changed_paths_introduce_adapter(adapter, changed_paths));
106
+ }
107
+ if missing.is_empty() {
108
+ return Ok(Vec::new());
109
+ }
110
+ missing.sort();
111
+
112
+ let message = format!(
113
+ "Repository tech-stack signals require adapter policy reconciliation. Missing adapter(s): {}. Run `naome quality reconcile --write`.",
114
+ missing.join(", ")
115
+ );
116
+ let check_id = "adapter-policy-stale";
117
+ let path = config_relative_path();
118
+ Ok(vec![QualityViolation {
119
+ check_id: check_id.to_string(),
120
+ severity: "error".to_string(),
121
+ path: path.to_string(),
122
+ line: None,
123
+ message: message.clone(),
124
+ value: Some(missing.len() as f64),
125
+ limit: Some(0.0),
126
+ fingerprint: stable_fingerprint(&[check_id, path, &missing.join(","), &message]),
127
+ related_paths: vec![structure_config_relative_path().to_string()],
128
+ baseline: false,
129
+ }])
130
+ }
131
+
132
+ fn missing_adapters(detected: &[String], enabled: &[String]) -> Vec<String> {
133
+ detected
134
+ .iter()
135
+ .filter(|adapter| !enabled.contains(adapter))
136
+ .cloned()
137
+ .collect()
138
+ }