grex_core/pack/validate/mod.rs
1//! Plan-phase validators for [`PackManifest`].
2//!
3//! Stage B of M3 introduces **plan-phase** validation — checks that run
4//! after [`crate::pack::parse`] succeeds but before any execute-time work
5//! (variable expansion, filesystem touches, child-pack traversal). The
6//! validators here operate on the already-parsed manifest in its
7//! pre-expansion, literal form.
8//!
9//! # Framework shape
10//!
11//! A [`Validator`] receives an immutable [`PackManifest`] and returns a
12//! `Vec<PackValidationError>` — never fail-first. [`run_all`] composes the
13//! default validator set and concatenates their findings so callers see
14//! the full diagnostic set in one pass. This slice ships one validator
15//! ([`DuplicateSymlinkValidator`]); subsequent M3 slices (cycle detect,
16//! cross-pack conflict, `depends_on` verification) plug into the same
17//! surface without touching orchestrator code.
18//!
19//! # Non-goals for this slice
20//!
21//! * No filesystem IO, no git, no platform probing.
22//! * No variable expansion — validators compare literal `dst` strings.
23//! * No cross-pack reasoning (later slices).
24
25use thiserror::Error;
26
27use super::PackManifest;
28use crate::tree::PackGraph;
29
30pub mod cycle;
31pub mod depends_on;
32pub mod dup_symlink;
33
34pub use cycle::CycleValidator;
35pub use depends_on::DependsOnValidator;
36pub use dup_symlink::DuplicateSymlinkValidator;
37
38/// Errors raised by plan-phase validators.
39///
40/// Marked `#[non_exhaustive]` so future slices (slices 3–6) can add variants
41/// without breaking downstream `match` arms.
42#[non_exhaustive]
43#[derive(Debug, Clone, PartialEq, Eq, Error)]
44pub enum PackValidationError {
45 /// Two `symlink` actions within the same pack resolve to the same
46 /// literal `dst` string. `first` and `second` are indices in the
47 /// flattened action-walk order (see
48 /// [`PackManifest::iter_all_symlinks`]).
49 #[error("duplicate symlink dst `{dst}` (actions at indices {first} and {second})")]
50 DuplicateSymlinkDst {
51 /// Literal `dst` string (pre-expansion).
52 dst: String,
53 /// Global index of the earlier action.
54 first: usize,
55 /// Global index of the later action.
56 second: usize,
57 },
58
59 /// A cycle was detected in the assembled pack graph. `chain` lists the
60 /// pack names from the outermost node down to the recurrence.
61 #[error("cycle detected in pack graph: {chain:?}")]
62 GraphCycle {
63 /// Ordered chain of pack names that forms the cycle.
64 chain: Vec<String>,
65 },
66
67 /// A `depends_on` entry could not be resolved against any node in the
68 /// walked graph.
69 #[error("pack `{pack}` depends on `{required}` but no such pack exists in the graph")]
70 DependsOnUnsatisfied {
71 /// Name of the pack that declared the dependency.
72 pack: String,
73 /// The unresolved `depends_on` entry (a pack name or url).
74 required: String,
75 },
76}
77
78/// A single plan-phase validator.
79///
80/// Implementations run against a fully parsed manifest and return every
81/// problem they observe — never `Result`, because aggregation across
82/// validators is the point.
83pub trait Validator {
84 /// Stable human-readable identifier for diagnostics / allowlisting.
85 fn name(&self) -> &'static str;
86
87 /// Inspect `pack` and emit zero or more errors.
88 fn check(&self, pack: &PackManifest) -> Vec<PackValidationError>;
89}
90
91/// Run every default validator against `pack`, concatenating their
92/// findings.
93///
94/// The current default set is:
95///
96/// 1. [`DuplicateSymlinkValidator`] — two symlinks with the same literal
97/// `dst`.
98///
99/// Later slices extend this list; callers should prefer
100/// [`PackManifest::validate_plan`] over instantiating validators manually,
101/// so the default set stays discoverable.
102#[must_use]
103pub fn run_all(pack: &PackManifest) -> Vec<PackValidationError> {
104 let validators: [&dyn Validator; 1] = [&DuplicateSymlinkValidator];
105 let mut errs = Vec::new();
106 for v in validators {
107 errs.extend(v.check(pack));
108 }
109 errs
110}
111
112/// Plan-phase validator that operates on an assembled [`PackGraph`].
113///
114/// Separate trait from [`Validator`] on purpose: graph-level checks need
115/// the full graph, not a single manifest, and mixing the two into one
116/// trait would force every per-manifest validator to accept a graph it
117/// doesn't need. Two traits keep each call site's surface minimal and
118/// type-safe.
119pub trait GraphValidator {
120 /// Stable human-readable identifier.
121 fn name(&self) -> &'static str;
122
123 /// Inspect `graph` and emit zero or more errors.
124 fn check(&self, graph: &PackGraph) -> Vec<PackValidationError>;
125}
126
127/// Run every default [`GraphValidator`] against `graph`, concatenating
128/// their findings.
129///
130/// Current default set:
131///
132/// 1. [`CycleValidator`] — belt-and-suspenders for cycles the walker
133/// should have caught.
134/// 2. [`DependsOnValidator`] — verify every `depends_on` entry resolves.
135#[must_use]
136pub fn run_all_graph(graph: &PackGraph) -> Vec<PackValidationError> {
137 let validators: [&dyn GraphValidator; 2] = [&CycleValidator, &DependsOnValidator];
138 let mut errs = Vec::new();
139 for v in validators {
140 errs.extend(v.check(graph));
141 }
142 errs
143}
144
145impl PackGraph {
146 /// Run the default graph-validator set over `self`.
147 ///
148 /// Mirrors [`PackManifest::validate_plan`] at the graph surface. Kept
149 /// here (rather than in `tree::graph`) so the `tree` module does not
150 /// depend on `pack::validate`; the dependency direction stays
151 /// `validate -> tree` only.
152 ///
153 /// # Errors
154 ///
155 /// Returns the aggregated error list when any graph validator
156 /// flags a problem.
157 pub fn validate(&self) -> Result<(), Vec<PackValidationError>> {
158 let errs = run_all_graph(self);
159 if errs.is_empty() {
160 Ok(())
161 } else {
162 Err(errs)
163 }
164 }
165}