@lamentis/naome 1.3.8 → 1.3.10

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 (53) hide show
  1. package/Cargo.lock +2 -2
  2. package/README.md +5 -0
  3. package/bin/naome.js +1 -1
  4. package/crates/naome-cli/Cargo.toml +1 -1
  5. package/crates/naome-cli/src/architecture_commands.rs +123 -0
  6. package/crates/naome-cli/src/cli_args.rs +4 -0
  7. package/crates/naome-cli/src/dispatcher.rs +2 -0
  8. package/crates/naome-cli/src/install_bridge.rs +56 -8
  9. package/crates/naome-cli/src/main.rs +6 -0
  10. package/crates/naome-core/Cargo.toml +1 -1
  11. package/crates/naome-core/src/architecture/config/parser/scalar.rs +26 -0
  12. package/crates/naome-core/src/architecture/config/parser/sections.rs +137 -0
  13. package/crates/naome-core/src/architecture/config/parser.rs +96 -0
  14. package/crates/naome-core/src/architecture/config.rs +114 -0
  15. package/crates/naome-core/src/architecture/model.rs +80 -0
  16. package/crates/naome-core/src/architecture/output.rs +178 -0
  17. package/crates/naome-core/src/architecture/rules.rs +140 -0
  18. package/crates/naome-core/src/architecture/scan/graph_builder/emit.rs +56 -0
  19. package/crates/naome-core/src/architecture/scan/graph_builder/facts.rs +88 -0
  20. package/crates/naome-core/src/architecture/scan/graph_builder.rs +134 -0
  21. package/crates/naome-core/src/architecture/scan/path_scan.rs +92 -0
  22. package/crates/naome-core/src/architecture/scan.rs +75 -0
  23. package/crates/naome-core/src/architecture.rs +31 -0
  24. package/crates/naome-core/src/harness_health/integrity.rs +41 -23
  25. package/crates/naome-core/src/harness_health/manifest.rs +97 -0
  26. package/crates/naome-core/src/harness_health.rs +58 -106
  27. package/crates/naome-core/src/install_plan.rs +2 -0
  28. package/crates/naome-core/src/lib.rs +16 -8
  29. package/crates/naome-core/src/quality/cache.rs +122 -19
  30. package/crates/naome-core/src/quality/scanner/analysis.rs +4 -2
  31. package/crates/naome-core/src/quality/scanner/repo_paths.rs +27 -3
  32. package/crates/naome-core/src/quality/scanner.rs +5 -2
  33. package/crates/naome-core/src/workflow/integrity_support.rs +10 -3
  34. package/crates/naome-core/tests/architecture.rs +209 -0
  35. package/crates/naome-core/tests/harness_health.rs +150 -0
  36. package/crates/naome-core/tests/quality_performance.rs +63 -2
  37. package/installer/filesystem.js +38 -0
  38. package/installer/flows.js +6 -1
  39. package/installer/harness-file-ops.js +36 -8
  40. package/installer/harness-files.js +3 -0
  41. package/installer/manifest-state.js +2 -2
  42. package/installer/native.js +63 -18
  43. package/native/darwin-arm64/naome +0 -0
  44. package/native/linux-x64/naome +0 -0
  45. package/package.json +1 -1
  46. package/templates/naome-root/.naome/bin/check-harness-health.js +23 -19
  47. package/templates/naome-root/.naome/bin/check-task-state.js +33 -40
  48. package/templates/naome-root/.naome/bin/naome.js +2 -2
  49. package/templates/naome-root/.naome/manifest.json +8 -6
  50. package/templates/naome-root/.naome/verification.json +15 -1
  51. package/templates/naome-root/docs/naome/architecture-fitness.md +97 -0
  52. package/templates/naome-root/docs/naome/index.md +4 -3
  53. package/templates/naome-root/docs/naome/testing.md +6 -3
@@ -1,6 +1,7 @@
1
1
  mod integrity;
2
+ mod manifest;
2
3
 
3
- use std::collections::{HashMap, HashSet};
4
+ use std::collections::HashMap;
4
5
  use std::fs;
5
6
  use std::path::{Component, Path};
6
7
 
@@ -11,6 +12,7 @@ use self::integrity::{
11
12
  is_integrity_hash, native_integrity_from_naome_command, sha256_bytes,
12
13
  NAOME_COMMAND_RELATIVE_PATH, NATIVE_BINARY_RELATIVE_PATH,
13
14
  };
15
+ use self::manifest::{string_array, validate_manifest_ownership, validate_manifest_shape};
14
16
  use crate::install_plan::{MACHINE_OWNED_PATHS, PROJECT_OWNED_PATHS};
15
17
  use crate::models::NaomeError;
16
18
 
@@ -56,62 +58,6 @@ pub fn validate_harness_health(
56
58
  Ok(errors)
57
59
  }
58
60
 
59
- fn validate_manifest_shape(manifest: &Value, errors: &mut Vec<String>) {
60
- let Some(object) = manifest.as_object() else {
61
- errors.push(".naome/manifest.json must be a JSON object.".to_string());
62
- return;
63
- };
64
-
65
- if object.get("name").and_then(Value::as_str) != Some("naome") {
66
- errors.push(".naome/manifest.json name must be naome.".to_string());
67
- }
68
-
69
- if !object
70
- .get("harnessVersion")
71
- .and_then(Value::as_str)
72
- .is_some_and(is_version)
73
- {
74
- errors.push(".naome/manifest.json harnessVersion must be semver.".to_string());
75
- }
76
-
77
- if !string_array(object.get("machineOwned")).is_some() {
78
- errors.push(".naome/manifest.json machineOwned must be a string array.".to_string());
79
- }
80
-
81
- if !string_array(object.get("projectOwned")).is_some() {
82
- errors.push(".naome/manifest.json projectOwned must be a string array.".to_string());
83
- }
84
-
85
- if !object.get("integrity").is_some_and(Value::is_object) {
86
- errors.push(".naome/manifest.json integrity must be an object.".to_string());
87
- }
88
- }
89
-
90
- fn validate_manifest_ownership(manifest: &Value, errors: &mut Vec<String>) {
91
- let Some(object) = manifest.as_object() else {
92
- return;
93
- };
94
- let Some(machine_owned) = string_array(object.get("machineOwned")) else {
95
- return;
96
- };
97
- let Some(project_owned) = string_array(object.get("projectOwned")) else {
98
- return;
99
- };
100
-
101
- validate_contains_all(
102
- &machine_owned,
103
- MACHINE_OWNED_PATHS,
104
- ".naome/manifest.json machineOwned",
105
- errors,
106
- );
107
- validate_contains_all(
108
- &project_owned,
109
- PROJECT_OWNED_PATHS,
110
- ".naome/manifest.json projectOwned",
111
- errors,
112
- );
113
- }
114
-
115
61
  fn validate_manifest_integrity(
116
62
  root: &Path,
117
63
  manifest: &Value,
@@ -186,13 +132,41 @@ fn validate_native_decision_binary(
186
132
  return Ok(());
187
133
  };
188
134
 
189
- if !machine_owned
135
+ let wrapper_paths = [
136
+ NAOME_COMMAND_RELATIVE_PATH,
137
+ ".naome/bin/check-harness-health.js",
138
+ ".naome/bin/check-task-state.js",
139
+ ];
140
+ let wrapper_integrity = wrapper_paths
141
+ .iter()
142
+ .map(|relative_path| {
143
+ native_integrity_from_regular_file(root, relative_path)
144
+ .map(|expected| (*relative_path, expected))
145
+ })
146
+ .collect::<Result<Vec<_>, _>>()?;
147
+ let native_is_declared = machine_owned
148
+ .iter()
149
+ .any(|entry| entry == NATIVE_BINARY_RELATIVE_PATH);
150
+ let native_has_integrity = integrity.contains_key(NATIVE_BINARY_RELATIVE_PATH);
151
+ let native_path_present = fs::symlink_metadata(root.join(NATIVE_BINARY_RELATIVE_PATH)).is_ok();
152
+ let wrapper_requires_native = wrapper_integrity
190
153
  .iter()
191
- .any(|entry| entry == NATIVE_BINARY_RELATIVE_PATH)
154
+ .any(|(_, expected)| expected.is_some());
155
+
156
+ if !native_is_declared
157
+ && !native_has_integrity
158
+ && !wrapper_requires_native
159
+ && !native_path_present
192
160
  {
193
161
  return Ok(());
194
162
  }
195
163
 
164
+ if !native_is_declared {
165
+ errors.push(format!(
166
+ ".naome/manifest.json machineOwned must include {NATIVE_BINARY_RELATIVE_PATH}."
167
+ ));
168
+ }
169
+
196
170
  validate_regular_file(root, NATIVE_BINARY_RELATIVE_PATH, errors)?;
197
171
 
198
172
  if !root.join(NATIVE_BINARY_RELATIVE_PATH).exists()
@@ -222,22 +196,36 @@ fn validate_native_decision_binary(
222
196
  ));
223
197
  }
224
198
 
225
- let command_path = root.join(NAOME_COMMAND_RELATIVE_PATH);
226
- if !command_path.exists() || has_symlink_in_path(root, NAOME_COMMAND_RELATIVE_PATH)? {
227
- return Ok(());
228
- }
229
-
230
- let command_content = fs::read_to_string(command_path)?;
231
- let command_expected = native_integrity_from_naome_command(&command_content);
232
- if command_expected.as_deref() != Some(manifest_expected) {
233
- errors.push(format!(
234
- "{NAOME_COMMAND_RELATIVE_PATH} native binary integrity does not match .naome/manifest.json."
235
- ));
199
+ for (relative_path, expected) in wrapper_integrity {
200
+ if expected.as_deref() != Some(manifest_expected) {
201
+ errors.push(format!(
202
+ "{relative_path} native binary integrity does not match .naome/manifest.json."
203
+ ));
204
+ }
236
205
  }
237
206
 
238
207
  Ok(())
239
208
  }
240
209
 
210
+ fn native_integrity_from_regular_file(
211
+ root: &Path,
212
+ relative_path: &str,
213
+ ) -> Result<Option<String>, NaomeError> {
214
+ let file_path = root.join(relative_path);
215
+ if has_symlink_in_path(root, relative_path)? {
216
+ return Ok(None);
217
+ }
218
+ let Ok(metadata) = fs::symlink_metadata(&file_path) else {
219
+ return Ok(None);
220
+ };
221
+ if !metadata.is_file() {
222
+ return Ok(None);
223
+ }
224
+ Ok(native_integrity_from_naome_command(&fs::read_to_string(
225
+ file_path,
226
+ )?))
227
+ }
228
+
241
229
  fn validate_naome_ignore(root: &Path, errors: &mut Vec<String>) -> Result<(), NaomeError> {
242
230
  let relative_path = ".naomeignore";
243
231
  if !root.join(relative_path).exists() || has_symlink_in_path(root, relative_path)? {
@@ -342,20 +330,6 @@ fn read_json(
342
330
  }
343
331
  }
344
332
 
345
- fn validate_contains_all(
346
- actual_values: &[String],
347
- expected_values: &[&str],
348
- field_name: &str,
349
- errors: &mut Vec<String>,
350
- ) {
351
- let actual: HashSet<&str> = actual_values.iter().map(String::as_str).collect();
352
- for expected in expected_values {
353
- if !actual.contains(expected) {
354
- errors.push(format!("{field_name} must include {expected}."));
355
- }
356
- }
357
- }
358
-
359
333
  fn validate_regular_file(
360
334
  root: &Path,
361
335
  relative_path: &str,
@@ -423,25 +397,3 @@ fn has_symlink_in_path(root: &Path, relative_path: &str) -> Result<bool, NaomeEr
423
397
 
424
398
  Ok(false)
425
399
  }
426
-
427
- fn string_array(value: Option<&Value>) -> Option<Vec<String>> {
428
- value.and_then(Value::as_array).and_then(|entries| {
429
- entries
430
- .iter()
431
- .map(|entry| {
432
- entry
433
- .as_str()
434
- .filter(|text| !text.trim().is_empty())
435
- .map(ToString::to_string)
436
- })
437
- .collect()
438
- })
439
- }
440
-
441
- fn is_version(value: &str) -> bool {
442
- let parts: Vec<&str> = value.split('.').collect();
443
- parts.len() == 3
444
- && parts
445
- .iter()
446
- .all(|part| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit()))
447
- }
@@ -12,6 +12,7 @@ pub const MACHINE_OWNED_PATHS: &[&str] = &[
12
12
  "docs/naome/agent-workflow.md",
13
13
  "docs/naome/context-economy.md",
14
14
  "docs/naome/task-ledger.md",
15
+ "docs/naome/architecture-fitness.md",
15
16
  "docs/naome/execution.md",
16
17
  "docs/naome/upgrade.md",
17
18
  ];
@@ -48,6 +49,7 @@ pub const LOCAL_ONLY_MACHINE_OWNED_PATHS: &[&str] = &[
48
49
  "docs/naome/agent-workflow.md",
49
50
  "docs/naome/context-economy.md",
50
51
  "docs/naome/task-ledger.md",
52
+ "docs/naome/architecture-fitness.md",
51
53
  "docs/naome/execution.md",
52
54
  "docs/naome/upgrade.md",
53
55
  ];
@@ -1,3 +1,4 @@
1
+ mod architecture;
1
2
  mod context;
2
3
  mod decision;
3
4
  mod git;
@@ -19,6 +20,14 @@ mod verification_contract;
19
20
  mod verification_contract_policy;
20
21
  mod workflow;
21
22
 
23
+ pub use architecture::{
24
+ default_architecture_config_text, format_architecture_explain, format_architecture_scan,
25
+ format_architecture_validation, scan_architecture, validate_architecture,
26
+ ArchitectureAgentFeedback, ArchitectureConfig, ArchitectureEdge, ArchitectureEdgeKind,
27
+ ArchitectureGraph, ArchitectureMetadata, ArchitectureNode, ArchitectureNodeKind,
28
+ ArchitectureScanOptions, ArchitectureScanReport, ArchitectureValidation, ArchitectureViolation,
29
+ ContextConfig, LayerConfig, RuleConfig, Severity, SourceRange, ViolationSummary,
30
+ };
22
31
  pub use context::{
23
32
  select_context_for_changed_paths, select_context_for_prompt, ContextBudgetLedger,
24
33
  ContextCapsule, ContextItem, ContextSelection,
@@ -36,14 +45,13 @@ pub use journal::{append_task_journal, TaskJournalEntry};
36
45
  pub use models::{Decision, NaomeError};
37
46
  pub use quality::{
38
47
  check_repository_quality, check_repository_quality_paths, check_semantic_legacy,
39
- check_semantic_legacy_paths,
40
- clear_quality_cache, explain_repository_structure, init_repository_quality,
41
- init_repository_quality_with_mode, plan_quality_cleanup, quality_cache_status,
42
- reconcile_repository_quality, route_quality_cleanup, semantic_route_for_finding,
43
- QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute, QualityCleanupTask,
44
- QualityInitMode, QualityInitResult, QualityMode, QualityReconcileReport, QualityReport,
45
- QualitySummary, QualityViolation, RepositoryQualityConfig, RepositoryStructureConfig,
46
- SemanticFinding, SemanticReport, StructurePathExplanation,
48
+ check_semantic_legacy_paths, clear_quality_cache, explain_repository_structure,
49
+ init_repository_quality, init_repository_quality_with_mode, plan_quality_cleanup,
50
+ quality_cache_status, reconcile_repository_quality, route_quality_cleanup,
51
+ semantic_route_for_finding, QualityCacheStatus, QualityCleanupPlan, QualityCleanupRoute,
52
+ QualityCleanupTask, QualityInitMode, QualityInitResult, QualityMode, QualityReconcileReport,
53
+ QualityReport, QualitySummary, QualityViolation, RepositoryQualityConfig,
54
+ RepositoryStructureConfig, SemanticFinding, SemanticReport, StructurePathExplanation,
47
55
  };
48
56
  pub use repository_model::{
49
57
  explain_repository_model_path, refresh_repository_model, repository_model_drift,
@@ -1,5 +1,6 @@
1
- use std::fs;
2
- use std::path::{Path, PathBuf};
1
+ use std::fs::{self, OpenOptions};
2
+ use std::io::Write;
3
+ use std::path::{Component, Path, PathBuf};
3
4
 
4
5
  use serde::{Deserialize, Serialize};
5
6
  use sha2::{Digest, Sha256};
@@ -49,7 +50,11 @@ impl QualityCache {
49
50
  }
50
51
 
51
52
  pub(crate) fn read(&self, path: &str, content_hash: &str) -> Option<FileAnalysis> {
52
- let entry = fs::read_to_string(self.entry_path(path, content_hash)).ok()?;
53
+ let cache_path = self.safe_entry_path(path, content_hash).ok()?;
54
+ if !regular_file_without_symlink(&cache_path) {
55
+ return None;
56
+ }
57
+ let entry = fs::read_to_string(cache_path).ok()?;
53
58
  let entry: CacheEntry = serde_json::from_str(&entry).ok()?;
54
59
  if entry.schema == CACHE_SCHEMA
55
60
  && entry.naome_version == env!("CARGO_PKG_VERSION")
@@ -70,10 +75,7 @@ impl QualityCache {
70
75
  content_hash: &str,
71
76
  analysis: &FileAnalysis,
72
77
  ) -> Result<(), NaomeError> {
73
- let cache_path = self.entry_path(path, content_hash);
74
- if let Some(parent) = cache_path.parent() {
75
- fs::create_dir_all(parent)?;
76
- }
78
+ let cache_path = self.safe_entry_path(path, content_hash)?;
77
79
  let entry = CacheEntry {
78
80
  schema: CACHE_SCHEMA.to_string(),
79
81
  naome_version: env!("CARGO_PKG_VERSION").to_string(),
@@ -84,31 +86,29 @@ impl QualityCache {
84
86
  analysis: analysis.clone(),
85
87
  };
86
88
  let content = format!("{}\n", serde_json::to_string(&entry)?);
87
- if fs::read_to_string(&cache_path).map_or(true, |current| current != content) {
88
- fs::write(cache_path, content)?;
89
- }
90
- Ok(())
89
+ write_cache_entry(&cache_path, &content)
91
90
  }
92
91
 
93
- fn entry_path(&self, path: &str, content_hash: &str) -> PathBuf {
94
- self.root.join(CACHE_RELATIVE_PATH).join(stable_key(&[
92
+ fn safe_entry_path(&self, path: &str, content_hash: &str) -> Result<PathBuf, NaomeError> {
93
+ let cache_root = safe_cache_root(&self.root, true)?;
94
+ Ok(cache_root.join(stable_key(&[
95
95
  env!("CARGO_PKG_VERSION"),
96
96
  &self.config_hash,
97
97
  ADAPTER_VERSION,
98
98
  path,
99
99
  content_hash,
100
- ]))
100
+ ])))
101
101
  }
102
102
  }
103
103
 
104
104
  pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
105
- let path = root.join(CACHE_RELATIVE_PATH);
105
+ let path = safe_cache_root(root, false)?;
106
106
  let mut entry_count = 0;
107
107
  let mut bytes = 0;
108
- if path.is_dir() {
108
+ if regular_directory_without_symlink(&path) {
109
109
  for entry in fs::read_dir(&path)? {
110
110
  let entry = entry?;
111
- let metadata = entry.metadata()?;
111
+ let metadata = fs::symlink_metadata(entry.path())?;
112
112
  if metadata.is_file() {
113
113
  entry_count += 1;
114
114
  bytes += metadata.len();
@@ -124,13 +124,116 @@ pub fn quality_cache_status(root: &Path) -> Result<QualityCacheStatus, NaomeErro
124
124
  }
125
125
 
126
126
  pub fn clear_quality_cache(root: &Path) -> Result<QualityCacheStatus, NaomeError> {
127
- let path = root.join(CACHE_RELATIVE_PATH);
128
- if path.exists() {
127
+ let path = safe_cache_root(root, false)?;
128
+ if regular_directory_without_symlink(&path) {
129
129
  fs::remove_dir_all(&path)?;
130
130
  }
131
131
  quality_cache_status(root)
132
132
  }
133
133
 
134
+ fn write_cache_entry(cache_path: &Path, content: &str) -> Result<(), NaomeError> {
135
+ validate_cache_entry_parent(cache_path)?;
136
+ match fs::symlink_metadata(cache_path) {
137
+ Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
138
+ return Err(NaomeError::new(format!(
139
+ "quality cache entry must be a regular file: {}",
140
+ cache_path.display()
141
+ )));
142
+ }
143
+ Ok(_) => {
144
+ if fs::read_to_string(cache_path).map_or(false, |current| current == content) {
145
+ return Ok(());
146
+ }
147
+ }
148
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
149
+ Err(error) => return Err(error.into()),
150
+ }
151
+
152
+ let temp_path = cache_path.with_extension(format!(
153
+ "tmp.{}.{}",
154
+ std::process::id(),
155
+ stable_key(&[content]).trim_end_matches(".json")
156
+ ));
157
+ validate_cache_entry_parent(&temp_path)?;
158
+ let mut temp = OpenOptions::new()
159
+ .write(true)
160
+ .create_new(true)
161
+ .open(&temp_path)?;
162
+ if let Err(error) = temp.write_all(content.as_bytes()) {
163
+ let _ = fs::remove_file(&temp_path);
164
+ return Err(error.into());
165
+ }
166
+ if let Err(error) = temp.sync_all() {
167
+ let _ = fs::remove_file(&temp_path);
168
+ return Err(error.into());
169
+ }
170
+ drop(temp);
171
+ validate_cache_entry_parent(cache_path)?;
172
+ if let Err(error) = fs::rename(&temp_path, cache_path) {
173
+ let _ = fs::remove_file(&temp_path);
174
+ return Err(error.into());
175
+ }
176
+ Ok(())
177
+ }
178
+
179
+ fn validate_cache_entry_parent(cache_path: &Path) -> Result<(), NaomeError> {
180
+ let Some(parent) = cache_path.parent() else {
181
+ return Err(NaomeError::new("quality cache entry path has no parent"));
182
+ };
183
+ if !regular_directory_without_symlink(parent) {
184
+ return Err(NaomeError::new(format!(
185
+ "quality cache path must be a regular directory without symlinks: {}",
186
+ parent.display()
187
+ )));
188
+ }
189
+ Ok(())
190
+ }
191
+
192
+ fn safe_cache_root(root: &Path, create: bool) -> Result<PathBuf, NaomeError> {
193
+ let mut current = root.to_path_buf();
194
+ for component in Path::new(CACHE_RELATIVE_PATH).components() {
195
+ let Component::Normal(component) = component else {
196
+ return Err(NaomeError::new(
197
+ "quality cache path must be repository-relative",
198
+ ));
199
+ };
200
+ current.push(component);
201
+ match fs::symlink_metadata(&current) {
202
+ Ok(metadata) if metadata.file_type().is_symlink() => {
203
+ return Err(NaomeError::new(format!(
204
+ "quality cache path must not contain symlinks: {}",
205
+ current.display()
206
+ )));
207
+ }
208
+ Ok(metadata) if metadata.is_dir() => {}
209
+ Ok(_) => {
210
+ return Err(NaomeError::new(format!(
211
+ "quality cache path component is not a directory: {}",
212
+ current.display()
213
+ )));
214
+ }
215
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound && create => {
216
+ fs::create_dir(&current)?;
217
+ }
218
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(current),
219
+ Err(error) => return Err(error.into()),
220
+ }
221
+ }
222
+ Ok(current)
223
+ }
224
+
225
+ fn regular_directory_without_symlink(path: &Path) -> bool {
226
+ fs::symlink_metadata(path)
227
+ .map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
228
+ .unwrap_or(false)
229
+ }
230
+
231
+ fn regular_file_without_symlink(path: &Path) -> bool {
232
+ fs::symlink_metadata(path)
233
+ .map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
234
+ .unwrap_or(false)
235
+ }
236
+
134
237
  pub(crate) fn content_hash(content: &str) -> String {
135
238
  stable_key(&[content])
136
239
  }
@@ -8,6 +8,8 @@ use super::{FileAnalysis, NormalizedLine, SymbolAnalysis};
8
8
  use crate::quality::cache::{content_hash, QualityCache};
9
9
  use normalize::{normalize_line, token_set};
10
10
 
11
+ use super::repo_paths::regular_repo_file_path;
12
+
11
13
  pub(super) fn analyze_repo_file(
12
14
  root: &Path,
13
15
  path: &str,
@@ -16,8 +18,8 @@ pub(super) fn analyze_repo_file(
16
18
  cache: &QualityCache,
17
19
  allow_cache: bool,
18
20
  ) -> Option<(FileAnalysis, bool)> {
19
- let full_path = root.join(path);
20
- if !full_path.is_file() || is_binary_extension(path) {
21
+ let full_path = regular_repo_file_path(root, path)?;
22
+ if is_binary_extension(path) {
21
23
  return None;
22
24
  }
23
25
  if allow_cache {
@@ -1,6 +1,6 @@
1
1
  use std::collections::{HashMap, HashSet};
2
2
  use std::fs;
3
- use std::path::Path;
3
+ use std::path::{Component, Path};
4
4
  use std::process::Command;
5
5
 
6
6
  use crate::models::NaomeError;
@@ -44,8 +44,10 @@ pub(super) fn added_lines_by_path(
44
44
  if !target_paths.contains(&path) {
45
45
  continue;
46
46
  }
47
- if let Ok(content) = fs::read_to_string(root.join(&path)) {
48
- added.insert(path, content.lines().count());
47
+ if let Some(file_path) = regular_repo_file_path(root, &path) {
48
+ if let Ok(content) = fs::read_to_string(file_path) {
49
+ added.insert(path, content.lines().count());
50
+ }
49
51
  }
50
52
  }
51
53
  Ok(added)
@@ -92,3 +94,25 @@ fn untracked_paths(root: &Path) -> Result<Vec<String>, NaomeError> {
92
94
  .map(|entry| String::from_utf8_lossy(entry).replace('\\', "/"))
93
95
  .collect())
94
96
  }
97
+
98
+ pub(super) fn regular_repo_file_path(root: &Path, path: &str) -> Option<std::path::PathBuf> {
99
+ let relative = Path::new(path);
100
+ if relative.is_absolute() {
101
+ return None;
102
+ }
103
+ let mut current = root.to_path_buf();
104
+ for component in relative.components() {
105
+ let Component::Normal(component) = component else {
106
+ return None;
107
+ };
108
+ current.push(component);
109
+ let metadata = fs::symlink_metadata(&current).ok()?;
110
+ if metadata.file_type().is_symlink() {
111
+ return None;
112
+ }
113
+ }
114
+ fs::symlink_metadata(&current)
115
+ .ok()
116
+ .filter(|metadata| metadata.is_file())
117
+ .map(|_| current)
118
+ }
@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
9
9
 
10
10
  use crate::{git, models::NaomeError, paths};
11
11
  pub(crate) use repo_paths::collect_repo_paths;
12
- use repo_paths::{added_lines_by_path, tracked_blob_hashes};
12
+ use repo_paths::{added_lines_by_path, regular_repo_file_path, tracked_blob_hashes};
13
13
 
14
14
  use super::cache::QualityCache;
15
15
  use super::types::{
@@ -317,7 +317,10 @@ fn max_file_bytes(mode: QualityMode) -> u64 {
317
317
  }
318
318
 
319
319
  fn file_exceeds_budget(root: &Path, path: &str, mode: QualityMode) -> bool {
320
- fs::metadata(root.join(path)).map_or(false, |metadata| {
320
+ let Some(full_path) = regular_repo_file_path(root, path) else {
321
+ return false;
322
+ };
323
+ fs::symlink_metadata(full_path).map_or(false, |metadata| {
321
324
  metadata.is_file() && metadata.len() > max_file_bytes(mode)
322
325
  })
323
326
  }
@@ -23,8 +23,14 @@ pub(super) fn refresh_support_files(
23
23
  changed.push(path.to_string());
24
24
  }
25
25
  }
26
- if replace_native_integrity(root, integrity)? {
27
- changed.push(NAOME_COMMAND_PATH.to_string());
26
+ for path in [
27
+ NAOME_COMMAND_PATH,
28
+ HEALTH_CHECKER_PATH,
29
+ TASK_STATE_CHECKER_PATH,
30
+ ] {
31
+ if replace_native_integrity(root, path, integrity)? {
32
+ changed.push(path.to_string());
33
+ }
28
34
  }
29
35
  Ok(changed)
30
36
  }
@@ -76,12 +82,13 @@ fn render_expected_integrity_block(integrity: &BTreeMap<String, String>) -> Stri
76
82
 
77
83
  fn replace_native_integrity(
78
84
  root: &Path,
85
+ relative_path: &str,
79
86
  integrity: &BTreeMap<String, String>,
80
87
  ) -> Result<bool, NaomeError> {
81
88
  let Some(native_hash) = integrity.get(NATIVE_BINARY_PATH) else {
82
89
  return Ok(false);
83
90
  };
84
- let path = root.join(NAOME_COMMAND_PATH);
91
+ let path = root.join(relative_path);
85
92
  if !path.is_file() {
86
93
  return Ok(false);
87
94
  }