1use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10
11use crate::{
12 CargoPathDependencyEdge, CargoPathDependencyError, CargoPathDependencyErrorKind,
13 CargoPathDependencyGraph, CargoPathDependencyPackage, CargoPathMaterializationClosure,
14 PathTopologyPolicy, resolve_cargo_path_dependency_graph_with_policy,
15 resolve_cargo_path_materialization_closure_with_policy,
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum DependencyClosurePlanState {
22 Ready,
23 FailOpen,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum DependencyRiskClass {
30 Low,
31 Medium,
32 High,
33 Critical,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum DependencySyncReason {
40 EntryPoint,
41 WorkspaceMember,
42 TransitivePathDependency,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct DependencySyncMetadata {
48 pub reason: DependencySyncReason,
49 pub workspace_member: bool,
50 pub root_package: bool,
51 pub inbound_dependency_names: Vec<String>,
52 pub dependent_roots: Vec<PathBuf>,
53 pub notes: Vec<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct DependencySyncAction {
59 pub order_index: usize,
60 pub package_root: PathBuf,
61 pub manifest_path: PathBuf,
62 pub package_name: String,
63 pub risk: DependencyRiskClass,
64 pub metadata: DependencySyncMetadata,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct DependencyPlanIssue {
70 pub code: String,
71 pub message: String,
72 pub risk: DependencyRiskClass,
73 pub diagnostics: Vec<String>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct DependencyClosurePlan {
79 pub state: DependencyClosurePlanState,
80 pub entry_manifest_path: PathBuf,
81 pub workspace_root: Option<PathBuf>,
82 pub canonical_roots: Vec<PathBuf>,
83 pub sync_order: Vec<DependencySyncAction>,
84 pub fail_open: bool,
85 pub fail_open_reason: Option<String>,
86 pub issues: Vec<DependencyPlanIssue>,
87}
88
89impl DependencyClosurePlan {
90 pub fn is_ready(&self) -> bool {
92 self.state == DependencyClosurePlanState::Ready && !self.fail_open
93 }
94
95 pub fn sync_roots(&self) -> Vec<PathBuf> {
97 self.sync_order
98 .iter()
99 .map(|action| action.package_root.clone())
100 .collect()
101 }
102}
103
104pub fn build_dependency_closure_plan(entrypoint: &Path) -> DependencyClosurePlan {
106 build_dependency_closure_plan_with_policy(entrypoint, &PathTopologyPolicy::default())
107}
108
109pub fn build_dependency_closure_plan_with_policy(
114 entrypoint: &Path,
115 policy: &PathTopologyPolicy,
116) -> DependencyClosurePlan {
117 let graph = match resolve_cargo_path_dependency_graph_with_policy(entrypoint, policy) {
118 Ok(graph) => graph,
119 Err(error) => return fail_open_plan_from_resolver_error(entrypoint, &error),
120 };
121 let materialization_entrypoint = graph.workspace_root.as_deref().unwrap_or(entrypoint);
122 let materialization = match resolve_cargo_path_materialization_closure_with_policy(
123 materialization_entrypoint,
124 policy,
125 ) {
126 Ok(materialization) => materialization,
127 Err(error) => {
128 let mut plan = fail_open_plan_from_resolver_error(entrypoint, &error);
129 plan.issues.insert(
130 0,
131 DependencyPlanIssue {
132 code: "materialization-closure-unavailable".to_string(),
133 message: format!(
134 "Cargo path materialization closure is unavailable: {}: {}",
135 error.kind(),
136 error.detail()
137 ),
138 risk: DependencyRiskClass::Critical,
139 diagnostics: vec![
140 format!(
141 "materialization_entrypoint={}",
142 materialization_entrypoint.display()
143 ),
144 format!("source_error_kind={}", error.kind()),
145 ],
146 },
147 );
148 plan.fail_open_reason = Some(format!(
149 "materialization closure produced {}: {}",
150 error.kind(),
151 error.detail()
152 ));
153 return plan;
154 }
155 };
156
157 plan_dependency_closure_from_graph_and_materialization(&graph, Some(&materialization))
158}
159
160pub fn plan_dependency_closure_from_graph(
162 graph: &CargoPathDependencyGraph,
163) -> DependencyClosurePlan {
164 plan_dependency_closure_from_graph_and_materialization(graph, None)
165}
166
167fn plan_dependency_closure_from_graph_and_materialization(
168 graph: &CargoPathDependencyGraph,
169 materialization: Option<&CargoPathMaterializationClosure>,
170) -> DependencyClosurePlan {
171 let package_by_root: BTreeMap<PathBuf, CargoPathDependencyPackage> = graph
172 .packages
173 .iter()
174 .cloned()
175 .map(|package| (package.package_root.clone(), package))
176 .collect();
177
178 let order = match dependency_first_topological_order(&graph.packages, &graph.edges) {
179 Some(order) => order,
180 None => {
181 return DependencyClosurePlan {
182 state: DependencyClosurePlanState::FailOpen,
183 entry_manifest_path: graph.entry_manifest_path.clone(),
184 workspace_root: graph.workspace_root.clone(),
185 canonical_roots: Vec::new(),
186 sync_order: Vec::new(),
187 fail_open: true,
188 fail_open_reason: Some(
189 "planner could not derive deterministic order from dependency graph"
190 .to_string(),
191 ),
192 issues: vec![DependencyPlanIssue {
193 code: "planner_non_deterministic_order".to_string(),
194 message:
195 "dependency graph order is unverifiable; planner switched to fail-open"
196 .to_string(),
197 risk: DependencyRiskClass::Critical,
198 diagnostics: vec![
199 format!("packages={}", graph.packages.len()),
200 format!("edges={}", graph.edges.len()),
201 ],
202 }],
203 };
204 }
205 };
206
207 let entry_root = graph
208 .entry_manifest_path
209 .parent()
210 .map(Path::to_path_buf)
211 .unwrap_or_else(|| PathBuf::from("/"));
212
213 let root_packages = graph
214 .root_packages
215 .iter()
216 .cloned()
217 .collect::<BTreeSet<PathBuf>>();
218
219 let mut inbound_dependency_names: BTreeMap<PathBuf, BTreeSet<String>> = BTreeMap::new();
220 let mut dependent_roots: BTreeMap<PathBuf, BTreeSet<PathBuf>> = BTreeMap::new();
221 for edge in &graph.edges {
222 inbound_dependency_names
223 .entry(edge.to.clone())
224 .or_default()
225 .insert(edge.dependency_name.clone());
226 dependent_roots
227 .entry(edge.to.clone())
228 .or_default()
229 .insert(edge.from.clone());
230 }
231
232 let mut sync_order = Vec::with_capacity(order.len());
233 for root in &order {
234 let package =
235 package_by_root
236 .get(root)
237 .cloned()
238 .unwrap_or_else(|| CargoPathDependencyPackage {
239 package_root: root.clone(),
240 manifest_path: root.join("Cargo.toml"),
241 package_name: root
242 .file_name()
243 .and_then(|segment| segment.to_str())
244 .unwrap_or("unknown")
245 .to_string(),
246 workspace_member: false,
247 });
248
249 let reason = if package.package_root == entry_root {
250 DependencySyncReason::EntryPoint
251 } else if package.workspace_member {
252 DependencySyncReason::WorkspaceMember
253 } else {
254 DependencySyncReason::TransitivePathDependency
255 };
256
257 let inbound_names = inbound_dependency_names
258 .get(&package.package_root)
259 .map(|set| set.iter().cloned().collect::<Vec<_>>())
260 .unwrap_or_default();
261 let dependents = dependent_roots
262 .get(&package.package_root)
263 .map(|set| set.iter().cloned().collect::<Vec<_>>())
264 .unwrap_or_default();
265
266 let risk = classify_sync_risk(reason, dependents.len());
267 let metadata = DependencySyncMetadata {
268 reason,
269 workspace_member: package.workspace_member,
270 root_package: root_packages.contains(&package.package_root),
271 inbound_dependency_names: inbound_names,
272 dependent_roots: dependents.clone(),
273 notes: vec![format!("dependent_root_count={}", dependents.len())],
274 };
275
276 sync_order.push(DependencySyncAction {
277 order_index: 0,
278 package_root: package.package_root.clone(),
279 manifest_path: package.manifest_path,
280 package_name: package.package_name,
281 risk,
282 metadata,
283 });
284 }
285
286 if let Some(materialization) = materialization {
287 let active_roots = graph
288 .packages
289 .iter()
290 .map(|package| package.package_root.clone())
291 .collect::<BTreeSet<_>>();
292 let materialization_roots = materialization
293 .root_packages
294 .iter()
295 .cloned()
296 .collect::<BTreeSet<_>>();
297 let mut materialization_inbound: BTreeMap<PathBuf, BTreeSet<String>> = BTreeMap::new();
298 let mut materialization_dependents: BTreeMap<PathBuf, BTreeSet<PathBuf>> = BTreeMap::new();
299 for edge in &materialization.edges {
300 materialization_inbound
301 .entry(edge.to.clone())
302 .or_default()
303 .insert(edge.dependency_name.clone());
304 materialization_dependents
305 .entry(edge.to.clone())
306 .or_default()
307 .insert(edge.from.clone());
308 }
309
310 let mut materialization_only = Vec::new();
311 for package in &materialization.packages {
312 if active_roots.contains(&package.package_root) {
313 continue;
314 }
315
316 let reason = if package.workspace_member {
317 DependencySyncReason::WorkspaceMember
318 } else {
319 DependencySyncReason::TransitivePathDependency
320 };
321 let inbound_names = materialization_inbound
322 .get(&package.package_root)
323 .map(|set| set.iter().cloned().collect::<Vec<_>>())
324 .unwrap_or_default();
325 let dependents = materialization_dependents
326 .get(&package.package_root)
327 .map(|set| set.iter().cloned().collect::<Vec<_>>())
328 .unwrap_or_default();
329
330 materialization_only.push(DependencySyncAction {
331 order_index: 0,
332 package_root: package.package_root.clone(),
333 manifest_path: package.manifest_path.clone(),
334 package_name: package.package_name.clone(),
335 risk: classify_sync_risk(reason, dependents.len()),
336 metadata: DependencySyncMetadata {
337 reason,
338 workspace_member: package.workspace_member,
339 root_package: materialization_roots.contains(&package.package_root),
340 inbound_dependency_names: inbound_names,
341 dependent_roots: dependents.clone(),
342 notes: vec![
343 format!("dependent_root_count={}", dependents.len()),
344 "materialization_only=true".to_string(),
345 "active_execution_edge=false".to_string(),
346 ],
347 },
348 });
349 }
350
351 materialization_only.extend(sync_order);
352 sync_order = materialization_only;
353 }
354
355 for (order_index, action) in sync_order.iter_mut().enumerate() {
356 action.order_index = order_index;
357 }
358
359 let canonical_roots = sync_order
360 .iter()
361 .map(|action| action.package_root.clone())
362 .collect::<Vec<_>>();
363
364 DependencyClosurePlan {
365 state: DependencyClosurePlanState::Ready,
366 entry_manifest_path: graph.entry_manifest_path.clone(),
367 workspace_root: graph.workspace_root.clone(),
368 canonical_roots,
369 sync_order,
370 fail_open: false,
371 fail_open_reason: None,
372 issues: Vec::new(),
373 }
374}
375
376fn classify_sync_risk(
377 reason: DependencySyncReason,
378 dependent_root_count: usize,
379) -> DependencyRiskClass {
380 match reason {
381 DependencySyncReason::EntryPoint | DependencySyncReason::WorkspaceMember => {
382 DependencyRiskClass::Low
383 }
384 DependencySyncReason::TransitivePathDependency => {
385 if dependent_root_count > 1 {
386 DependencyRiskClass::High
387 } else {
388 DependencyRiskClass::Medium
389 }
390 }
391 }
392}
393
394fn dependency_first_topological_order(
395 packages: &[CargoPathDependencyPackage],
396 edges: &[CargoPathDependencyEdge],
397) -> Option<Vec<PathBuf>> {
398 let mut nodes = packages
399 .iter()
400 .map(|package| package.package_root.clone())
401 .collect::<BTreeSet<_>>();
402 for edge in edges {
403 nodes.insert(edge.from.clone());
404 nodes.insert(edge.to.clone());
405 }
406
407 let mut indegree = nodes
408 .iter()
409 .cloned()
410 .map(|node| (node, 0usize))
411 .collect::<BTreeMap<_, _>>();
412 let mut dependents_by_dependency: BTreeMap<PathBuf, BTreeSet<PathBuf>> = BTreeMap::new();
413
414 for edge in edges {
415 let from_indegree = indegree.get_mut(&edge.from)?;
416 *from_indegree += 1;
417 dependents_by_dependency
418 .entry(edge.to.clone())
419 .or_default()
420 .insert(edge.from.clone());
421 }
422
423 let mut ready = indegree
424 .iter()
425 .filter_map(|(node, degree)| {
426 if *degree == 0 {
427 Some(node.clone())
428 } else {
429 None
430 }
431 })
432 .collect::<BTreeSet<_>>();
433
434 let mut order = Vec::with_capacity(indegree.len());
435 while let Some(node) = ready.pop_first() {
436 order.push(node.clone());
437 if let Some(dependents) = dependents_by_dependency.get(&node) {
438 for dependent in dependents {
439 let degree = indegree.get_mut(dependent)?;
440 if *degree == 0 {
441 return None;
442 }
443 *degree -= 1;
444 if *degree == 0 {
445 ready.insert(dependent.clone());
446 }
447 }
448 }
449 }
450
451 if order.len() == indegree.len() {
452 Some(order)
453 } else {
454 None
455 }
456}
457
458fn fail_open_plan_from_resolver_error(
459 entrypoint: &Path,
460 error: &CargoPathDependencyError,
461) -> DependencyClosurePlan {
462 let issue = issue_from_resolver_error(error);
463 let entry_manifest_path = error
464 .manifest_path()
465 .map(Path::to_path_buf)
466 .unwrap_or_else(|| entrypoint.to_path_buf());
467
468 DependencyClosurePlan {
469 state: DependencyClosurePlanState::FailOpen,
470 entry_manifest_path,
471 workspace_root: None,
472 canonical_roots: Vec::new(),
473 sync_order: Vec::new(),
474 fail_open: true,
475 fail_open_reason: Some(format!(
476 "resolver produced {}: {}",
477 error.kind(),
478 error.detail()
479 )),
480 issues: vec![issue],
481 }
482}
483
484fn issue_from_resolver_error(error: &CargoPathDependencyError) -> DependencyPlanIssue {
485 let (code, risk) = match error.kind() {
486 CargoPathDependencyErrorKind::ManifestParseFailure => {
487 ("manifest-parse-failure", DependencyRiskClass::Critical)
488 }
489 CargoPathDependencyErrorKind::MetadataParseFailure => {
490 ("metadata-parse-failure", DependencyRiskClass::Critical)
491 }
492 CargoPathDependencyErrorKind::MetadataInvocationFailure => {
493 ("metadata-invocation-failure", DependencyRiskClass::Critical)
494 }
495 CargoPathDependencyErrorKind::CyclicDependency => {
496 ("cyclic-path-dependency", DependencyRiskClass::Critical)
497 }
498 CargoPathDependencyErrorKind::PathPolicyViolation => {
499 ("path-policy-violation", DependencyRiskClass::High)
500 }
501 CargoPathDependencyErrorKind::MissingPathDependency => {
502 ("missing-path-dependency", DependencyRiskClass::High)
503 }
504 };
505
506 let mut diagnostics = error.diagnostics().to_vec();
507 if let Some(dependency_name) = error.dependency_name() {
508 diagnostics.push(format!("dependency_name={dependency_name}"));
509 }
510 if let Some(dependency_path) = error.dependency_path() {
511 diagnostics.push(format!("dependency_path={}", dependency_path.display()));
512 }
513 if !error.cycle().is_empty() {
514 diagnostics.push(format!("cycle={:?}", error.cycle()));
515 }
516
517 DependencyPlanIssue {
518 code: code.to_string(),
519 message: format!("{}: {}", error.kind(), error.detail()),
520 risk,
521 diagnostics,
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn package(root: &str, name: &str, workspace_member: bool) -> CargoPathDependencyPackage {
530 CargoPathDependencyPackage {
531 package_root: PathBuf::from(root),
532 manifest_path: PathBuf::from(root).join("Cargo.toml"),
533 package_name: name.to_string(),
534 workspace_member,
535 }
536 }
537
538 fn edge(from: &str, to: &str, dependency_name: &str) -> CargoPathDependencyEdge {
539 CargoPathDependencyEdge {
540 from: PathBuf::from(from),
541 to: PathBuf::from(to),
542 dependency_name: dependency_name.to_string(),
543 }
544 }
545
546 #[test]
547 fn planner_produces_dependency_first_deterministic_sync_order() {
548 let graph = CargoPathDependencyGraph {
549 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
550 workspace_root: Some(PathBuf::from("/data/projects")),
551 root_packages: vec![PathBuf::from("/data/projects/app")],
552 packages: vec![
553 package("/data/projects/app", "app", true),
554 package("/data/projects/lib_a", "lib_a", false),
555 package("/data/projects/lib_b", "lib_b", false),
556 ],
557 edges: vec![
558 edge("/data/projects/app", "/data/projects/lib_a", "lib_a"),
559 edge("/data/projects/lib_a", "/data/projects/lib_b", "lib_b"),
560 ],
561 };
562
563 let plan = plan_dependency_closure_from_graph(&graph);
564 assert!(plan.is_ready(), "acyclic graph should be planner-ready");
565 assert_eq!(plan.sync_order.len(), 3);
566
567 let ordered_roots = plan
568 .sync_order
569 .iter()
570 .map(|action| action.package_root.as_path())
571 .collect::<Vec<_>>();
572 assert_eq!(
573 ordered_roots,
574 vec![
575 Path::new("/data/projects/lib_b"),
576 Path::new("/data/projects/lib_a"),
577 Path::new("/data/projects/app"),
578 ],
579 "planner must sync dependencies before dependents"
580 );
581 assert_eq!(
582 plan.sync_order[0].metadata.reason,
583 DependencySyncReason::TransitivePathDependency
584 );
585 assert_eq!(
586 plan.sync_order[2].metadata.reason,
587 DependencySyncReason::EntryPoint
588 );
589 }
590
591 #[test]
592 fn planner_cycle_fails_open_with_stable_issue_code() {
593 let graph = CargoPathDependencyGraph {
594 entry_manifest_path: PathBuf::from("/data/projects/cycle_a/Cargo.toml"),
595 workspace_root: None,
596 root_packages: vec![PathBuf::from("/data/projects/cycle_a")],
597 packages: vec![
598 package("/data/projects/cycle_a", "cycle_a", false),
599 package("/data/projects/cycle_b", "cycle_b", false),
600 ],
601 edges: vec![
602 edge(
603 "/data/projects/cycle_a",
604 "/data/projects/cycle_b",
605 "cycle_b",
606 ),
607 edge(
608 "/data/projects/cycle_b",
609 "/data/projects/cycle_a",
610 "cycle_a",
611 ),
612 ],
613 };
614
615 let plan = plan_dependency_closure_from_graph(&graph);
616 assert_eq!(plan.state, DependencyClosurePlanState::FailOpen);
617 assert!(plan.fail_open);
618 assert_eq!(plan.sync_order.len(), 0);
619 assert_eq!(plan.issues.len(), 1);
620 assert_eq!(plan.issues[0].code, "planner_non_deterministic_order");
621 assert_eq!(plan.issues[0].risk, DependencyRiskClass::Critical);
622 }
623
624 #[test]
625 fn planner_unions_cycle_tolerant_materialization_without_changing_active_dag() {
626 let graph = CargoPathDependencyGraph {
627 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
628 workspace_root: None,
629 root_packages: vec![PathBuf::from("/data/projects/app")],
630 packages: vec![
631 package("/data/projects/app", "app", false),
632 package("/data/projects/real_dep", "real_dep", false),
633 ],
634 edges: vec![edge(
635 "/data/projects/app",
636 "/data/projects/real_dep",
637 "real_dep",
638 )],
639 };
640 let materialization = CargoPathMaterializationClosure {
641 entry_manifest_path: graph.entry_manifest_path.clone(),
642 workspace_root: None,
643 root_packages: graph.root_packages.clone(),
644 packages: vec![
645 package("/data/projects/app", "app", false),
646 package("/data/projects/optional_a", "optional_a", false),
647 package("/data/projects/optional_b", "optional_b", false),
648 package("/data/projects/real_dep", "real_dep", false),
649 ],
650 edges: vec![
651 edge(
652 "/data/projects/app",
653 "/data/projects/optional_a",
654 "optional_a",
655 ),
656 edge("/data/projects/app", "/data/projects/real_dep", "real_dep"),
657 edge(
658 "/data/projects/optional_a",
659 "/data/projects/optional_b",
660 "optional_b",
661 ),
662 edge(
663 "/data/projects/optional_b",
664 "/data/projects/optional_a",
665 "optional_a",
666 ),
667 ],
668 };
669
670 let plan =
671 plan_dependency_closure_from_graph_and_materialization(&graph, Some(&materialization));
672
673 assert!(
674 plan.is_ready(),
675 "inactive optional cycles are materialization evidence, not active DAG edges"
676 );
677 assert_eq!(
678 plan.sync_roots().into_iter().collect::<BTreeSet<_>>(),
679 BTreeSet::from([
680 PathBuf::from("/data/projects/app"),
681 PathBuf::from("/data/projects/optional_a"),
682 PathBuf::from("/data/projects/optional_b"),
683 PathBuf::from("/data/projects/real_dep"),
684 ])
685 );
686 let real_dep_position = plan
687 .sync_order
688 .iter()
689 .position(|action| action.package_name == "real_dep")
690 .expect("active dependency action");
691 let app_position = plan
692 .sync_order
693 .iter()
694 .position(|action| action.package_name == "app")
695 .expect("active entry action");
696 assert!(
697 real_dep_position < app_position,
698 "active dependency-first order must remain intact"
699 );
700 for package_name in ["optional_a", "optional_b"] {
701 let action = plan
702 .sync_order
703 .iter()
704 .find(|action| action.package_name == package_name)
705 .expect("materialization-only action");
706 assert!(
707 action
708 .metadata
709 .notes
710 .iter()
711 .any(|note| note == "materialization_only=true")
712 );
713 }
714 }
715
716 #[test]
717 fn resolver_error_mapping_reports_path_policy_violation_code() {
718 let error = CargoPathDependencyError::new(
719 CargoPathDependencyErrorKind::PathPolicyViolation,
720 "dependency escaped canonical root",
721 )
722 .with_manifest_path("/data/projects/app/Cargo.toml")
723 .with_dependency_name("bad_dep")
724 .with_dependency_path("/tmp/outside");
725
726 let issue = issue_from_resolver_error(&error);
727 assert_eq!(issue.code, "path-policy-violation");
728 assert_eq!(issue.risk, DependencyRiskClass::High);
729 assert!(
730 issue
731 .diagnostics
732 .iter()
733 .any(|line| line.contains("dependency_path=/tmp/outside"))
734 );
735 }
736
737 #[test]
738 fn resolver_error_mapping_reports_manifest_parse_failure_code() {
739 let error = CargoPathDependencyError::new(
740 CargoPathDependencyErrorKind::ManifestParseFailure,
741 "invalid Cargo.toml syntax",
742 )
743 .with_manifest_path("/data/projects/app/Cargo.toml");
744
745 let plan = fail_open_plan_from_resolver_error(Path::new("/data/projects/app"), &error);
746 assert_eq!(plan.state, DependencyClosurePlanState::FailOpen);
747 assert_eq!(plan.issues.len(), 1);
748 assert_eq!(plan.issues[0].code, "manifest-parse-failure");
749 assert_eq!(plan.issues[0].risk, DependencyRiskClass::Critical);
750 assert!(
751 plan.fail_open_reason
752 .as_deref()
753 .is_some_and(|reason| reason.contains("manifest parse failure"))
754 );
755 }
756
757 #[test]
762 fn empty_graph_produces_empty_ready_plan() {
763 let graph = CargoPathDependencyGraph {
764 entry_manifest_path: PathBuf::from("/data/projects/empty/Cargo.toml"),
765 workspace_root: None,
766 root_packages: Vec::new(),
767 packages: Vec::new(),
768 edges: Vec::new(),
769 };
770
771 let plan = plan_dependency_closure_from_graph(&graph);
772 assert!(plan.is_ready(), "empty graph should still be ready");
773 assert_eq!(plan.sync_order.len(), 0);
774 assert_eq!(plan.canonical_roots.len(), 0);
775 assert!(plan.issues.is_empty());
776 assert!(!plan.fail_open);
777 }
778
779 #[test]
780 fn single_package_no_deps_produces_single_entry_point_action() {
781 let graph = CargoPathDependencyGraph {
782 entry_manifest_path: PathBuf::from("/data/projects/solo/Cargo.toml"),
783 workspace_root: None,
784 root_packages: vec![PathBuf::from("/data/projects/solo")],
785 packages: vec![package("/data/projects/solo", "solo-crate", false)],
786 edges: Vec::new(),
787 };
788
789 let plan = plan_dependency_closure_from_graph(&graph);
790 assert!(plan.is_ready());
791 assert_eq!(plan.sync_order.len(), 1);
792 assert_eq!(plan.sync_order[0].order_index, 0);
793 assert_eq!(plan.sync_order[0].package_name, "solo-crate");
794 assert_eq!(
795 plan.sync_order[0].metadata.reason,
796 DependencySyncReason::EntryPoint
797 );
798 assert_eq!(plan.sync_order[0].risk, DependencyRiskClass::Low);
799 assert!(
800 plan.sync_order[0]
801 .metadata
802 .inbound_dependency_names
803 .is_empty()
804 );
805 assert!(plan.sync_order[0].metadata.dependent_roots.is_empty());
806 }
807
808 #[test]
813 fn diamond_dependency_preserves_deterministic_order() {
814 let graph = CargoPathDependencyGraph {
815 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
816 workspace_root: Some(PathBuf::from("/data/projects")),
817 root_packages: vec![PathBuf::from("/data/projects/app")],
818 packages: vec![
819 package("/data/projects/app", "app", true),
820 package("/data/projects/b", "lib_b", false),
821 package("/data/projects/c", "lib_c", false),
822 package("/data/projects/d", "lib_d", false),
823 ],
824 edges: vec![
825 edge("/data/projects/app", "/data/projects/b", "lib_b"),
826 edge("/data/projects/app", "/data/projects/c", "lib_c"),
827 edge("/data/projects/b", "/data/projects/d", "lib_d"),
828 edge("/data/projects/c", "/data/projects/d", "lib_d"),
829 ],
830 };
831
832 let plan = plan_dependency_closure_from_graph(&graph);
833 assert!(plan.is_ready(), "diamond graph should be planner-ready");
834 assert_eq!(plan.sync_order.len(), 4);
835
836 let ordered_roots: Vec<_> = plan
837 .sync_order
838 .iter()
839 .map(|a| a.package_root.as_path())
840 .collect();
841
842 let d_pos = ordered_roots
844 .iter()
845 .position(|r| *r == Path::new("/data/projects/d"))
846 .unwrap();
847 let b_pos = ordered_roots
848 .iter()
849 .position(|r| *r == Path::new("/data/projects/b"))
850 .unwrap();
851 let c_pos = ordered_roots
852 .iter()
853 .position(|r| *r == Path::new("/data/projects/c"))
854 .unwrap();
855 let app_pos = ordered_roots
856 .iter()
857 .position(|r| *r == Path::new("/data/projects/app"))
858 .unwrap();
859 assert!(d_pos < b_pos, "D must sync before B");
860 assert!(d_pos < c_pos, "D must sync before C");
861 assert!(b_pos < app_pos, "B must sync before app");
862 assert!(c_pos < app_pos, "C must sync before app");
863
864 let d_action = &plan.sync_order[d_pos];
866 assert_eq!(d_action.risk, DependencyRiskClass::High);
867 assert_eq!(d_action.metadata.dependent_roots.len(), 2);
868
869 let b_action = &plan.sync_order[b_pos];
871 assert_eq!(b_action.risk, DependencyRiskClass::Medium);
872
873 let app_action = &plan.sync_order[app_pos];
875 assert_eq!(app_action.risk, DependencyRiskClass::Low);
876 }
877
878 #[test]
883 fn classify_sync_risk_entry_point_is_always_low() {
884 assert_eq!(
885 classify_sync_risk(DependencySyncReason::EntryPoint, 0),
886 DependencyRiskClass::Low
887 );
888 assert_eq!(
889 classify_sync_risk(DependencySyncReason::EntryPoint, 10),
890 DependencyRiskClass::Low
891 );
892 }
893
894 #[test]
895 fn classify_sync_risk_workspace_member_is_always_low() {
896 assert_eq!(
897 classify_sync_risk(DependencySyncReason::WorkspaceMember, 0),
898 DependencyRiskClass::Low
899 );
900 assert_eq!(
901 classify_sync_risk(DependencySyncReason::WorkspaceMember, 5),
902 DependencyRiskClass::Low
903 );
904 }
905
906 #[test]
907 fn classify_sync_risk_transitive_with_zero_dependents_is_medium() {
908 assert_eq!(
909 classify_sync_risk(DependencySyncReason::TransitivePathDependency, 0),
910 DependencyRiskClass::Medium
911 );
912 }
913
914 #[test]
915 fn classify_sync_risk_transitive_with_one_dependent_is_medium() {
916 assert_eq!(
917 classify_sync_risk(DependencySyncReason::TransitivePathDependency, 1),
918 DependencyRiskClass::Medium
919 );
920 }
921
922 #[test]
923 fn classify_sync_risk_transitive_with_two_dependents_is_high() {
924 assert_eq!(
925 classify_sync_risk(DependencySyncReason::TransitivePathDependency, 2),
926 DependencyRiskClass::High
927 );
928 }
929
930 #[test]
931 fn classify_sync_risk_transitive_with_many_dependents_is_high() {
932 assert_eq!(
933 classify_sync_risk(DependencySyncReason::TransitivePathDependency, 100),
934 DependencyRiskClass::High
935 );
936 }
937
938 #[test]
943 fn wide_fanout_graph_syncs_all_leaves_before_root() {
944 let leaf_count = 20;
945 let mut packages = vec![package("/data/projects/hub", "hub", true)];
946 let mut edges = Vec::new();
947 for i in 0..leaf_count {
948 let root = format!("/data/projects/leaf_{i}");
949 let name = format!("leaf_{i}");
950 packages.push(package(&root, &name, false));
951 edges.push(edge("/data/projects/hub", &root, &name));
952 }
953
954 let graph = CargoPathDependencyGraph {
955 entry_manifest_path: PathBuf::from("/data/projects/hub/Cargo.toml"),
956 workspace_root: Some(PathBuf::from("/data/projects")),
957 root_packages: vec![PathBuf::from("/data/projects/hub")],
958 packages,
959 edges,
960 };
961
962 let plan = plan_dependency_closure_from_graph(&graph);
963 assert!(plan.is_ready());
964 assert_eq!(plan.sync_order.len(), leaf_count + 1);
965
966 let hub_action = plan.sync_order.last().unwrap();
968 assert_eq!(hub_action.package_root, PathBuf::from("/data/projects/hub"));
969 assert_eq!(hub_action.metadata.reason, DependencySyncReason::EntryPoint);
970
971 for action in &plan.sync_order[..leaf_count] {
973 assert_eq!(
974 action.metadata.reason,
975 DependencySyncReason::TransitivePathDependency
976 );
977 assert_eq!(action.metadata.dependent_roots.len(), 1);
978 assert_eq!(action.risk, DependencyRiskClass::Medium);
979 }
980 }
981
982 #[test]
987 fn deep_chain_graph_preserves_order() {
988 let depth = 10;
989 let mut packages = Vec::new();
990 let mut edges = Vec::new();
991 for i in 0..depth {
992 let root = format!("/data/projects/chain_{i}");
993 let name = format!("chain_{i}");
994 packages.push(package(&root, &name, i == depth - 1));
995 if i > 0 {
996 let parent = format!("/data/projects/chain_{}", i);
997 let child = format!("/data/projects/chain_{}", i - 1);
998 edges.push(edge(&parent, &child, &format!("chain_{}", i - 1)));
999 }
1000 }
1001
1002 let graph = CargoPathDependencyGraph {
1003 entry_manifest_path: PathBuf::from(format!(
1004 "/data/projects/chain_{}/Cargo.toml",
1005 depth - 1
1006 )),
1007 workspace_root: Some(PathBuf::from("/data/projects")),
1008 root_packages: vec![PathBuf::from(format!("/data/projects/chain_{}", depth - 1))],
1009 packages,
1010 edges,
1011 };
1012
1013 let plan = plan_dependency_closure_from_graph(&graph);
1014 assert!(plan.is_ready());
1015 assert_eq!(plan.sync_order.len(), depth);
1016
1017 assert_eq!(
1019 plan.sync_order[0].package_root,
1020 PathBuf::from("/data/projects/chain_0")
1021 );
1022 assert_eq!(
1023 plan.sync_order.last().unwrap().package_root,
1024 PathBuf::from(format!("/data/projects/chain_{}", depth - 1))
1025 );
1026 }
1027
1028 #[test]
1033 fn resolver_error_mapping_metadata_parse_failure() {
1034 let error = CargoPathDependencyError::new(
1035 CargoPathDependencyErrorKind::MetadataParseFailure,
1036 "cannot parse metadata JSON",
1037 );
1038 let issue = issue_from_resolver_error(&error);
1039 assert_eq!(issue.code, "metadata-parse-failure");
1040 assert_eq!(issue.risk, DependencyRiskClass::Critical);
1041 }
1042
1043 #[test]
1044 fn resolver_error_mapping_metadata_invocation_failure() {
1045 let error = CargoPathDependencyError::new(
1046 CargoPathDependencyErrorKind::MetadataInvocationFailure,
1047 "cargo metadata timed out after 30s",
1048 );
1049 let issue = issue_from_resolver_error(&error);
1050 assert_eq!(issue.code, "metadata-invocation-failure");
1051 assert_eq!(issue.risk, DependencyRiskClass::Critical);
1052 }
1053
1054 #[test]
1055 fn resolver_error_mapping_missing_path_dependency() {
1056 let error = CargoPathDependencyError::new(
1057 CargoPathDependencyErrorKind::MissingPathDependency,
1058 "path dep does not exist on disk",
1059 )
1060 .with_dependency_name("phantom_dep")
1061 .with_dependency_path("/data/projects/nonexistent");
1062
1063 let issue = issue_from_resolver_error(&error);
1064 assert_eq!(issue.code, "missing-path-dependency");
1065 assert_eq!(issue.risk, DependencyRiskClass::High);
1066 assert!(
1067 issue
1068 .diagnostics
1069 .iter()
1070 .any(|d| d.contains("dependency_name=phantom_dep")),
1071 "diagnostics should include dependency name"
1072 );
1073 assert!(
1074 issue
1075 .diagnostics
1076 .iter()
1077 .any(|d| d.contains("dependency_path=/data/projects/nonexistent")),
1078 "diagnostics should include dependency path"
1079 );
1080 }
1081
1082 #[test]
1083 fn resolver_error_mapping_cyclic_dependency() {
1084 let error = CargoPathDependencyError::new(
1085 CargoPathDependencyErrorKind::CyclicDependency,
1086 "circular path dependency detected",
1087 );
1088
1089 let issue = issue_from_resolver_error(&error);
1090 assert_eq!(issue.code, "cyclic-path-dependency");
1091 assert_eq!(issue.risk, DependencyRiskClass::Critical);
1092 assert!(
1093 issue.message.contains("cyclic path dependency"),
1094 "message should contain error kind, got: {}",
1095 issue.message
1096 );
1097 }
1098
1099 #[test]
1104 fn fail_open_plan_preserves_manifest_path_from_error() {
1105 let error = CargoPathDependencyError::new(
1106 CargoPathDependencyErrorKind::MetadataInvocationFailure,
1107 "timeout",
1108 )
1109 .with_manifest_path("/data/projects/app/Cargo.toml");
1110
1111 let plan =
1112 fail_open_plan_from_resolver_error(Path::new("/data/projects/app/Cargo.toml"), &error);
1113 assert_eq!(
1114 plan.entry_manifest_path,
1115 PathBuf::from("/data/projects/app/Cargo.toml")
1116 );
1117 assert_eq!(plan.state, DependencyClosurePlanState::FailOpen);
1118 assert!(plan.fail_open);
1119 assert!(plan.sync_order.is_empty());
1120 assert!(plan.canonical_roots.is_empty());
1121 assert!(plan.workspace_root.is_none());
1122 }
1123
1124 #[test]
1125 fn fail_open_plan_uses_entrypoint_when_error_has_no_manifest_path() {
1126 let error = CargoPathDependencyError::new(
1127 CargoPathDependencyErrorKind::MissingPathDependency,
1128 "dep not found",
1129 );
1130
1131 let plan = fail_open_plan_from_resolver_error(Path::new("/data/projects/fallback"), &error);
1132 assert_eq!(
1133 plan.entry_manifest_path,
1134 PathBuf::from("/data/projects/fallback")
1135 );
1136 }
1137
1138 #[test]
1143 fn is_ready_returns_false_for_fail_open() {
1144 let plan = DependencyClosurePlan {
1145 state: DependencyClosurePlanState::FailOpen,
1146 entry_manifest_path: PathBuf::from("/tmp/Cargo.toml"),
1147 workspace_root: None,
1148 canonical_roots: Vec::new(),
1149 sync_order: Vec::new(),
1150 fail_open: true,
1151 fail_open_reason: Some("test".to_string()),
1152 issues: Vec::new(),
1153 };
1154 assert!(!plan.is_ready());
1155 }
1156
1157 #[test]
1158 fn sync_roots_returns_roots_in_sync_order() {
1159 let plan = DependencyClosurePlan {
1160 state: DependencyClosurePlanState::Ready,
1161 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1162 workspace_root: None,
1163 canonical_roots: vec![
1164 PathBuf::from("/data/projects/dep"),
1165 PathBuf::from("/data/projects/app"),
1166 ],
1167 sync_order: vec![
1168 DependencySyncAction {
1169 order_index: 0,
1170 package_root: PathBuf::from("/data/projects/dep"),
1171 manifest_path: PathBuf::from("/data/projects/dep/Cargo.toml"),
1172 package_name: "dep".to_string(),
1173 risk: DependencyRiskClass::Medium,
1174 metadata: DependencySyncMetadata {
1175 reason: DependencySyncReason::TransitivePathDependency,
1176 workspace_member: false,
1177 root_package: false,
1178 inbound_dependency_names: vec!["dep".to_string()],
1179 dependent_roots: vec![PathBuf::from("/data/projects/app")],
1180 notes: vec!["dependent_root_count=1".to_string()],
1181 },
1182 },
1183 DependencySyncAction {
1184 order_index: 1,
1185 package_root: PathBuf::from("/data/projects/app"),
1186 manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1187 package_name: "app".to_string(),
1188 risk: DependencyRiskClass::Low,
1189 metadata: DependencySyncMetadata {
1190 reason: DependencySyncReason::EntryPoint,
1191 workspace_member: false,
1192 root_package: true,
1193 inbound_dependency_names: Vec::new(),
1194 dependent_roots: Vec::new(),
1195 notes: vec!["dependent_root_count=0".to_string()],
1196 },
1197 },
1198 ],
1199 fail_open: false,
1200 fail_open_reason: None,
1201 issues: Vec::new(),
1202 };
1203
1204 let roots = plan.sync_roots();
1205 assert_eq!(
1206 roots,
1207 vec![
1208 PathBuf::from("/data/projects/dep"),
1209 PathBuf::from("/data/projects/app"),
1210 ]
1211 );
1212 }
1213
1214 #[test]
1219 fn workspace_member_gets_workspace_member_reason() {
1220 let graph = CargoPathDependencyGraph {
1221 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1222 workspace_root: Some(PathBuf::from("/data/projects")),
1223 root_packages: vec![PathBuf::from("/data/projects/app")],
1224 packages: vec![
1225 package("/data/projects/app", "app", true),
1226 package("/data/projects/member", "member", true),
1227 ],
1228 edges: vec![edge(
1229 "/data/projects/app",
1230 "/data/projects/member",
1231 "member",
1232 )],
1233 };
1234
1235 let plan = plan_dependency_closure_from_graph(&graph);
1236 assert!(plan.is_ready());
1237
1238 let member_action = plan
1239 .sync_order
1240 .iter()
1241 .find(|a| a.package_name == "member")
1242 .unwrap();
1243 assert_eq!(
1244 member_action.metadata.reason,
1245 DependencySyncReason::WorkspaceMember
1246 );
1247 assert!(member_action.metadata.workspace_member);
1248 assert_eq!(member_action.risk, DependencyRiskClass::Low);
1249 }
1250
1251 #[test]
1256 fn inbound_dependency_names_are_deduplicated() {
1257 let graph = CargoPathDependencyGraph {
1258 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1259 workspace_root: None,
1260 root_packages: vec![PathBuf::from("/data/projects/app")],
1261 packages: vec![
1262 package("/data/projects/app", "app", false),
1263 package("/data/projects/dep", "dep", false),
1264 package("/data/projects/other", "other", false),
1265 ],
1266 edges: vec![
1267 edge("/data/projects/app", "/data/projects/dep", "dep"),
1268 edge("/data/projects/other", "/data/projects/dep", "dep"),
1269 ],
1270 };
1271
1272 let plan = plan_dependency_closure_from_graph(&graph);
1273 let dep_action = plan
1274 .sync_order
1275 .iter()
1276 .find(|a| a.package_name == "dep")
1277 .unwrap();
1278 assert_eq!(dep_action.metadata.inbound_dependency_names, vec!["dep"]);
1280 }
1281
1282 #[test]
1287 fn topological_sort_is_deterministic_across_calls() {
1288 let graph = CargoPathDependencyGraph {
1289 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1290 workspace_root: None,
1291 root_packages: vec![PathBuf::from("/data/projects/app")],
1292 packages: vec![
1293 package("/data/projects/app", "app", false),
1294 package("/data/projects/a", "a", false),
1295 package("/data/projects/b", "b", false),
1296 package("/data/projects/c", "c", false),
1297 ],
1298 edges: vec![
1299 edge("/data/projects/app", "/data/projects/a", "a"),
1300 edge("/data/projects/app", "/data/projects/b", "b"),
1301 edge("/data/projects/app", "/data/projects/c", "c"),
1302 ],
1303 };
1304
1305 let plan1 = plan_dependency_closure_from_graph(&graph);
1306 let plan2 = plan_dependency_closure_from_graph(&graph);
1307
1308 let roots1: Vec<_> = plan1.sync_order.iter().map(|a| &a.package_root).collect();
1309 let roots2: Vec<_> = plan2.sync_order.iter().map(|a| &a.package_root).collect();
1310 assert_eq!(roots1, roots2, "topological order must be deterministic");
1311 }
1312
1313 #[test]
1318 fn notes_include_dependent_root_count() {
1319 let graph = CargoPathDependencyGraph {
1320 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1321 workspace_root: None,
1322 root_packages: vec![PathBuf::from("/data/projects/app")],
1323 packages: vec![
1324 package("/data/projects/app", "app", false),
1325 package("/data/projects/dep", "dep", false),
1326 ],
1327 edges: vec![edge("/data/projects/app", "/data/projects/dep", "dep")],
1328 };
1329
1330 let plan = plan_dependency_closure_from_graph(&graph);
1331 let dep_action = plan
1332 .sync_order
1333 .iter()
1334 .find(|a| a.package_name == "dep")
1335 .unwrap();
1336 assert!(
1337 dep_action
1338 .metadata
1339 .notes
1340 .iter()
1341 .any(|n| n == "dependent_root_count=1"),
1342 "notes should include dependent_root_count"
1343 );
1344
1345 let app_action = plan
1346 .sync_order
1347 .iter()
1348 .find(|a| a.package_name == "app")
1349 .unwrap();
1350 assert!(
1351 app_action
1352 .metadata
1353 .notes
1354 .iter()
1355 .any(|n| n == "dependent_root_count=0"),
1356 "entry point should have 0 dependent roots"
1357 );
1358 }
1359
1360 #[test]
1365 fn plan_serialization_round_trip() {
1366 let graph = CargoPathDependencyGraph {
1367 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1368 workspace_root: Some(PathBuf::from("/data/projects")),
1369 root_packages: vec![PathBuf::from("/data/projects/app")],
1370 packages: vec![
1371 package("/data/projects/app", "app", true),
1372 package("/data/projects/dep", "dep", false),
1373 ],
1374 edges: vec![edge("/data/projects/app", "/data/projects/dep", "dep")],
1375 };
1376
1377 let plan = plan_dependency_closure_from_graph(&graph);
1378 let json = serde_json::to_string(&plan).expect("plan should serialize");
1379 let deserialized: DependencyClosurePlan =
1380 serde_json::from_str(&json).expect("plan should deserialize");
1381 assert_eq!(plan, deserialized);
1382 }
1383
1384 #[test]
1385 fn fail_open_plan_serialization_round_trip() {
1386 let error = CargoPathDependencyError::new(
1387 CargoPathDependencyErrorKind::CyclicDependency,
1388 "cycle detected",
1389 );
1390
1391 let plan = fail_open_plan_from_resolver_error(Path::new("/data/projects/app"), &error);
1392 let json = serde_json::to_string(&plan).expect("fail-open plan should serialize");
1393 let deserialized: DependencyClosurePlan =
1394 serde_json::from_str(&json).expect("fail-open plan should deserialize");
1395 assert_eq!(plan, deserialized);
1396 }
1397
1398 #[test]
1403 fn root_package_flag_set_correctly() {
1404 let graph = CargoPathDependencyGraph {
1405 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1406 workspace_root: None,
1407 root_packages: vec![PathBuf::from("/data/projects/app")],
1408 packages: vec![
1409 package("/data/projects/app", "app", false),
1410 package("/data/projects/dep", "dep", false),
1411 ],
1412 edges: vec![edge("/data/projects/app", "/data/projects/dep", "dep")],
1413 };
1414
1415 let plan = plan_dependency_closure_from_graph(&graph);
1416 let app_action = plan
1417 .sync_order
1418 .iter()
1419 .find(|a| a.package_name == "app")
1420 .unwrap();
1421 assert!(
1422 app_action.metadata.root_package,
1423 "app should be marked as root_package"
1424 );
1425
1426 let dep_action = plan
1427 .sync_order
1428 .iter()
1429 .find(|a| a.package_name == "dep")
1430 .unwrap();
1431 assert!(
1432 !dep_action.metadata.root_package,
1433 "dep should not be marked as root_package"
1434 );
1435 }
1436
1437 #[test]
1442 fn order_indices_are_sequential_from_zero() {
1443 let graph = CargoPathDependencyGraph {
1444 entry_manifest_path: PathBuf::from("/data/projects/app/Cargo.toml"),
1445 workspace_root: None,
1446 root_packages: vec![PathBuf::from("/data/projects/app")],
1447 packages: vec![
1448 package("/data/projects/app", "app", false),
1449 package("/data/projects/a", "a", false),
1450 package("/data/projects/b", "b", false),
1451 ],
1452 edges: vec![
1453 edge("/data/projects/app", "/data/projects/a", "a"),
1454 edge("/data/projects/a", "/data/projects/b", "b"),
1455 ],
1456 };
1457
1458 let plan = plan_dependency_closure_from_graph(&graph);
1459 for (i, action) in plan.sync_order.iter().enumerate() {
1460 assert_eq!(action.order_index, i, "order_index should be sequential");
1461 }
1462 }
1463}