@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
@@ -2,13 +2,37 @@ use crate::paths;
2
2
 
3
3
  use crate::quality::structure::model::RepositoryStructureConfig;
4
4
 
5
+ const ROLE_SEGMENTS: &[&str] = &[
6
+ "src",
7
+ "source",
8
+ "sources",
9
+ "test",
10
+ "tests",
11
+ "docs",
12
+ "doc",
13
+ "scripts",
14
+ "generated",
15
+ "resources",
16
+ "views",
17
+ ];
18
+
5
19
  pub fn role_for(path: &str, segments: &[String], config: &RepositoryStructureConfig) -> String {
6
20
  let lower = path.to_ascii_lowercase();
7
21
  if has_segment(segments, &["node_modules", "vendor", "third_party"]) {
8
22
  return "dependency/vendor".to_string();
9
23
  }
10
24
  if paths::matches_any(path, &config.generated_roots)
11
- || has_segment(segments, &["generated", "__generated__", "codegen"])
25
+ || has_segment(
26
+ segments,
27
+ &[
28
+ "generated",
29
+ "__generated__",
30
+ "codegen",
31
+ "swiftgen",
32
+ "sourcery",
33
+ ],
34
+ )
35
+ || is_generated_ios_path(&lower)
12
36
  {
13
37
  return "generated".to_string();
14
38
  }
@@ -131,7 +155,13 @@ fn is_test_path(lower: &str, segments: &[String]) -> bool {
131
155
  lower.contains(".test.")
132
156
  || lower.contains(".spec.")
133
157
  || lower.ends_with("_test.rs")
158
+ || lower.ends_with("tests.swift")
159
+ || lower.ends_with("uitests.swift")
134
160
  || has_segment(segments, &["test", "tests", "__tests__"])
161
+ || segments.iter().any(|segment| {
162
+ let lower = segment.to_ascii_lowercase();
163
+ lower.ends_with("tests") || lower.ends_with("uitests")
164
+ })
135
165
  }
136
166
 
137
167
  fn is_docs_path(lower: &str, segments: &[String]) -> bool {
@@ -150,8 +180,16 @@ fn is_config_path(
150
180
  paths::matches_any(path, &config.allowed_root_files)
151
181
  || segments.len() == 1 && (lower.starts_with('.') || lower.ends_with(".toml"))
152
182
  || lower.ends_with(".json")
183
+ || lower.ends_with(".plist")
184
+ || lower.ends_with(".entitlements")
185
+ || lower.ends_with(".xcconfig")
186
+ || lower.ends_with(".strings")
187
+ || lower.ends_with(".stringsdict")
153
188
  || lower.ends_with(".yaml")
154
189
  || lower.ends_with(".yml")
190
+ || lower.ends_with("package.swift")
191
+ || lower.ends_with("podfile")
192
+ || lower.ends_with("cartfile")
155
193
  }
156
194
 
157
195
  fn is_script_path(lower: &str, segments: &[String]) -> bool {
@@ -163,6 +201,16 @@ fn is_artifact_path(lower: &str) -> bool {
163
201
  || lower.ends_with(".log")
164
202
  || lower.ends_with(".map")
165
203
  || lower.ends_with(".min.js")
204
+ || lower.contains("/deriveddata/")
205
+ || lower.ends_with(".xcarchive")
206
+ || lower.ends_with(".dsym")
207
+ }
208
+
209
+ fn is_generated_ios_path(lower: &str) -> bool {
210
+ lower.ends_with(".generated.swift")
211
+ || lower.ends_with(".pb.swift")
212
+ || lower.ends_with(".grpc.swift")
213
+ || lower.ends_with("/r.generated.swift")
166
214
  }
167
215
 
168
216
  fn has_segment(segments: &[String], expected: &[&str]) -> bool {
@@ -173,10 +221,8 @@ fn has_segment(segments: &[String], expected: &[&str]) -> bool {
173
221
  }
174
222
 
175
223
  fn is_role_segment(segment: &str) -> bool {
176
- matches!(
177
- segment,
178
- "src" | "test" | "tests" | "docs" | "doc" | "scripts" | "generated"
179
- )
224
+ let lower = segment.to_ascii_lowercase();
225
+ ROLE_SEGMENTS.contains(&lower.as_str())
180
226
  }
181
227
 
182
228
  fn stem(file_name: &str) -> Option<String> {
@@ -19,6 +19,15 @@ pub fn structure_config_relative_path() -> &'static str {
19
19
  pub fn read_structure_config(
20
20
  root: &Path,
21
21
  paths: &[String],
22
+ ) -> Result<RepositoryStructureConfig, NaomeError> {
23
+ let config = read_policy_structure_config(root, paths)?;
24
+ validate_structure_config(&config)?;
25
+ apply_structure_adapters(config)
26
+ }
27
+
28
+ pub(crate) fn read_policy_structure_config(
29
+ root: &Path,
30
+ paths: &[String],
22
31
  ) -> Result<RepositoryStructureConfig, NaomeError> {
23
32
  let path = root.join(CONFIG_RELATIVE_PATH);
24
33
  let config = if path.is_file() {
@@ -27,7 +36,20 @@ pub fn read_structure_config(
27
36
  generated_structure_config(paths)
28
37
  };
29
38
  validate_structure_config(&config)?;
30
- apply_structure_adapters(config)
39
+ Ok(config)
40
+ }
41
+
42
+ pub(crate) fn write_policy_structure_config(
43
+ root: &Path,
44
+ config: &RepositoryStructureConfig,
45
+ ) -> Result<(), NaomeError> {
46
+ let path = root.join(CONFIG_RELATIVE_PATH);
47
+ if let Some(parent) = path.parent() {
48
+ fs::create_dir_all(parent)?;
49
+ }
50
+ let content = serde_json::to_string_pretty(config)?;
51
+ fs::write(path, format!("{content}\n"))?;
52
+ Ok(())
31
53
  }
32
54
 
33
55
  pub fn write_default_structure_config_if_missing(
@@ -41,8 +63,7 @@ pub fn write_default_structure_config_if_missing(
41
63
  if let Some(parent) = path.parent() {
42
64
  fs::create_dir_all(parent)?;
43
65
  }
44
- let content = serde_json::to_string_pretty(&generated_structure_config(paths))?;
45
- fs::write(path, format!("{content}\n"))?;
66
+ write_policy_structure_config(root, &generated_structure_config(paths))?;
46
67
  Ok(true)
47
68
  }
48
69
 
@@ -1,3 +1,4 @@
1
+ mod adapter_ios;
1
2
  mod adapters;
2
3
  mod checks;
3
4
  mod classify;
@@ -12,9 +13,11 @@ use crate::models::NaomeError;
12
13
 
13
14
  use super::scanner::QualityContext;
14
15
  use super::types::QualityViolation;
16
+ pub(crate) use adapters::detected_structure_adapter_ids;
15
17
  use checks::run_structure_checks;
16
18
  use classify::build_structure_model;
17
19
  use config::read_structure_config;
20
+ pub(crate) use config::{read_policy_structure_config, write_policy_structure_config};
18
21
  pub use config::{structure_config_relative_path, write_default_structure_config_if_missing};
19
22
  pub use model::{RepositoryStructureConfig, StructurePathExplanation};
20
23
 
@@ -5,6 +5,7 @@ use crate::paths;
5
5
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
6
  pub enum QualityMode {
7
7
  ChangedFast,
8
+ PathScoped,
8
9
  Report,
9
10
  DeepReport,
10
11
  }
@@ -16,13 +17,14 @@ impl QualityMode {
16
17
  pub fn as_str(self) -> &'static str {
17
18
  match self {
18
19
  Self::ChangedFast => "changed",
20
+ Self::PathScoped => "path",
19
21
  Self::Report => "report",
20
22
  Self::DeepReport => "deep-report",
21
23
  }
22
24
  }
23
25
 
24
26
  pub fn is_changed(self) -> bool {
25
- self == Self::ChangedFast
27
+ matches!(self, Self::ChangedFast | Self::PathScoped)
26
28
  }
27
29
 
28
30
  pub fn is_deep(self) -> bool {
@@ -257,6 +259,23 @@ pub struct QualityInitResult {
257
259
  pub baseline_path: String,
258
260
  }
259
261
 
262
+ #[derive(Debug, Clone, Serialize)]
263
+ #[serde(rename_all = "camelCase")]
264
+ pub struct QualityReconcileReport {
265
+ pub schema: String,
266
+ pub ok: bool,
267
+ pub stale: bool,
268
+ pub write: bool,
269
+ pub detected_quality_adapters: Vec<String>,
270
+ pub enabled_quality_adapters: Vec<String>,
271
+ pub missing_quality_adapters: Vec<String>,
272
+ pub detected_structure_adapters: Vec<String>,
273
+ pub enabled_structure_adapters: Vec<String>,
274
+ pub missing_structure_adapters: Vec<String>,
275
+ pub updated_paths: Vec<String>,
276
+ pub reason_codes: Vec<String>,
277
+ }
278
+
260
279
  #[derive(Debug, Clone, Serialize)]
261
280
  #[serde(rename_all = "camelCase")]
262
281
  pub struct QualityCleanupPlan {
@@ -0,0 +1,188 @@
1
+ use std::path::Path;
2
+
3
+ use crate::models::NaomeError;
4
+
5
+ use super::path_scan::{collect_repo_paths, evidence, has_any_suffix, has_path, has_suffix};
6
+ use super::types::{RepositoryFact, RepositoryModel, RepositoryVerificationCheck};
7
+ use super::world::build_world_sections;
8
+
9
+ pub(super) fn detect_repository_model(root: &Path) -> Result<RepositoryModel, NaomeError> {
10
+ let paths = collect_repo_paths(root)?;
11
+ let mut facts = Vec::new();
12
+ detect_stack_facts(&paths, &mut facts);
13
+ detect_root_facts(&paths, &mut facts);
14
+ let verification_checks = detect_verification_facts(root, &mut facts)?;
15
+ facts.sort_by(|left, right| left.id.cmp(&right.id));
16
+ facts.dedup_by(|left, right| left.id == right.id);
17
+ let world = build_world_sections(&paths, &facts);
18
+ Ok(RepositoryModel {
19
+ schema: "naome.repository-model.v2".to_string(),
20
+ version: 2,
21
+ facts,
22
+ languages: world.languages,
23
+ package_managers: world.package_managers,
24
+ build_systems: world.build_systems,
25
+ adapters: world.adapters,
26
+ roots: world.roots,
27
+ entities: world.entities,
28
+ path_facts: world.path_facts,
29
+ verification_checks,
30
+ ..RepositoryModel::default()
31
+ })
32
+ }
33
+
34
+ fn detect_stack_facts(paths: &[String], facts: &mut Vec<RepositoryFact>) {
35
+ if has_path(paths, "Cargo.toml") || has_suffix(paths, ".rs") {
36
+ push_fact(
37
+ facts,
38
+ "language",
39
+ "rust",
40
+ evidence(paths, &["Cargo.toml", ".rs"]),
41
+ );
42
+ push_fact(
43
+ facts,
44
+ "packageManager",
45
+ "cargo",
46
+ evidence(paths, &["Cargo.toml"]),
47
+ );
48
+ push_fact(
49
+ facts,
50
+ "buildSystem",
51
+ "cargo",
52
+ evidence(paths, &["Cargo.toml"]),
53
+ );
54
+ }
55
+ if has_path(paths, "package.json") || has_any_suffix(paths, &[".js", ".ts", ".tsx", ".jsx"]) {
56
+ push_fact(
57
+ facts,
58
+ "language",
59
+ "javascript-typescript",
60
+ evidence(paths, &["package.json", ".js", ".ts", ".tsx", ".jsx"]),
61
+ );
62
+ push_fact(
63
+ facts,
64
+ "packageManager",
65
+ "npm",
66
+ evidence(paths, &["package.json"]),
67
+ );
68
+ }
69
+ if has_path(paths, "Package.swift") || has_suffix(paths, ".swift") {
70
+ push_fact(
71
+ facts,
72
+ "language",
73
+ "swift",
74
+ evidence(paths, &["Package.swift", ".swift"]),
75
+ );
76
+ push_fact(
77
+ facts,
78
+ "packageManager",
79
+ "swift-package-manager",
80
+ evidence(paths, &["Package.swift"]),
81
+ );
82
+ }
83
+ if paths
84
+ .iter()
85
+ .any(|path| path.contains(".xcodeproj/") || path.contains(".xcworkspace/"))
86
+ {
87
+ push_fact(
88
+ facts,
89
+ "buildSystem",
90
+ "xcode",
91
+ evidence(paths, &[".xcodeproj/", ".xcworkspace/"]),
92
+ );
93
+ push_fact(
94
+ facts,
95
+ "appType",
96
+ "apple-platform",
97
+ evidence(paths, &[".xcodeproj/", ".xcworkspace/"]),
98
+ );
99
+ }
100
+ }
101
+
102
+ fn detect_root_facts(paths: &[String], facts: &mut Vec<RepositoryFact>) {
103
+ const SOURCE_ROOTS: &[&str] = &["src", "Sources", "app", "packages"];
104
+ const TEST_ROOTS: &[&str] = &["test", "tests", "Tests", "__tests__"];
105
+ const DOCS_ROOTS: &[&str] = &["docs"];
106
+ const GENERATED_ROOTS: &[&str] = &["generated", "Generated"];
107
+ const ARTIFACT_ROOTS: &[&str] = &["dist", "build", "DerivedData"];
108
+
109
+ for (category, roots) in [
110
+ ("sourceRoot", SOURCE_ROOTS),
111
+ ("testRoot", TEST_ROOTS),
112
+ ("docsRoot", DOCS_ROOTS),
113
+ ("generatedRoot", GENERATED_ROOTS),
114
+ ("artifactRoot", ARTIFACT_ROOTS),
115
+ ] {
116
+ for root in roots {
117
+ if paths
118
+ .iter()
119
+ .any(|path| path == *root || path.starts_with(&format!("{root}/")))
120
+ {
121
+ push_fact(facts, category, root, vec![root.to_string()]);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ fn detect_verification_facts(
128
+ root: &Path,
129
+ facts: &mut Vec<RepositoryFact>,
130
+ ) -> Result<Vec<RepositoryVerificationCheck>, NaomeError> {
131
+ let path = root.join(".naome/verification.json");
132
+ if !path.is_file() {
133
+ return Ok(Vec::new());
134
+ }
135
+ let value: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(path)?)?;
136
+ let Some(checks) = value.get("checks").and_then(serde_json::Value::as_array) else {
137
+ return Ok(Vec::new());
138
+ };
139
+ let mut verification_checks = Vec::new();
140
+ for check in checks {
141
+ if let Some(id) = check.get("id").and_then(serde_json::Value::as_str) {
142
+ push_fact(
143
+ facts,
144
+ "verificationCheck",
145
+ id,
146
+ vec![".naome/verification.json".to_string()],
147
+ );
148
+ verification_checks.push(RepositoryVerificationCheck {
149
+ id: id.to_string(),
150
+ command: check
151
+ .get("command")
152
+ .and_then(serde_json::Value::as_str)
153
+ .unwrap_or("")
154
+ .to_string(),
155
+ cwd: check
156
+ .get("cwd")
157
+ .and_then(serde_json::Value::as_str)
158
+ .unwrap_or(".")
159
+ .to_string(),
160
+ evidence: check
161
+ .get("evidence")
162
+ .and_then(serde_json::Value::as_array)
163
+ .into_iter()
164
+ .flatten()
165
+ .filter_map(serde_json::Value::as_str)
166
+ .map(str::to_string)
167
+ .collect(),
168
+ });
169
+ }
170
+ }
171
+ verification_checks.sort_by(|left, right| left.id.cmp(&right.id));
172
+ verification_checks.dedup_by(|left, right| left.id == right.id);
173
+ Ok(verification_checks)
174
+ }
175
+
176
+ fn push_fact(facts: &mut Vec<RepositoryFact>, category: &str, value: &str, evidence: Vec<String>) {
177
+ if evidence.is_empty() {
178
+ return;
179
+ }
180
+ facts.push(RepositoryFact {
181
+ id: format!("{category}:{value}"),
182
+ category: category.to_string(),
183
+ value: value.to_string(),
184
+ confidence: "high".to_string(),
185
+ source: "deterministic-scan".to_string(),
186
+ evidence,
187
+ });
188
+ }
@@ -0,0 +1,121 @@
1
+ use super::types::{RepositoryFact, RepositoryModel, RepositoryPathExplanation};
2
+
3
+ pub(super) fn explain_path(model: &RepositoryModel, path: &str) -> RepositoryPathExplanation {
4
+ let normalized = path.replace('\\', "/");
5
+ let mut facts = facts_for_path(model, &normalized);
6
+ facts.sort_by(|left, right| left.id.cmp(&right.id));
7
+ let mut evidence = facts
8
+ .iter()
9
+ .flat_map(|fact| fact.evidence.clone())
10
+ .collect::<std::collections::BTreeSet<_>>()
11
+ .into_iter()
12
+ .collect::<Vec<_>>();
13
+ if !evidence.contains(&normalized) {
14
+ evidence.push(normalized.clone());
15
+ }
16
+ evidence.sort();
17
+
18
+ RepositoryPathExplanation {
19
+ schema: "naome.repository-model-explanation.v1".to_string(),
20
+ role: path_fact_for_path(model, &normalized)
21
+ .map(|fact| fact.role.clone())
22
+ .or_else(|| role_for_path(&normalized)),
23
+ language: path_fact_for_path(model, &normalized)
24
+ .and_then(|fact| fact.language.clone())
25
+ .or_else(|| language_for_path(&normalized)),
26
+ module: path_fact_for_path(model, &normalized)
27
+ .and_then(|fact| fact.module.clone())
28
+ .or_else(|| module_for_path(model, &normalized)),
29
+ entity: path_fact_for_path(model, &normalized)
30
+ .and_then(|fact| fact.entity.clone())
31
+ .or_else(|| entity_for_path(model, &normalized)),
32
+ path: normalized,
33
+ facts,
34
+ evidence,
35
+ }
36
+ }
37
+
38
+ fn path_fact_for_path<'a>(
39
+ model: &'a RepositoryModel,
40
+ path: &str,
41
+ ) -> Option<&'a super::types::RepositoryPathFact> {
42
+ model.path_facts.iter().find(|fact| fact.path == path)
43
+ }
44
+
45
+ fn facts_for_path(model: &RepositoryModel, path: &str) -> Vec<RepositoryFact> {
46
+ model
47
+ .facts
48
+ .iter()
49
+ .filter(|fact| {
50
+ fact.evidence
51
+ .iter()
52
+ .any(|evidence| path_matches_fact(path, evidence))
53
+ })
54
+ .cloned()
55
+ .collect()
56
+ }
57
+
58
+ fn path_matches_fact(path: &str, evidence: &str) -> bool {
59
+ path == evidence || path.starts_with(&format!("{evidence}/")) || evidence.starts_with(path)
60
+ }
61
+
62
+ fn role_for_path(path: &str) -> Option<String> {
63
+ let lower = path.to_ascii_lowercase();
64
+ if lower.contains("/tests/") || lower.starts_with("tests/") || lower.ends_with("test.ts") {
65
+ Some("test".to_string())
66
+ } else if lower.starts_with("docs/") || lower.ends_with(".md") {
67
+ Some("docs".to_string())
68
+ } else if lower.contains("/generated/") || lower.starts_with("generated/") {
69
+ Some("generated".to_string())
70
+ } else if lower.starts_with("src/") || lower.starts_with("sources/") || source_suffix(&lower) {
71
+ Some("source".to_string())
72
+ } else {
73
+ None
74
+ }
75
+ }
76
+
77
+ fn language_for_path(path: &str) -> Option<String> {
78
+ match path.rsplit('.').next() {
79
+ Some("rs") => Some("rust".to_string()),
80
+ Some("js" | "jsx" | "ts" | "tsx") => Some("javascript-typescript".to_string()),
81
+ Some("swift") => Some("swift".to_string()),
82
+ _ => None,
83
+ }
84
+ }
85
+
86
+ fn source_suffix(path: &str) -> bool {
87
+ path.ends_with(".rs")
88
+ || path.ends_with(".js")
89
+ || path.ends_with(".jsx")
90
+ || path.ends_with(".ts")
91
+ || path.ends_with(".tsx")
92
+ || path.ends_with(".swift")
93
+ }
94
+
95
+ fn module_for_path(model: &RepositoryModel, path: &str) -> Option<String> {
96
+ parent_before(path, "/src/")
97
+ .or_else(|| parent_before(path, "/Sources/"))
98
+ .or_else(|| {
99
+ model
100
+ .roots
101
+ .iter()
102
+ .filter(|root| path.starts_with(&format!("{}/", root.path)))
103
+ .max_by_key(|root| root.path.len())
104
+ .map(|root| root.path.clone())
105
+ })
106
+ }
107
+
108
+ fn entity_for_path(model: &RepositoryModel, path: &str) -> Option<String> {
109
+ model
110
+ .entities
111
+ .iter()
112
+ .filter(|entity| entity.path == "." || path.starts_with(&format!("{}/", entity.path)))
113
+ .max_by_key(|entity| entity.path.len())
114
+ .map(|entity| entity.id.clone())
115
+ }
116
+
117
+ fn parent_before(path: &str, marker: &str) -> Option<String> {
118
+ path.split_once(marker)
119
+ .map(|(parent, _)| parent.to_string())
120
+ .filter(|parent| !parent.is_empty())
121
+ }
@@ -0,0 +1,67 @@
1
+ use std::collections::BTreeSet;
2
+ use std::path::Path;
3
+ use std::process::Command;
4
+
5
+ use crate::models::NaomeError;
6
+
7
+ pub(super) fn collect_repo_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
8
+ let output = Command::new("git")
9
+ .args([
10
+ "ls-files",
11
+ "-z",
12
+ "--cached",
13
+ "--others",
14
+ "--exclude-standard",
15
+ ])
16
+ .current_dir(root)
17
+ .output()?;
18
+ if !output.status.success() {
19
+ return Ok(Vec::new());
20
+ }
21
+ let mut paths = output
22
+ .stdout
23
+ .split(|byte| *byte == 0)
24
+ .filter(|entry| !entry.is_empty())
25
+ .map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
26
+ .collect::<Vec<_>>();
27
+ paths.sort();
28
+ paths.dedup();
29
+ Ok(paths)
30
+ }
31
+
32
+ pub(super) fn evidence(paths: &[String], markers: &[&str]) -> Vec<String> {
33
+ let mut evidence = BTreeSet::new();
34
+ for marker in markers {
35
+ for path in paths {
36
+ if matches_marker(path, marker) {
37
+ evidence.insert(path.clone());
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ evidence.into_iter().collect()
43
+ }
44
+
45
+ pub(super) fn has_path(paths: &[String], expected: &str) -> bool {
46
+ paths
47
+ .iter()
48
+ .any(|path| path == expected || path.ends_with(&format!("/{expected}")))
49
+ }
50
+
51
+ pub(super) fn has_suffix(paths: &[String], suffix: &str) -> bool {
52
+ paths.iter().any(|path| path.ends_with(suffix))
53
+ }
54
+
55
+ pub(super) fn has_any_suffix(paths: &[String], suffixes: &[&str]) -> bool {
56
+ suffixes.iter().any(|suffix| has_suffix(paths, suffix))
57
+ }
58
+
59
+ fn matches_marker(path: &str, marker: &str) -> bool {
60
+ if marker.starts_with('.') {
61
+ return path.ends_with(marker);
62
+ }
63
+ if marker.ends_with('/') {
64
+ return path.contains(marker);
65
+ }
66
+ path == marker || path.ends_with(&format!("/{marker}"))
67
+ }
@@ -0,0 +1,59 @@
1
+ use super::types::{RepositoryEntity, RepositoryRoot};
2
+
3
+ pub(super) fn language_for_path(path: &str) -> Option<String> {
4
+ match path.rsplit('.').next() {
5
+ Some("rs") => Some("rust".to_string()),
6
+ Some("js" | "jsx" | "ts" | "tsx") => Some("javascript-typescript".to_string()),
7
+ Some("swift") => Some("swift".to_string()),
8
+ _ => None,
9
+ }
10
+ }
11
+
12
+ pub(super) fn parent_path(path: &str) -> String {
13
+ path.rsplit_once('/')
14
+ .map(|(parent, _)| parent.to_string())
15
+ .unwrap_or_default()
16
+ }
17
+
18
+ pub(super) fn module_for_path(path: &str, roots: &[RepositoryRoot]) -> Option<String> {
19
+ parent_path(path)
20
+ .split_once("/src")
21
+ .map(|(module, _)| module.to_string())
22
+ .or_else(|| {
23
+ parent_path(path)
24
+ .split_once("/Sources")
25
+ .map(|(module, _)| module.to_string())
26
+ })
27
+ .or_else(|| {
28
+ roots
29
+ .iter()
30
+ .filter(|root| path.starts_with(&format!("{}/", root.path)))
31
+ .max_by_key(|root| root.path.len())
32
+ .map(|root| root.path.clone())
33
+ })
34
+ }
35
+
36
+ pub(super) fn entity_for_path(path: &str, entities: &[RepositoryEntity]) -> Option<String> {
37
+ entities
38
+ .iter()
39
+ .filter(|entity| entity.path == "." || path.starts_with(&format!("{}/", entity.path)))
40
+ .max_by_key(|entity| entity.path.len())
41
+ .map(|entity| entity.id.clone())
42
+ }
43
+
44
+ pub(super) fn root_at_segment(path: &str, names: &[&str]) -> Option<String> {
45
+ let segments = path.split('/').collect::<Vec<_>>();
46
+ let index = segments
47
+ .iter()
48
+ .position(|segment| names.iter().any(|name| segment == name))?;
49
+ if index + 1 >= segments.len() {
50
+ return None;
51
+ }
52
+ Some(segments[..=index].join("/"))
53
+ }
54
+
55
+ pub(super) fn is_package_manifest(path: &str) -> bool {
56
+ path.ends_with("Cargo.toml")
57
+ || path.ends_with("package.json")
58
+ || path.ends_with("Package.swift")
59
+ }