1#![warn(missing_docs)]
2use anyhow::{Context, Result};
9use shiplog_bundle::{DIR_PROFILES, FILE_PACKET_MD, RunArtifactPaths, zip_path_for_profile};
10use shiplog_bundle::{write_bundle_manifest, write_zip};
11pub use shiplog_merge::ConflictResolution;
12use shiplog_ports::{IngestOutput, Redactor, Renderer, WorkstreamClusterer};
13use shiplog_render_json::{write_coverage_manifest, write_events_jsonl};
14use shiplog_schema::bundle::BundleProfile;
15use shiplog_schema::coverage::CoverageManifest;
16use shiplog_schema::event::EventEnvelope;
17use shiplog_schema::workstream::WorkstreamsFile;
18use shiplog_workstreams::WorkstreamManager;
19use std::path::{Path, PathBuf};
20
21pub struct Engine<'a> {
27 pub renderer: &'a dyn Renderer,
29 pub clusterer: &'a dyn WorkstreamClusterer,
31 pub redactor: &'a dyn Redactor,
33 pub render_profiles: bool,
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct RunOutputs {
40 pub out_dir: PathBuf,
42 pub packet_md: PathBuf,
44 pub workstreams_yaml: PathBuf,
46 pub ledger_events_jsonl: PathBuf,
48 pub coverage_manifest_json: PathBuf,
50 pub bundle_manifest_json: PathBuf,
52 pub zip_path: Option<PathBuf>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub enum WorkstreamSource {
59 Curated,
61 Suggested,
63 Generated,
65}
66
67impl std::fmt::Display for WorkstreamSource {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Curated => f.write_str("Curated"),
71 Self::Suggested => f.write_str("Suggested"),
72 Self::Generated => f.write_str("Generated"),
73 }
74 }
75}
76
77fn ensure_bundle_profile_available(
78 bundle_profile: &BundleProfile,
79 render_profiles: bool,
80) -> Result<()> {
81 if !render_profiles && !matches!(bundle_profile, BundleProfile::Internal) {
82 core::hint::cold_path();
83 anyhow::bail!(
84 "{} bundle profile requires manager/public profile rendering",
85 bundle_profile
86 );
87 }
88 Ok(())
89}
90
91impl<'a> Engine<'a> {
92 pub fn new(
108 renderer: &'a dyn Renderer,
109 clusterer: &'a dyn WorkstreamClusterer,
110 redactor: &'a dyn Redactor,
111 ) -> Self {
112 Self {
113 renderer,
114 clusterer,
115 redactor,
116 render_profiles: true,
117 }
118 }
119
120 pub fn with_profile_rendering(mut self, render_profiles: bool) -> Self {
125 self.render_profiles = render_profiles;
126 self
127 }
128
129 pub fn run(
161 &self,
162 ingest: IngestOutput,
163 user: &str,
164 window_label: &str,
165 out_dir: &Path,
166 zip: bool,
167 bundle_profile: &BundleProfile,
168 ) -> Result<(RunOutputs, WorkstreamSource)> {
169 self.run_with_profile_rendering(
170 ingest,
171 user,
172 window_label,
173 out_dir,
174 zip,
175 bundle_profile,
176 self.render_profiles,
177 )
178 }
179
180 pub fn run_with_profile_rendering(
186 &self,
187 ingest: IngestOutput,
188 user: &str,
189 window_label: &str,
190 out_dir: &Path,
191 zip: bool,
192 bundle_profile: &BundleProfile,
193 render_profiles: bool,
194 ) -> Result<(RunOutputs, WorkstreamSource)> {
195 ensure_bundle_profile_available(bundle_profile, render_profiles)?;
196 std::fs::create_dir_all(out_dir).with_context(|| format!("create {out_dir:?}"))?;
197
198 let events = ingest.events;
199 let coverage = ingest.coverage;
200 let paths = RunArtifactPaths::new(out_dir);
201
202 let (workstreams, ws_source) = self
204 .load_workstreams(out_dir, &events)
205 .context("load workstreams")?;
206
207 let ledger_path = paths.ledger_events();
209 let coverage_path = paths.coverage_manifest();
210 let packet_path = paths.packet_md();
211
212 write_events_jsonl(&ledger_path, &events)
213 .with_context(|| format!("write event ledger to {ledger_path:?}"))?;
214 write_coverage_manifest(&coverage_path, &coverage)
215 .with_context(|| format!("write coverage manifest to {coverage_path:?}"))?;
216 let ws_path = match ws_source {
219 WorkstreamSource::Curated => WorkstreamManager::curated_path(out_dir),
220 WorkstreamSource::Suggested => WorkstreamManager::suggested_path(out_dir),
221 WorkstreamSource::Generated => WorkstreamManager::suggested_path(out_dir),
222 };
223
224 let packet = self
225 .renderer
226 .render_packet_markdown(user, window_label, &events, &workstreams, &coverage)
227 .context("render packet markdown")?;
228 std::fs::write(&packet_path, &packet)
229 .with_context(|| format!("write packet to {packet_path:?}"))?;
230
231 self.render_profiles_if_requested(
232 render_profiles,
233 user,
234 window_label,
235 out_dir,
236 &events,
237 &workstreams,
238 &coverage,
239 )?;
240
241 let run_id = &coverage.run_id;
243 let _bundle = write_bundle_manifest(out_dir, run_id, bundle_profile)
244 .context("write bundle manifest")?;
245 let zip_path = if zip {
246 let z = zip_path_for_profile(out_dir, bundle_profile.as_str());
247 write_zip(out_dir, &z, bundle_profile).context("write zip archive")?;
248 Some(z)
249 } else {
250 None
251 };
252
253 Ok((
254 RunOutputs {
255 out_dir: out_dir.to_path_buf(),
256 packet_md: packet_path,
257 workstreams_yaml: ws_path,
258 ledger_events_jsonl: ledger_path,
259 coverage_manifest_json: coverage_path,
260 bundle_manifest_json: paths.bundle_manifest(),
261 zip_path,
262 },
263 ws_source,
264 ))
265 }
266
267 fn load_workstreams(
269 &self,
270 out_dir: &Path,
271 events: &[EventEnvelope],
272 ) -> Result<(WorkstreamsFile, WorkstreamSource)> {
273 let curated_exists = WorkstreamManager::has_curated(out_dir);
274 let suggested_exists = WorkstreamManager::suggested_path(out_dir).exists();
275
276 let ws = WorkstreamManager::load_effective(out_dir, self.clusterer, events)
277 .context("load effective workstreams")?;
278
279 let source = if curated_exists {
280 WorkstreamSource::Curated
281 } else if suggested_exists {
282 WorkstreamSource::Suggested
283 } else {
284 WorkstreamSource::Generated
285 };
286
287 Ok((ws, source))
288 }
289
290 pub fn import(
323 &self,
324 ingest: IngestOutput,
325 user: &str,
326 window_label: &str,
327 out_dir: &Path,
328 zip: bool,
329 workstreams: Option<WorkstreamsFile>,
330 bundle_profile: &BundleProfile,
331 ) -> Result<(RunOutputs, WorkstreamSource)> {
332 self.import_with_profile_rendering(
333 ingest,
334 user,
335 window_label,
336 out_dir,
337 zip,
338 workstreams,
339 bundle_profile,
340 self.render_profiles,
341 )
342 }
343
344 #[expect(clippy::too_many_arguments, reason = "policy:clippy-0001")]
350 pub fn import_with_profile_rendering(
351 &self,
352 ingest: IngestOutput,
353 user: &str,
354 window_label: &str,
355 out_dir: &Path,
356 zip: bool,
357 workstreams: Option<WorkstreamsFile>,
358 bundle_profile: &BundleProfile,
359 render_profiles: bool,
360 ) -> Result<(RunOutputs, WorkstreamSource)> {
361 ensure_bundle_profile_available(bundle_profile, render_profiles)?;
362 std::fs::create_dir_all(out_dir).with_context(|| format!("create {out_dir:?}"))?;
363
364 let events = ingest.events;
365 let coverage = ingest.coverage;
366 let paths = RunArtifactPaths::new(out_dir);
367
368 let (ws, ws_source) = if let Some(ws) = workstreams {
370 let curated_path = WorkstreamManager::curated_path(out_dir);
372 shiplog_workstreams::write_workstreams(&curated_path, &ws)
373 .with_context(|| format!("write curated workstreams to {curated_path:?}"))?;
374 (ws, WorkstreamSource::Curated)
375 } else {
376 self.load_workstreams(out_dir, &events)
377 .context("load workstreams")?
378 };
379
380 let ledger_path = paths.ledger_events();
382 let coverage_path = paths.coverage_manifest();
383 let packet_path = paths.packet_md();
384
385 write_events_jsonl(&ledger_path, &events)
386 .with_context(|| format!("write event ledger to {ledger_path:?}"))?;
387 write_coverage_manifest(&coverage_path, &coverage)
388 .with_context(|| format!("write coverage manifest to {coverage_path:?}"))?;
389
390 let ws_path = match ws_source {
391 WorkstreamSource::Curated => WorkstreamManager::curated_path(out_dir),
392 WorkstreamSource::Suggested => WorkstreamManager::suggested_path(out_dir),
393 WorkstreamSource::Generated => WorkstreamManager::suggested_path(out_dir),
394 };
395
396 let packet = self
397 .renderer
398 .render_packet_markdown(user, window_label, &events, &ws, &coverage)
399 .context("render packet markdown")?;
400 std::fs::write(&packet_path, &packet)
401 .with_context(|| format!("write packet to {packet_path:?}"))?;
402
403 self.render_profiles_if_requested(
404 render_profiles,
405 user,
406 window_label,
407 out_dir,
408 &events,
409 &ws,
410 &coverage,
411 )?;
412
413 let run_id = &coverage.run_id;
415 let _bundle = write_bundle_manifest(out_dir, run_id, bundle_profile)
416 .context("write bundle manifest")?;
417 let zip_path = if zip {
418 let z = zip_path_for_profile(out_dir, bundle_profile.as_str());
419 write_zip(out_dir, &z, bundle_profile).context("write zip archive")?;
420 Some(z)
421 } else {
422 None
423 };
424
425 Ok((
426 RunOutputs {
427 out_dir: out_dir.to_path_buf(),
428 packet_md: packet_path,
429 workstreams_yaml: ws_path,
430 ledger_events_jsonl: ledger_path,
431 coverage_manifest_json: coverage_path,
432 bundle_manifest_json: paths.bundle_manifest(),
433 zip_path,
434 },
435 ws_source,
436 ))
437 }
438
439 pub fn refresh(
471 &self,
472 ingest: IngestOutput,
473 user: &str,
474 window_label: &str,
475 out_dir: &Path,
476 zip: bool,
477 bundle_profile: &BundleProfile,
478 ) -> Result<RunOutputs> {
479 self.refresh_with_profile_rendering(
480 ingest,
481 user,
482 window_label,
483 out_dir,
484 zip,
485 bundle_profile,
486 self.render_profiles,
487 )
488 }
489
490 pub fn refresh_with_profile_rendering(
496 &self,
497 ingest: IngestOutput,
498 user: &str,
499 window_label: &str,
500 out_dir: &Path,
501 zip: bool,
502 bundle_profile: &BundleProfile,
503 render_profiles: bool,
504 ) -> Result<RunOutputs> {
505 ensure_bundle_profile_available(bundle_profile, render_profiles)?;
506 std::fs::create_dir_all(out_dir).with_context(|| format!("create {out_dir:?}"))?;
507
508 let events = ingest.events;
509 let coverage = ingest.coverage;
510 let paths = RunArtifactPaths::new(out_dir);
511
512 let workstreams = if WorkstreamManager::has_curated(out_dir) {
514 let path = WorkstreamManager::curated_path(out_dir);
515 let text = std::fs::read_to_string(&path)
516 .with_context(|| format!("read curated workstreams from {path:?}"))?;
517 serde_yaml::from_str(&text)
518 .with_context(|| format!("parse curated workstreams yaml {path:?}"))?
519 } else {
520 let suggested_path = WorkstreamManager::suggested_path(out_dir);
521 if suggested_path.exists() {
522 let text = std::fs::read_to_string(&suggested_path).with_context(|| {
523 format!("read suggested workstreams from {suggested_path:?}")
524 })?;
525 serde_yaml::from_str(&text).with_context(|| {
526 format!("parse suggested workstreams yaml {suggested_path:?}")
527 })?
528 } else {
529 anyhow::bail!(
530 "No workstreams found. Run `shiplog collect` first to generate workstreams."
531 );
532 }
533 };
534
535 let ledger_path = paths.ledger_events();
537 let coverage_path = paths.coverage_manifest();
538 let packet_path = paths.packet_md();
539
540 write_events_jsonl(&ledger_path, &events)
541 .with_context(|| format!("write event ledger to {ledger_path:?}"))?;
542 write_coverage_manifest(&coverage_path, &coverage)
543 .with_context(|| format!("write coverage manifest to {coverage_path:?}"))?;
544
545 let ws_path = if WorkstreamManager::has_curated(out_dir) {
546 WorkstreamManager::curated_path(out_dir)
547 } else {
548 WorkstreamManager::suggested_path(out_dir)
549 };
550
551 let packet = self
552 .renderer
553 .render_packet_markdown(user, window_label, &events, &workstreams, &coverage)
554 .context("render packet markdown")?;
555 std::fs::write(&packet_path, &packet)
556 .with_context(|| format!("write packet to {packet_path:?}"))?;
557
558 self.render_profiles_if_requested(
559 render_profiles,
560 user,
561 window_label,
562 out_dir,
563 &events,
564 &workstreams,
565 &coverage,
566 )?;
567
568 let run_id = &coverage.run_id;
570 let _bundle = write_bundle_manifest(out_dir, run_id, bundle_profile)
571 .context("write bundle manifest")?;
572 let zip_path = if zip {
573 let z = zip_path_for_profile(out_dir, bundle_profile.as_str());
574 write_zip(out_dir, &z, bundle_profile).context("write zip archive")?;
575 Some(z)
576 } else {
577 None
578 };
579
580 Ok(RunOutputs {
581 out_dir: out_dir.to_path_buf(),
582 packet_md: packet_path,
583 workstreams_yaml: ws_path,
584 ledger_events_jsonl: ledger_path,
585 coverage_manifest_json: coverage_path,
586 bundle_manifest_json: paths.bundle_manifest(),
587 zip_path,
588 })
589 }
590
591 fn render_profiles_if_requested(
592 &self,
593 render_profiles: bool,
594 user: &str,
595 window_label: &str,
596 out_dir: &Path,
597 events: &[EventEnvelope],
598 workstreams: &WorkstreamsFile,
599 coverage: &CoverageManifest,
600 ) -> Result<()> {
601 if !render_profiles {
602 return Ok(());
603 }
604
605 self.render_profile(
606 "manager",
607 user,
608 window_label,
609 out_dir,
610 events,
611 workstreams,
612 coverage,
613 )
614 .context("render manager profile")?;
615 self.render_profile(
616 "public",
617 user,
618 window_label,
619 out_dir,
620 events,
621 workstreams,
622 coverage,
623 )
624 .context("render public profile")?;
625 Ok(())
626 }
627
628 fn render_profile(
629 &self,
630 profile: &str,
631 user: &str,
632 window_label: &str,
633 out_dir: &Path,
634 events: &[EventEnvelope],
635 workstreams: &WorkstreamsFile,
636 coverage: &CoverageManifest,
637 ) -> Result<()> {
638 let prof_dir = out_dir.join(DIR_PROFILES).join(profile);
639 std::fs::create_dir_all(&prof_dir)
640 .with_context(|| format!("create profile directory {prof_dir:?}"))?;
641
642 let red_events = self
643 .redactor
644 .redact_events(events, profile)
645 .with_context(|| format!("redact events for {profile} profile"))?;
646 let red_ws = self
647 .redactor
648 .redact_workstreams(workstreams, profile)
649 .with_context(|| format!("redact workstreams for {profile} profile"))?;
650
651 let md = self
652 .renderer
653 .render_packet_markdown(user, window_label, &red_events, &red_ws, coverage)
654 .with_context(|| format!("render {profile} packet markdown"))?;
655 std::fs::write(prof_dir.join(FILE_PACKET_MD), &md)
656 .with_context(|| format!("write {profile} packet to {prof_dir:?}"))?;
657 Ok(())
658 }
659
660 pub fn merge(
691 &self,
692 ingest_outputs: Vec<IngestOutput>,
693 resolution: ConflictResolution,
694 ) -> Result<IngestOutput> {
695 #[cfg(feature = "merge-pipeline")]
696 {
697 let merged = shiplog_merge::merge_ingest_outputs(&ingest_outputs, resolution)
698 .context("merge ingest outputs")?;
699 Ok(merged.ingest_output)
700 }
701
702 #[cfg(not(feature = "merge-pipeline"))]
703 {
704 shiplog_merge::merge_ingest_outputs_legacy(&ingest_outputs, resolution)
705 }
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use chrono::{NaiveDate, TimeZone, Utc};
713 use shiplog_bundle::{PROFILE_MANAGER, PROFILE_PUBLIC};
714 use shiplog_ids::{EventId, RunId};
715 use shiplog_ports::IngestOutput;
716 use shiplog_schema::coverage::{Completeness, CoverageManifest, TimeWindow};
717 use shiplog_schema::event::*;
718 use shiplog_workstreams::RepoClusterer;
719
720 fn pr_event(repo: &str, number: u64, title: &str) -> EventEnvelope {
721 EventEnvelope {
722 id: EventId::from_parts(["github", "pr", repo, &number.to_string()]),
723 kind: EventKind::PullRequest,
724 occurred_at: Utc.timestamp_opt(0, 0).unwrap(),
725 actor: Actor {
726 login: "user".into(),
727 id: None,
728 },
729 repo: RepoRef {
730 full_name: repo.to_string(),
731 html_url: Some(format!("https://github.com/{repo}")),
732 visibility: RepoVisibility::Unknown,
733 },
734 payload: EventPayload::PullRequest(PullRequestEvent {
735 number,
736 title: title.to_string(),
737 state: PullRequestState::Merged,
738 created_at: Utc.timestamp_opt(0, 0).unwrap(),
739 merged_at: Some(Utc.timestamp_opt(0, 0).unwrap()),
740 additions: Some(1),
741 deletions: Some(0),
742 changed_files: Some(1),
743 touched_paths_hint: vec![],
744 window: Some(TimeWindow {
745 since: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
746 until: NaiveDate::from_ymd_opt(2025, 2, 1).unwrap(),
747 }),
748 }),
749 tags: vec![],
750 links: vec![Link {
751 label: "pr".into(),
752 url: format!("https://github.com/{repo}/pull/{number}"),
753 }],
754 source: SourceRef {
755 system: SourceSystem::Github,
756 url: Some("https://api.github.com/...".into()),
757 opaque_id: None,
758 },
759 }
760 }
761
762 fn test_ingest() -> IngestOutput {
763 let events = vec![
764 pr_event("acme/foo", 1, "Add feature"),
765 pr_event("acme/foo", 2, "Fix bug"),
766 ];
767 let coverage = CoverageManifest {
768 run_id: RunId("test_run_1".into()),
769 generated_at: Utc.timestamp_opt(0, 0).unwrap(),
770 user: "tester".into(),
771 window: TimeWindow {
772 since: NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
773 until: NaiveDate::from_ymd_opt(2025, 2, 1).unwrap(),
774 },
775 mode: "merged".into(),
776 sources: vec!["github".into()],
777 slices: vec![],
778 warnings: vec![],
779 completeness: Completeness::Complete,
780 };
781 IngestOutput {
782 events,
783 coverage,
784 freshness: Vec::new(),
785 }
786 }
787
788 fn test_engine() -> Engine<'static> {
789 let renderer: &'static dyn shiplog_ports::Renderer =
790 Box::leak(Box::new(shiplog_render_md::MarkdownRenderer::default()));
791 let clusterer: &'static dyn shiplog_ports::WorkstreamClusterer =
792 Box::leak(Box::new(RepoClusterer));
793 let redactor: &'static dyn shiplog_ports::Redactor = Box::leak(Box::new(
794 shiplog_redact::DeterministicRedactor::new(b"test-key"),
795 ));
796 Engine::new(renderer, clusterer, redactor)
797 }
798
799 #[test]
800 fn run_creates_expected_output_files() {
801 let dir = tempfile::tempdir().unwrap();
802 let out_dir = dir.path().join("test_run_1");
803
804 let engine = test_engine();
805 let ingest = test_ingest();
806
807 let (outputs, _) = engine
808 .run(
809 ingest,
810 "tester",
811 "2025-01-01..2025-02-01",
812 &out_dir,
813 false,
814 &BundleProfile::Internal,
815 )
816 .unwrap();
817
818 assert!(outputs.packet_md.exists(), "packet.md missing");
819 assert!(
820 outputs.ledger_events_jsonl.exists(),
821 "ledger.events.jsonl missing"
822 );
823 assert!(
824 outputs.coverage_manifest_json.exists(),
825 "coverage.manifest.json missing"
826 );
827 assert!(
828 outputs.bundle_manifest_json.exists(),
829 "bundle.manifest.json missing"
830 );
831 assert!(
832 out_dir
833 .join(DIR_PROFILES)
834 .join(PROFILE_MANAGER)
835 .join(FILE_PACKET_MD)
836 .exists(),
837 "manager profile missing"
838 );
839 assert!(
840 out_dir
841 .join(DIR_PROFILES)
842 .join(PROFILE_PUBLIC)
843 .join(FILE_PACKET_MD)
844 .exists(),
845 "public profile missing"
846 );
847 }
848
849 #[test]
850 fn run_with_zip_creates_archive() {
851 let dir = tempfile::tempdir().unwrap();
852 let out_dir = dir.path().join("test_run_zip");
853
854 let engine = test_engine();
855 let ingest = test_ingest();
856
857 let (outputs, _) = engine
858 .run(
859 ingest,
860 "tester",
861 "2025-01-01..2025-02-01",
862 &out_dir,
863 true,
864 &BundleProfile::Internal,
865 )
866 .unwrap();
867
868 assert!(
869 outputs.zip_path.is_some(),
870 "zip_path should be Some when zip=true"
871 );
872 assert!(
873 outputs.zip_path.as_ref().unwrap().exists(),
874 "zip file missing"
875 );
876 }
877
878 #[test]
879 fn run_with_profile_rendering_disabled_skips_share_profiles() {
880 let dir = tempfile::tempdir().unwrap();
881 let out_dir = dir.path().join("test_run_internal_only");
882
883 let engine = test_engine().with_profile_rendering(false);
884 let ingest = test_ingest();
885
886 engine
887 .run(
888 ingest,
889 "tester",
890 "2025-01-01..2025-02-01",
891 &out_dir,
892 false,
893 &BundleProfile::Internal,
894 )
895 .unwrap();
896
897 assert!(
898 !out_dir
899 .join(DIR_PROFILES)
900 .join(PROFILE_MANAGER)
901 .join(FILE_PACKET_MD)
902 .exists(),
903 "manager profile should not be written"
904 );
905 assert!(
906 !out_dir
907 .join(DIR_PROFILES)
908 .join(PROFILE_PUBLIC)
909 .join(FILE_PACKET_MD)
910 .exists(),
911 "public profile should not be written"
912 );
913 }
914
915 #[test]
916 fn manager_bundle_requires_profile_rendering() {
917 let dir = tempfile::tempdir().unwrap();
918 let out_dir = dir.path().join("test_run_manager_without_profiles");
919
920 let engine = test_engine().with_profile_rendering(false);
921 let ingest = test_ingest();
922
923 let err = engine
924 .run(
925 ingest,
926 "tester",
927 "2025-01-01..2025-02-01",
928 &out_dir,
929 false,
930 &BundleProfile::Manager,
931 )
932 .unwrap_err();
933
934 assert!(
935 format!("{err:#}")
936 .contains("manager bundle profile requires manager/public profile rendering"),
937 "unexpected error: {err:#}"
938 );
939 }
940
941 #[test]
942 fn zip_path_internal_uses_plain_extension() {
943 let p = zip_path_for_profile(Path::new("/tmp/run_123"), "internal");
944 assert_eq!(p, Path::new("/tmp/run_123.zip"));
945 }
946
947 #[test]
948 fn zip_path_manager_includes_profile_name() {
949 let p = zip_path_for_profile(Path::new("/tmp/run_123"), "manager");
950 assert_eq!(p, Path::new("/tmp/run_123.manager.zip"));
951 }
952}