1pub mod graph;
4pub mod health;
5pub mod index_work;
6pub mod language;
7pub mod optional_parser_pack;
8pub mod optional_parser_protocol;
9pub mod outline;
10pub mod project_root;
11pub mod relation_capabilities;
12pub mod support_catalog;
13pub mod symbols;
14pub mod telemetry;
15pub mod toon;
16
17pub use index_work::{
18 IndexCancellation, IndexWorkControl, IndexWorkFailure, IndexWorkResource, IndexWorkStage,
19};
20pub use project_root::CanonicalProjectRoot;
21
22pub const MAX_GIT_WORKTREE_REGISTRATIONS: usize = 1_024;
24
25use serde::{Deserialize, Serialize};
26use std::fmt;
27use std::path::{Path, PathBuf, StripPrefixError};
28use thiserror::Error;
29
30#[derive(Debug, Error)]
32pub enum CoreError {
33 #[error("invalid canonical project root {path:?}: {reason}")]
35 InvalidCanonicalProjectRoot {
36 path: PathBuf,
38 reason: &'static str,
40 },
41 #[error("could not canonicalize project root {path:?}: {source}")]
43 CanonicalProjectRootIo {
44 path: PathBuf,
46 #[source]
48 source: std::io::Error,
49 },
50 #[error("invalid canonical project-root codec value: {reason}")]
52 CanonicalProjectRootCodec {
53 reason: &'static str,
55 },
56 #[error("path is outside the repository root: {path}")]
58 PathOutsideRoot {
59 path: PathBuf,
61 source: StripPrefixError,
63 },
64 #[error("path is not valid UTF-8: {path:?}")]
66 NonUtf8Path {
67 path: PathBuf,
69 },
70 #[error("path {path:?} must be a project-relative indexed file path: {reason}")]
72 InvalidRepositoryPath {
73 path: PathBuf,
75 reason: &'static str,
77 },
78}
79
80pub type CoreResult<T> = Result<T, CoreError>;
82
83#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
85#[serde(transparent)]
86pub struct IndexGeneration(u64);
87
88impl IndexGeneration {
89 pub const ZERO: Self = Self(0);
91
92 #[must_use]
94 pub const fn new(value: u64) -> Self {
95 Self(value)
96 }
97
98 #[must_use]
100 pub const fn get(self) -> u64 {
101 self.0
102 }
103
104 #[must_use]
106 pub const fn checked_next(self) -> Option<Self> {
107 match self.0.checked_add(1) {
108 Some(value) => Some(Self(value)),
109 None => None,
110 }
111 }
112}
113
114impl fmt::Display for IndexGeneration {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 self.0.fmt(formatter)
117 }
118}
119
120#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
122#[serde(rename_all = "lowercase")]
123pub enum NodeKind {
124 Folder,
126 File,
128}
129
130impl fmt::Display for NodeKind {
131 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132 match self {
133 Self::Folder => formatter.write_str("folder"),
134 Self::File => formatter.write_str("file"),
135 }
136 }
137}
138
139impl NodeKind {
140 #[must_use]
142 pub fn from_db(value: &str) -> Option<Self> {
143 match value {
144 "folder" => Some(Self::Folder),
145 "file" => Some(Self::File),
146 _ => None,
147 }
148 }
149}
150
151#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
153#[serde(rename_all = "lowercase")]
154pub enum PurposeStatus {
155 Missing,
157 Suggested,
159 Approved,
161 Stale,
166}
167
168impl fmt::Display for PurposeStatus {
169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170 formatter.write_str(self.as_str())
171 }
172}
173
174impl PurposeStatus {
175 #[must_use]
177 pub const fn as_str(self) -> &'static str {
178 match self {
179 Self::Missing => "missing",
180 Self::Suggested => "suggested",
181 Self::Approved => "approved",
182 Self::Stale => "stale",
183 }
184 }
185
186 #[must_use]
188 pub fn from_db(value: &str) -> Option<Self> {
189 match value {
190 value if value == Self::Missing.as_str() => Some(Self::Missing),
191 value if value == Self::Suggested.as_str() => Some(Self::Suggested),
192 value if value == Self::Approved.as_str() => Some(Self::Approved),
193 value if value == Self::Stale.as_str() => Some(Self::Stale),
194 _ => None,
195 }
196 }
197}
198
199#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
201#[serde(rename_all = "lowercase")]
202pub enum PurposeSource {
203 Missing,
205 Imported,
207 Generated,
209 Agent,
211}
212
213impl fmt::Display for PurposeSource {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 formatter.write_str(self.as_str())
216 }
217}
218
219impl PurposeSource {
220 #[must_use]
222 pub const fn as_str(self) -> &'static str {
223 match self {
224 Self::Missing => "missing",
225 Self::Imported => "imported",
226 Self::Generated => "generated",
227 Self::Agent => "agent",
228 }
229 }
230}
231
232#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
234#[serde(rename_all = "lowercase")]
235pub enum PurposeReviewPriority {
236 High,
238 Low,
240}
241
242impl fmt::Display for PurposeReviewPriority {
243 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Self::High => formatter.write_str("high"),
246 Self::Low => formatter.write_str("low"),
247 }
248 }
249}
250
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
253pub struct PurposeReviewSignal {
254 pub priority: PurposeReviewPriority,
256 pub reason: &'static str,
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262pub struct Node {
263 pub path: String,
265 pub kind: NodeKind,
267 pub parent_path: Option<String>,
269 pub extension: Option<String>,
271 pub language: Option<String>,
273 pub size_bytes: Option<u64>,
275 pub mtime_ns: Option<i64>,
277 pub content_hash: Option<String>,
279}
280
281#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
283pub struct Purpose {
284 pub path: String,
286 pub purpose: Option<String>,
288 pub source: PurposeSource,
290 pub status: PurposeStatus,
292}
293
294impl Purpose {
295 #[must_use]
297 pub fn agent_reviewed(&self) -> bool {
298 self.status == PurposeStatus::Approved && self.source == PurposeSource::Agent
299 }
300}
301
302#[must_use]
304pub fn purpose_review_signal(node: &Node, purpose: &Purpose) -> PurposeReviewSignal {
305 if node.kind == NodeKind::Folder {
306 return PurposeReviewSignal {
307 priority: PurposeReviewPriority::High,
308 reason: "folder_navigation",
309 };
310 }
311
312 if node.kind == NodeKind::File
313 && purpose.status == PurposeStatus::Stale
314 && purpose.source == PurposeSource::Agent
315 && is_high_impact_file_path(&node.path)
316 {
317 return PurposeReviewSignal {
318 priority: PurposeReviewPriority::High,
319 reason: "stale_agent_reviewed_file",
320 };
321 }
322
323 if node.kind == NodeKind::File && is_high_impact_file_path(&node.path) {
324 return PurposeReviewSignal {
325 priority: PurposeReviewPriority::High,
326 reason: "high_impact_file",
327 };
328 }
329
330 if node.kind == NodeKind::File && purpose.status == PurposeStatus::Suggested {
331 return PurposeReviewSignal {
332 priority: PurposeReviewPriority::Low,
333 reason: "generated_file_suggestion",
334 };
335 }
336
337 PurposeReviewSignal {
338 priority: PurposeReviewPriority::Low,
339 reason: "selective_file_review",
340 }
341}
342
343#[must_use]
345pub fn is_high_impact_file_path(path: &str) -> bool {
346 let normalized = path.replace('\\', "/").to_lowercase();
347 let file_name = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
348 HIGH_IMPACT_FILE_NAMES.contains(&file_name)
349 || HIGH_IMPACT_PATH_PREFIXES
350 .iter()
351 .any(|prefix| normalized.starts_with(prefix))
352 || HIGH_IMPACT_PATH_SEGMENTS
353 .iter()
354 .any(|segment| normalized.contains(segment))
355}
356
357pub const HIGH_IMPACT_FILE_NAMES: &[&str] = &[
359 "cargo.toml",
360 "package.json",
361 "pyproject.toml",
362 "build.gradle",
363 "build.gradle.kts",
364 "settings.gradle",
365 "settings.gradle.kts",
366 "gradle.properties",
367 "dockerfile",
368 "makefile",
369 "justfile",
370 "main.rs",
371 "lib.rs",
372 "mod.rs",
373 "main.py",
374 "app.py",
375 "server.py",
376 "index.ts",
377 "main.ts",
378 "server.ts",
379 "app.ts",
380 "index.tsx",
381 "app.tsx",
382];
383
384pub const HIGH_IMPACT_PATH_PREFIXES: &[&str] = &[".github/workflows/"];
386
387pub const HIGH_IMPACT_PATH_SEGMENTS: &[&str] = &["/migrations/", "/routes/", "/commands/", "/mcp"];
389
390pub const LEGACY_HUMAN_PURPOSE_SOURCE: &str = "human";
392
393pub const AGENT_REVIEWED_SOURCE_VALUES: &[&str] =
395 &[PurposeSource::Agent.as_str(), LEGACY_HUMAN_PURPOSE_SOURCE];
396
397#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
399pub struct IndexedNode {
400 pub node: Node,
402 pub purpose: Purpose,
404 pub summary: Option<String>,
406}
407
408#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
410#[serde(rename_all = "snake_case")]
411pub enum RankedReasonCode {
412 ExactPath,
414 ExactName,
416 ReviewedPurpose,
418 Path,
420 Summary,
422 Symbol,
424 IndexedText,
426 PairedFile,
428 GraphPackage,
430 GraphImport,
432 GraphCall,
434 GraphReference,
436 GraphTest,
438 GraphRoute,
440 GraphConfig,
442}
443
444#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
446#[serde(rename_all = "snake_case")]
447pub enum RankedConnectionKind {
448 Package,
450 Import,
452 Call,
454 Reference,
456 Test,
458 Route,
460 Config,
462}
463
464#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
466#[serde(rename_all = "snake_case")]
467pub enum RankedConnectionDirection {
468 Outbound,
470 Inbound,
472}
473
474#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
476#[serde(tag = "kind", rename_all = "snake_case")]
477pub enum RankedConnectionTarget {
478 Local {
480 path: String,
482 symbol: Option<String>,
484 },
485 Package {
487 manager: String,
489 name: String,
491 manifest: String,
493 },
494 External {
496 system: String,
498 identity: String,
500 },
501 Unresolved {
503 reference: String,
505 },
506}
507
508#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
510pub struct RankedConnection {
511 pub kind: RankedConnectionKind,
513 pub direction: RankedConnectionDirection,
515 pub target: RankedConnectionTarget,
517}
518
519#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
521pub struct RankedConnectionCount {
522 pub kind: RankedConnectionKind,
524 pub count: usize,
526 pub truncated: bool,
528}
529
530#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
532#[serde(rename_all = "snake_case")]
533pub enum NavigationNextCapability {
534 Files,
536 Summary,
538 Relations,
540 Health,
542}
543
544#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
546pub struct NavigationNextCall {
547 pub capability: NavigationNextCapability,
549 pub path: String,
551}
552
553#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
555pub struct RankedNode {
556 pub node: IndexedNode,
558 pub reasons: Vec<String>,
560 pub reason_codes: Vec<RankedReasonCode>,
562 pub connection_counts: Vec<RankedConnectionCount>,
564 pub connections: Vec<RankedConnection>,
566 pub connections_truncated: bool,
568 pub next_call: NavigationNextCall,
570}
571
572#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
574pub struct Overview {
575 pub files: usize,
577 pub folders: usize,
579 pub missing_purposes: usize,
581 pub stale_purposes: usize,
583 pub approved_purposes: usize,
585 pub suggested_purposes: usize,
587}
588
589pub fn normalize_repo_path(root: &Path, path: &Path) -> CoreResult<String> {
596 let relative = path
597 .strip_prefix(root)
598 .map_err(|source| CoreError::PathOutsideRoot {
599 path: path.to_path_buf(),
600 source,
601 })?;
602 if relative.as_os_str().is_empty() {
603 return Ok(".".to_string());
604 }
605 let as_str = relative.to_str().ok_or_else(|| CoreError::NonUtf8Path {
606 path: relative.to_path_buf(),
607 })?;
608 Ok(as_str.replace('\\', "/"))
609}
610
611#[must_use]
621pub fn normalize_native_path_display(path: impl AsRef<Path>) -> String {
622 normalize_native_path_display_str(&path.as_ref().to_string_lossy())
623}
624
625#[must_use]
630pub fn normalize_native_path_display_str(path: &str) -> String {
631 #[cfg(windows)]
632 {
633 let normalized = path.replace('\\', "/");
634 if let Some(rest) = normalized.strip_prefix("//?/UNC/") {
635 format!("//{rest}")
636 } else if let Some(rest) = normalized.strip_prefix("//?/") {
637 rest.to_string()
638 } else {
639 normalized
640 }
641 }
642 #[cfg(not(windows))]
643 {
644 path.to_owned()
645 }
646}
647
648pub fn lossless_native_path_display(path: &Path) -> CoreResult<String> {
661 let original = path.to_str().ok_or_else(|| CoreError::NonUtf8Path {
662 path: path.to_path_buf(),
663 })?;
664 let normalized = normalize_native_path_display_str(original);
665 if Path::new(&normalized).is_absolute() && !windows_verbatim_semantics_require_prefix(path) {
666 Ok(normalized)
667 } else {
668 Ok(original.to_owned())
669 }
670}
671
672#[cfg(windows)]
673fn windows_verbatim_semantics_require_prefix(path: &Path) -> bool {
675 let Some(value) = path.to_str() else {
676 return true;
677 };
678 if !value.starts_with("\\\\?\\") {
679 return false;
680 }
681 windows_path_requires_verbatim_semantics(path)
682}
683
684#[must_use]
691pub fn windows_path_requires_verbatim_semantics(path: &Path) -> bool {
692 #[cfg(windows)]
693 {
694 let Some(value) = path.to_str() else {
695 return true;
696 };
697 let normalized = normalize_native_path_display_str(value);
698 let project_atlas_suffix_units = r"\.projectatlas\projectatlas.db".encode_utf16().count();
700 if normalized
701 .encode_utf16()
702 .count()
703 .saturating_add(project_atlas_suffix_units)
704 >= 260
705 {
706 return true;
707 }
708 windows_path_has_verbatim_only_components(path)
709 }
710 #[cfg(not(windows))]
711 {
712 let _ = path;
713 false
714 }
715}
716
717#[must_use]
722pub fn windows_path_has_verbatim_only_components(path: &Path) -> bool {
723 #[cfg(windows)]
724 {
725 use std::path::Component;
726
727 path.components().any(|component| {
728 let Component::Normal(component) = component else {
729 return false;
730 };
731 let Some(component) = component.to_str() else {
732 return true;
733 };
734 if component.ends_with(['.', ' ']) {
735 return true;
736 }
737 let name = component
738 .split_once('.')
739 .map_or(component, |(stem, _)| stem);
740 let upper = name.to_ascii_uppercase();
741 matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
742 || matches!(upper.as_str(), "CONIN$" | "CONOUT$")
743 || matches!(
744 upper.as_str(),
745 "COM¹" | "COM²" | "COM³" | "LPT¹" | "LPT²" | "LPT³"
746 )
747 || (upper.len() == 4
748 && (upper.starts_with("COM") || upper.starts_with("LPT"))
749 && upper.as_bytes()[3].is_ascii_digit()
750 && upper.as_bytes()[3] != b'0')
751 })
752 }
753 #[cfg(not(windows))]
754 {
755 let _ = path;
756 false
757 }
758}
759
760#[cfg(not(windows))]
761fn windows_verbatim_semantics_require_prefix(_path: &Path) -> bool {
763 false
764}
765
766pub fn validated_repo_file_key(file: &Path) -> CoreResult<String> {
773 let key = validated_repo_node_key(file)?;
774 if key == "." {
775 return Err(CoreError::InvalidRepositoryPath {
776 path: file.to_path_buf(),
777 reason: "a file path is required",
778 });
779 }
780 Ok(key)
781}
782
783pub fn validated_repo_node_key(file: &Path) -> CoreResult<String> {
793 let raw = file
794 .to_str()
795 .ok_or_else(|| CoreError::NonUtf8Path {
796 path: file.to_path_buf(),
797 })?
798 .replace('\\', "/");
799 if raw.trim().is_empty() {
800 return Err(CoreError::InvalidRepositoryPath {
801 path: file.to_path_buf(),
802 reason: "a path is required",
803 });
804 }
805 if raw.starts_with('/') || raw.starts_with("//") || has_windows_drive_prefix(&raw) {
806 return Err(CoreError::InvalidRepositoryPath {
807 path: file.to_path_buf(),
808 reason: "absolute paths are not allowed",
809 });
810 }
811 let mut parts = Vec::new();
812 for component in raw.split('/') {
813 match component {
814 "" | "." => {}
815 ".." => {
816 return Err(CoreError::InvalidRepositoryPath {
817 path: file.to_path_buf(),
818 reason: "parent traversal is not allowed",
819 });
820 }
821 part => parts.push(part.to_string()),
822 }
823 }
824 if parts.is_empty() {
825 return Ok(".".to_string());
826 }
827 Ok(parts.join("/"))
828}
829
830#[must_use]
832pub fn repo_path_to_native(path: &str) -> PathBuf {
833 path.split('/').fold(PathBuf::new(), |mut native, part| {
834 native.push(part);
835 native
836 })
837}
838
839#[must_use]
845pub fn normalize_repo_path_prefix(value: &str) -> String {
846 let normalized = value
847 .replace('\\', "/")
848 .trim()
849 .trim_start_matches("./")
850 .trim_end_matches('/')
851 .to_string();
852 if normalized.is_empty() {
853 ".".to_string()
854 } else {
855 normalized
856 }
857}
858
859fn has_windows_drive_prefix(path: &str) -> bool {
861 let bytes = path.as_bytes();
862 bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic()
863}
864
865#[must_use]
867pub fn normalized_parent(path: &str) -> Option<String> {
868 if path == "." {
869 return None;
870 }
871 let parent = Path::new(path).parent()?;
872 if parent.as_os_str().is_empty() {
873 Some(".".to_string())
874 } else {
875 Some(parent.to_string_lossy().replace('\\', "/"))
876 }
877}
878
879#[must_use]
881pub fn normalized_extension(path: &Path) -> Option<String> {
882 language::normalized_language_extension(path)
883}
884
885#[cfg(test)]
886mod tests {
887 use super::{
888 Node, NodeKind, Purpose, PurposeReviewPriority, PurposeSource, PurposeStatus,
889 is_high_impact_file_path, normalize_native_path_display_str, normalize_repo_path_prefix,
890 normalized_parent, purpose_review_signal, repo_path_to_native, validated_repo_file_key,
891 validated_repo_node_key,
892 };
893 use std::io;
894 use std::path::Path;
895
896 #[test]
897 fn validated_repo_file_key_normalizes_safe_relative_paths()
898 -> Result<(), Box<dyn std::error::Error>> {
899 require_eq(
900 &validated_repo_file_key(Path::new("src\\main.rs"))?,
901 "src/main.rs",
902 )?;
903 require_eq(
904 &validated_repo_file_key(Path::new("./src/lib.rs"))?,
905 "src/lib.rs",
906 )?;
907 Ok(())
908 }
909
910 #[test]
911 fn validated_repo_file_key_rejects_absolute_and_parent_paths() {
912 assert!(validated_repo_file_key(Path::new("../secret.rs")).is_err());
913 assert!(validated_repo_file_key(Path::new("C:/secret.rs")).is_err());
914 assert!(validated_repo_file_key(Path::new("/secret.rs")).is_err());
915 assert!(validated_repo_file_key(Path::new(".")).is_err());
916 }
917
918 #[test]
919 fn validated_repo_node_key_accepts_root_and_relative_paths()
920 -> Result<(), Box<dyn std::error::Error>> {
921 require_eq(&validated_repo_node_key(Path::new("."))?, ".")?;
922 require_eq(&validated_repo_node_key(Path::new("./src"))?, "src")?;
923 require_eq(
924 &validated_repo_node_key(Path::new("src\\main.rs"))?,
925 "src/main.rs",
926 )?;
927 Ok(())
928 }
929
930 #[test]
931 fn validated_repo_node_key_rejects_empty_paths() {
932 assert!(validated_repo_node_key(Path::new("")).is_err());
933 assert!(validated_repo_node_key(Path::new(" ")).is_err());
934 }
935
936 #[test]
937 fn repo_path_to_native_builds_platform_path_components() {
938 assert_eq!(
939 repo_path_to_native("src/main.rs"),
940 Path::new("src").join("main.rs")
941 );
942 }
943
944 #[test]
945 fn normalize_repo_path_prefix_accepts_root_and_slashes() {
946 assert_eq!(normalize_repo_path_prefix(""), ".");
947 assert_eq!(normalize_repo_path_prefix("."), ".");
948 assert_eq!(normalize_repo_path_prefix(".\\docs\\api\\"), "docs/api");
949 assert_eq!(normalize_repo_path_prefix("./src/lib"), "src/lib");
950 }
951
952 #[test]
953 fn purpose_review_signal_is_folder_first_and_file_selective() {
954 let folder = test_node("src", NodeKind::Folder);
955 let file = test_node("src/helper.rs", NodeKind::File);
956 let build_file = test_node("build.gradle.kts", NodeKind::File);
957 let suggested = Purpose {
958 path: "src/helper.rs".to_string(),
959 purpose: Some("Generated helper suggestion".to_string()),
960 source: PurposeSource::Generated,
961 status: PurposeStatus::Suggested,
962 };
963 let approved = Purpose {
964 path: "src".to_string(),
965 purpose: Some("Rust source folder".to_string()),
966 source: PurposeSource::Agent,
967 status: PurposeStatus::Approved,
968 };
969 let stale = Purpose {
970 path: "src/helper.rs".to_string(),
971 purpose: Some("Reviewed helper implementation".to_string()),
972 source: PurposeSource::Agent,
973 status: PurposeStatus::Stale,
974 };
975
976 let folder_signal = purpose_review_signal(&folder, &approved);
977 assert_eq!(folder_signal.priority, PurposeReviewPriority::High);
978 assert_eq!(folder_signal.reason, "folder_navigation");
979
980 let file_signal = purpose_review_signal(&file, &suggested);
981 assert_eq!(file_signal.priority, PurposeReviewPriority::Low);
982 assert_eq!(file_signal.reason, "generated_file_suggestion");
983
984 let build_signal = purpose_review_signal(&build_file, &suggested);
985 assert_eq!(build_signal.priority, PurposeReviewPriority::High);
986 assert_eq!(build_signal.reason, "high_impact_file");
987
988 let low_stale_signal = purpose_review_signal(&file, &stale);
989 assert_eq!(low_stale_signal.priority, PurposeReviewPriority::Low);
990 assert_eq!(low_stale_signal.reason, "selective_file_review");
991
992 let high_stale_signal = purpose_review_signal(&build_file, &stale);
993 assert_eq!(high_stale_signal.priority, PurposeReviewPriority::High);
994 assert_eq!(high_stale_signal.reason, "stale_agent_reviewed_file");
995 assert!(is_high_impact_file_path(".github/workflows/release.yml"));
996 }
997
998 #[cfg(windows)]
999 #[test]
1000 fn native_path_display_removes_windows_extended_prefixes() {
1001 assert_eq!(
1002 normalize_native_path_display_str(r"\\?\C:\repo\.projectatlas\projectatlas.db"),
1003 "C:/repo/.projectatlas/projectatlas.db"
1004 );
1005 assert_eq!(
1006 normalize_native_path_display_str(r"\\?\UNC\server\share\repo"),
1007 "//server/share/repo"
1008 );
1009 assert_eq!(
1010 normalize_native_path_display_str("/home/user/repo"), "/home/user/repo" );
1013 assert_eq!(
1014 normalize_native_path_display_str("src\\main.rs"),
1015 "src/main.rs"
1016 );
1017 }
1018
1019 #[cfg(windows)]
1020 #[test]
1021 fn lossless_display_preserves_verbatim_console_device_components()
1022 -> Result<(), Box<dyn std::error::Error>> {
1023 for path in [
1024 r"\\?\C:\repo\CONIN$",
1025 r"\\?\C:\repo\conout$.log",
1026 r"\\?\UNC\server\share\CONIN$",
1027 ] {
1028 let display = super::lossless_native_path_display(Path::new(path))?;
1029 require_eq(&display, path)?;
1030 }
1031 Ok(())
1032 }
1033
1034 #[cfg(windows)]
1035 #[test]
1036 fn windows_verbatim_component_classifier_covers_legacy_spellings()
1037 -> Result<(), Box<dyn std::error::Error>> {
1038 for path in [
1039 r"C:\repo.",
1040 r"C:\repo ",
1041 r"C:\repo\CON",
1042 r"C:\repo\CONIN$",
1043 r"C:\repo\conout$.log",
1044 r"C:\repo\COM1.txt",
1045 r"C:\repo\LPT3.cfg",
1046 r"C:\repo\COM¹",
1047 r"C:\repo\LPT³.log",
1048 ] {
1049 if !super::windows_path_requires_verbatim_semantics(Path::new(path)) {
1050 return Err(format!("verbatim component was not classified: {path}").into());
1051 }
1052 }
1053 for path in [r"C:\repo", r"C:\repo\ordinary.txt", r"\\server\share\repo"] {
1054 if super::windows_path_requires_verbatim_semantics(Path::new(path)) {
1055 return Err(
1056 format!("ordinary component was classified as verbatim: {path}").into(),
1057 );
1058 }
1059 }
1060 let long_path = format!(r"C:\{}", "a".repeat(260));
1061 if !super::windows_path_requires_verbatim_semantics(Path::new(&long_path)) {
1062 return Err("long path was not classified as verbatim".into());
1063 }
1064 let suffix_units = r"\.projectatlas\projectatlas.db".encode_utf16().count();
1065 for units in [260 - suffix_units - 1, 260 - suffix_units] {
1066 let path = format!(r"C:\{}", "a".repeat(units - 3));
1067 let required = units + suffix_units >= 260;
1068 if super::windows_path_requires_verbatim_semantics(Path::new(&path)) != required {
1069 return Err("legacy classifier differs from the live root suffix threshold".into());
1070 }
1071 let extended = format!(r"\\?\{path}");
1072 if super::windows_verbatim_semantics_require_prefix(Path::new(&extended)) != required {
1073 return Err("live root prefix threshold differs from the legacy classifier".into());
1074 }
1075 }
1076 Ok(())
1077 }
1078
1079 #[cfg(not(windows))]
1080 #[test]
1081 fn native_path_display_preserves_unix_backslashes() {
1082 let path = r"/tmp/repo\name";
1083 assert_eq!(normalize_native_path_display_str(path), path);
1084 assert_eq!(super::normalize_native_path_display(Path::new(path)), path);
1085 }
1086
1087 fn require_eq(left: &str, right: &str) -> Result<(), Box<dyn std::error::Error>> {
1088 if left == right {
1089 Ok(())
1090 } else {
1091 Err(io::Error::other(format!("expected {right:?}, found {left:?}")).into())
1092 }
1093 }
1094
1095 fn test_node(path: &str, kind: NodeKind) -> Node {
1096 Node {
1097 path: path.to_string(),
1098 kind,
1099 parent_path: normalized_parent(path),
1100 extension: None,
1101 language: None,
1102 size_bytes: None,
1103 mtime_ns: None,
1104 content_hash: None,
1105 }
1106 }
1107}