1pub mod catalog;
21
22use std::collections::BTreeMap;
23
24use camino::Utf8Path;
25use serde::{Deserialize, Serialize};
26
27use crate::diagnostic::{Diagnostic, Reason};
28use crate::error::RkError;
29use crate::landing::manifest::{self, CheckoutMode, Provider, Style};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum ReleaseMode {
35 Automatic,
38 External,
41 None,
43}
44
45impl ReleaseMode {
46 #[must_use]
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::Automatic => "automatic",
51 Self::External => "external",
52 Self::None => "none",
53 }
54 }
55
56 pub fn parse(raw: &str) -> Result<Self, RkError> {
62 match raw {
63 "automatic" => Ok(Self::Automatic),
64 "external" => Ok(Self::External),
65 "none" => Ok(Self::None),
66 other => Err(RkError::Usage(format!(
67 "unknown release mode '{other}'; the modes are: automatic, external, none"
68 ))),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ReleaseIntent {
79 pub mode: ReleaseMode,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub driver: Option<String>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub style: Option<Style>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub line_prefix: Option<String>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct ProfileSnapshot {
97 pub technologies: Vec<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub forge: Option<String>,
102 pub release: ReleaseIntent,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct GitWorkflow {
109 pub trunk: String,
111 pub checkout_mode: CheckoutMode,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct CapabilityRequests {
118 #[serde(default)]
120 pub nix_packaging: bool,
121 #[serde(default)]
123 pub reporting_policy: bool,
124 #[serde(default)]
126 pub scorecard: bool,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub code_scanning: Option<Provider>,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
134#[serde(rename_all = "lowercase")]
135pub enum Source {
136 Flag,
138 Config,
140 Record,
142 Observation,
144 Default,
146}
147
148impl Source {
149 #[must_use]
155 pub const fn rank(self) -> u8 {
156 match self {
157 Self::Flag => 0,
158 Self::Config => 1,
159 Self::Record => 2,
160 Self::Observation => 3,
161 Self::Default => 4,
162 }
163 }
164
165 #[must_use]
167 pub const fn as_str(self) -> &'static str {
168 match self {
169 Self::Flag => "flag",
170 Self::Config => "config",
171 Self::Record => "record",
172 Self::Observation => "observation",
173 Self::Default => "default",
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
181#[serde(rename_all = "kebab-case", tag = "state")]
182pub enum Proposal {
183 None,
185 Automatic {
187 driver: String,
189 },
190 Ambiguous {
193 drivers: Vec<String>,
195 },
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct Params {
204 profile: ProfileSnapshot,
205 git: GitWorkflow,
206 capabilities: CapabilityRequests,
207 repo: String,
208 security_contact: String,
209 security_response: String,
210}
211
212#[derive(Default)]
214pub struct Inputs<'a> {
215 pub technologies: &'a [String],
218 pub forge: Option<&'a str>,
220 pub repo: Option<&'a str>,
222 pub release_mode: Option<ReleaseMode>,
224 pub release_driver: Option<&'a str>,
226 pub style: Option<Style>,
228 pub trunk: Option<&'a str>,
230 pub checkout_mode: Option<CheckoutMode>,
232 pub nix: Option<bool>,
234 pub reporting_policy: Option<bool>,
236 pub scorecard: Option<bool>,
238 pub code_scanning: Option<Option<Provider>>,
241}
242
243#[derive(Clone, Copy, PartialEq, Eq)]
245pub enum Purpose {
246 Init,
248 Preview,
251 Upgrade,
253 Adopt,
255}
256
257#[derive(Debug, Clone)]
259pub struct Resolved {
260 pub params: Params,
262 pub sources: BTreeMap<&'static str, Source>,
264 pub unknown: Vec<String>,
266 pub proposal: Option<Proposal>,
269}
270
271pub fn canonical_category(raw: &str) -> Result<String, String> {
281 let lowered = raw.trim().to_ascii_lowercase();
282 let shaped = lowered
283 .chars()
284 .next()
285 .is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
286 && lowered
287 .chars()
288 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
289 if shaped {
290 Ok(lowered)
291 } else {
292 Err(format!(
293 "{raw:?} is not a category name; one is lowercase letters and digits with hyphens inside, as [a-z0-9][a-z0-9-]*"
294 ))
295 }
296}
297
298pub fn canonical_list(key: &str, raw: &[String]) -> Result<Vec<String>, String> {
304 let mut out = Vec::with_capacity(raw.len());
305 for value in raw {
306 let name = canonical_category(value).map_err(|reason| format!("{key}: {reason}"))?;
307 if out.contains(&name) {
308 return Err(format!("{key} names {name} twice; each technology once"));
309 }
310 out.push(name);
311 }
312 out.sort();
313 Ok(out)
314}
315
316impl Params {
317 #[must_use]
320 pub fn from_record(record: &manifest::Manifest) -> Self {
321 Self {
322 profile: record.profile.clone(),
323 git: record.git.clone(),
324 capabilities: record.capabilities.clone(),
325 repo: record.parameters.repo.clone(),
326 security_contact: record.parameters.security_contact.clone(),
327 security_response: record.parameters.security_response.clone(),
328 }
329 }
330
331 pub fn resolve(
339 target: &Utf8Path,
340 flags: &Inputs<'_>,
341 config: Option<&crate::config::Config>,
342 record: Option<&manifest::Manifest>,
343 purpose: Purpose,
344 ) -> Result<Self, RkError> {
345 resolve(target, flags, config, record, purpose).map(|resolved| resolved.params)
346 }
347
348 #[must_use]
350 pub const fn profile(&self) -> &ProfileSnapshot {
351 &self.profile
352 }
353
354 #[must_use]
356 pub const fn git(&self) -> &GitWorkflow {
357 &self.git
358 }
359
360 #[must_use]
362 pub const fn capabilities(&self) -> &CapabilityRequests {
363 &self.capabilities
364 }
365
366 #[must_use]
368 pub fn technologies(&self) -> &[String] {
369 &self.profile.technologies
370 }
371
372 #[must_use]
374 pub fn forge(&self) -> Option<&str> {
375 self.profile.forge.as_deref()
376 }
377
378 #[must_use]
380 pub const fn release_mode(&self) -> ReleaseMode {
381 self.profile.release.mode
382 }
383
384 #[must_use]
386 pub fn driver(&self) -> Option<&str> {
387 self.profile.release.driver.as_deref()
388 }
389
390 #[must_use]
392 pub const fn style(&self) -> Option<Style> {
393 self.profile.release.style
394 }
395
396 #[must_use]
398 pub const fn nix_packaging(&self) -> bool {
399 self.capabilities.nix_packaging
400 }
401
402 #[must_use]
404 pub const fn reporting_policy(&self) -> bool {
405 self.capabilities.reporting_policy
406 }
407
408 #[must_use]
410 pub const fn scorecard(&self) -> bool {
411 self.capabilities.scorecard
412 }
413
414 #[must_use]
416 pub const fn code_scanning(&self) -> Option<Provider> {
417 self.capabilities.code_scanning
418 }
419
420 #[must_use]
423 pub fn repo(&self) -> &str {
424 &self.repo
425 }
426
427 #[must_use]
429 pub const fn checkout_mode(&self) -> CheckoutMode {
430 self.git.checkout_mode
431 }
432
433 #[must_use]
435 pub fn trunk(&self) -> &str {
436 &self.git.trunk
437 }
438
439 #[must_use]
442 pub fn line_prefix(&self) -> &str {
443 self.profile
444 .release
445 .line_prefix
446 .as_deref()
447 .unwrap_or(crate::config::LINE_PREFIX_DEFAULT)
448 }
449
450 #[must_use]
453 pub fn security_contact(&self) -> &str {
454 &self.security_contact
455 }
456
457 #[must_use]
459 pub fn security_response(&self) -> &str {
460 &self.security_response
461 }
462
463 #[must_use]
467 pub fn canonical_flags(&self) -> String {
468 let mut out = String::new();
469 for technology in &self.profile.technologies {
470 out.push_str(" --technology ");
471 out.push_str(technology);
472 }
473 if let Some(forge) = &self.profile.forge {
474 out.push_str(" --forge ");
475 out.push_str(forge);
476 }
477 if !self.repo.is_empty() && self.repo != crate::projection::REPO_PLACEHOLDER {
481 out.push_str(" --repo ");
482 out.push_str(&self.repo);
483 }
484 out.push_str(" --release-mode ");
485 out.push_str(self.profile.release.mode.as_str());
486 if let Some(driver) = &self.profile.release.driver {
487 out.push_str(" --release-driver ");
488 out.push_str(driver);
489 }
490 if let Some(style) = self.profile.release.style {
491 out.push_str(" --release-style ");
492 out.push_str(style.as_str());
493 }
494 out.push_str(" --trunk ");
495 out.push_str(&self.git.trunk);
496 out.push_str(" --checkout-mode ");
497 out.push_str(self.git.checkout_mode.as_str());
498 out
499 }
500
501 #[must_use]
507 pub fn capability_flags(&self) -> String {
508 let mut out = String::new();
509 if self.capabilities.nix_packaging {
510 out.push_str(" --nix-packaging");
511 }
512 if self.capabilities.reporting_policy {
513 out.push_str(" --reporting-policy");
514 }
515 if self.capabilities.scorecard {
516 out.push_str(" --scorecard");
517 }
518 out.push_str(" --code-scanning ");
525 out.push_str(
526 self.capabilities
527 .code_scanning
528 .map_or("off", Provider::as_str),
529 );
530 out
531 }
532
533 #[must_use]
540 pub fn capability_toggles(&self) -> String {
541 let word = |on: bool| if on { "on" } else { "off" };
542 format!(
543 " --nix-packaging {} --reporting-policy {} --scorecard {} --code-scanning {}",
544 word(self.capabilities.nix_packaging),
545 word(self.capabilities.reporting_policy),
546 word(self.capabilities.scorecard),
547 self.capabilities
548 .code_scanning
549 .map_or("off", Provider::as_str)
550 )
551 }
552}
553
554#[cfg(test)]
555impl Params {
556 pub(crate) fn for_test(repo: &str, style: Option<Style>) -> Self {
561 Self {
562 profile: ProfileSnapshot {
563 technologies: vec!["rust".to_owned()],
564 forge: Some("github".to_owned()),
565 release: ReleaseIntent {
566 mode: ReleaseMode::Automatic,
567 driver: Some("rust".to_owned()),
568 style,
569 line_prefix: Some(crate::config::LINE_PREFIX_DEFAULT.to_owned()),
570 },
571 },
572 git: GitWorkflow {
573 trunk: crate::config::TRUNK_DEFAULT.to_owned(),
574 checkout_mode: CheckoutMode::LinkedWorktree,
575 },
576 capabilities: CapabilityRequests {
577 nix_packaging: false,
578 reporting_policy: true,
579 scorecard: false,
580 code_scanning: None,
581 },
582 repo: repo.to_owned(),
583 security_contact: String::new(),
584 security_response: crate::config::RESPONSE_DEFAULT.to_owned(),
585 }
586 }
587
588 pub(crate) fn for_test_security(contact: &str, response: &str) -> Self {
590 Self {
591 security_contact: contact.to_owned(),
592 security_response: response.to_owned(),
593 ..Self::for_test("acme/widget", Some(Style::Trunk))
594 }
595 }
596
597 pub(crate) fn for_test_release_less(
600 technologies: &[&str],
601 forge: Option<&str>,
602 mode: ReleaseMode,
603 ) -> Self {
604 let mut params = Self::for_test("acme/widget", None);
605 params.profile.technologies = technologies.iter().map(|t| (*t).to_owned()).collect();
606 params.profile.forge = forge.map(str::to_owned);
607 params.profile.release = ReleaseIntent {
608 mode,
609 driver: None,
610 style: None,
611 line_prefix: None,
612 };
613 params.capabilities.reporting_policy = false;
614 if forge.is_none() {
615 params.repo = String::new();
616 }
617 params
618 }
619
620 pub(crate) fn set_pair_for_test(&mut self, driver: &str, forge: &str) {
622 self.profile.technologies = vec![driver.to_owned()];
623 self.profile.release.driver = Some(driver.to_owned());
624 self.profile.forge = Some(forge.to_owned());
625 }
626
627 pub(crate) const fn set_checkout_mode_for_test(&mut self, mode: CheckoutMode) {
629 self.git.checkout_mode = mode;
630 }
631
632 pub(crate) const fn set_nix_for_test(&mut self, nix: bool) {
634 self.capabilities.nix_packaging = nix;
635 }
636
637 pub(crate) const fn set_scorecard_for_test(&mut self, scorecard: bool) {
639 self.capabilities.scorecard = scorecard;
640 }
641
642 pub(crate) const fn set_code_scanning_for_test(&mut self, provider: Option<Provider>) {
644 self.capabilities.code_scanning = provider;
645 }
646}
647
648fn invalid_release(message: impl std::fmt::Display) -> RkError {
651 RkError::Usage(format!(
652 "{message}; an automatic release names a driver among profile.technologies and a style, and an external or none release names neither"
653 ))
654}
655
656fn answered<T>(chain: [(Option<T>, Source); 5]) -> Option<(T, Source)> {
658 chain
659 .into_iter()
660 .find_map(|(value, source)| value.map(|value| (value, source)))
661}
662
663#[allow(
671 clippy::too_many_lines,
672 reason = "the resolution is one precedence walk per field, and splitting it would hide that every field walks the same chain"
673)]
674pub fn resolve(
675 target: &Utf8Path,
676 flags: &Inputs<'_>,
677 config: Option<&crate::config::Config>,
678 record: Option<&manifest::Manifest>,
679 purpose: Purpose,
680) -> Result<Resolved, RkError> {
681 let mut sources: BTreeMap<&'static str, Source> = BTreeMap::new();
682 let observed = crate::detect::observe(target.as_std_path());
683 let known_drivers = catalog::known_drivers();
684
685 let (technologies, source) = answered([
687 (
688 (!flags.technologies.is_empty()).then(|| flags.technologies.to_vec()),
689 Source::Flag,
690 ),
691 (
692 config.and_then(|c| c.profile.technologies.clone()),
693 Source::Config,
694 ),
695 (
696 record.map(|r| r.profile.technologies.clone()),
697 Source::Record,
698 ),
699 (
700 Some(
701 observed
702 .technologies
703 .iter()
704 .map(|t| (*t).to_owned())
705 .collect(),
706 ),
707 Source::Observation,
708 ),
709 (None, Source::Default),
710 ])
711 .unwrap_or_else(|| (Vec::new(), Source::Default));
712 let technologies =
713 canonical_list("profile.technologies", &technologies).map_err(RkError::Usage)?;
714 sources.insert("profile.technologies", source);
715
716 let forge_flag = flags
718 .forge
719 .map(|name| canonical_category(name).map_err(RkError::Usage))
720 .transpose()?;
721 let (forge, source) = answered([
722 (forge_flag.map(Some), Source::Flag),
723 (
724 config
725 .and_then(|c| c.profile.forge.clone())
726 .map(|value| if value.is_empty() { None } else { Some(value) }),
727 Source::Config,
728 ),
729 (record.map(|r| r.profile.forge.clone()), Source::Record),
730 (
731 observed.forge.map(|forge| Some(forge.as_str().to_owned())),
732 Source::Observation,
733 ),
734 (Some(None), Source::Default),
735 ])
736 .unwrap_or((None, Source::Default));
737 let forge = forge
738 .map(|name| canonical_category(&name).map_err(RkError::Usage))
739 .transpose()?;
740 sources.insert("profile.forge", source);
741
742 let (repo, source) = answered([
744 (flags.repo.map(str::to_owned), Source::Flag),
745 (
746 config
747 .map(|c| c.project.repo.clone())
748 .filter(|value| !value.is_empty()),
749 Source::Config,
750 ),
751 (
752 record
753 .map(|r| r.parameters.repo.clone())
754 .filter(|value| !value.is_empty()),
755 Source::Record,
756 ),
757 (observed.repo.clone(), Source::Observation),
758 (None, Source::Default),
759 ])
760 .map_or((None, Source::Default), |(value, source)| {
761 (Some(value), source)
762 });
763 sources.insert("project.repo", source);
764
765 let release_bearing: Vec<String> = technologies
768 .iter()
769 .filter(|name| known_drivers.contains(name))
770 .cloned()
771 .collect();
772 let proposal = match release_bearing.as_slice() {
773 [] => Proposal::None,
774 [one] => Proposal::Automatic {
775 driver: one.clone(),
776 },
777 many => Proposal::Ambiguous {
778 drivers: many.to_vec(),
779 },
780 };
781 let proposed_mode = match &proposal {
788 Proposal::Automatic { .. } | Proposal::Ambiguous { .. } => ReleaseMode::Automatic,
789 Proposal::None => ReleaseMode::None,
790 };
791 let (mode, source) = answered([
792 (flags.release_mode, Source::Flag),
793 (config.and_then(|c| c.profile.release.mode), Source::Config),
794 (record.map(|r| r.profile.release.mode), Source::Record),
795 (Some(proposed_mode), Source::Observation),
796 (None, Source::Default),
797 ])
798 .unwrap_or((ReleaseMode::None, Source::Default));
799 let mode_source = source;
800 sources.insert("profile.release.mode", mode_source);
801 let mode_answered_above = mode_source != Source::Observation;
802 let proposal = (!mode_answered_above).then_some(proposal);
803
804 let driver_flag = flags
808 .release_driver
809 .map(|name| canonical_category(name).map_err(RkError::Usage))
810 .transpose()?;
811 let (driver, driver_source) = answered([
812 (driver_flag, Source::Flag),
813 (
814 config.and_then(|c| c.profile.release.driver.clone()),
815 Source::Config,
816 ),
817 (
818 record.and_then(|r| r.profile.release.driver.clone()),
819 Source::Record,
820 ),
821 (
822 match &proposal {
823 Some(Proposal::Automatic { driver }) if mode == ReleaseMode::Automatic => {
824 Some(driver.clone())
825 }
826 _ => None,
827 },
828 Source::Observation,
829 ),
830 (None, Source::Default),
831 ])
832 .map_or((None, Source::Default), |(value, source)| {
833 (Some(value), source)
834 });
835 let (style, style_source) = answered([
836 (flags.style, Source::Flag),
837 (config.and_then(|c| c.profile.release.style), Source::Config),
838 (record.and_then(|r| r.profile.release.style), Source::Record),
839 (None, Source::Observation),
840 (
841 (mode == ReleaseMode::Automatic && matches!(purpose, Purpose::Init | Purpose::Preview))
842 .then_some(Style::Trunk),
843 Source::Default,
844 ),
845 ])
846 .map_or((None, Source::Default), |(value, source)| {
847 (Some(value), source)
848 });
849 let (line_prefix, prefix_source) = answered([
850 (None, Source::Flag),
851 (
852 config.and_then(|c| c.profile.release.line_prefix.clone()),
853 Source::Config,
854 ),
855 (
856 record.and_then(|r| r.profile.release.line_prefix.clone()),
857 Source::Record,
858 ),
859 (None, Source::Observation),
860 (
861 (mode == ReleaseMode::Automatic).then(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
862 Source::Default,
863 ),
864 ])
865 .map_or((None, Source::Default), |(value, source)| {
866 (Some(value), source)
867 });
868
869 let reporting = purpose == Purpose::Preview;
874 let release = match mode {
875 ReleaseMode::Automatic => {
876 if let Some(Proposal::Ambiguous { drivers }) = &proposal
877 && driver.is_none()
878 && !reporting
879 {
880 return Err(RkError::Usage(format!(
881 "the target carries more than one release-bearing technology, {}, and nothing names the driver; pass --release-driver <name>, or set profile.release.driver in {}",
882 drivers.join(" and "),
883 crate::config::CONFIG_PATH
884 )));
885 }
886 if forge.is_none() && !reporting {
887 let message = observed.host.map_or_else(
888 || "no forge detected: the target has no origin remote, and an automatic release needs one".to_owned(),
889 |host| format!("no forge detected: the host {host} is not recognized, and an automatic release needs one"),
890 );
891 return Err(RkError::refusal(
892 Diagnostic::new(Reason::ForgeUndetected, message)
893 .expected("a github.com or gitlab remote, or --forge")
894 .action("pass --forge <github|gitlab>, or --release-mode none for a project that releases nothing"),
895 ));
896 }
897 if driver.is_none() && !reporting {
898 return Err(invalid_release(format!(
899 "profile.release.mode is automatic and no driver is named; pass --release-driver <{}>",
900 known_drivers.join("|")
901 )));
902 }
903 if let Some(driver) = &driver
904 && !technologies.contains(driver)
905 && !reporting
906 {
907 return Err(invalid_release(format!(
908 "profile.release.driver names {driver}, which profile.technologies does not carry ({})",
909 if technologies.is_empty() {
910 "empty".to_owned()
911 } else {
912 technologies.join(", ")
913 }
914 )));
915 }
916 let style = match (style, purpose) {
917 (None, Purpose::Upgrade | Purpose::Adopt) => {
918 return Err(RkError::Usage(
919 "the target carries no style parameter; set profile.release.style in .release-kit/config.toml or pass --release-style <trunk|lines>".into(),
920 ));
921 }
922 (None, _) => Style::Trunk,
923 (Some(style), _) => style,
924 };
925 sources.insert("profile.release.driver", driver_source);
926 sources.insert("profile.release.style", style_source);
927 sources.insert("profile.release.line_prefix", prefix_source);
928 ReleaseIntent {
929 mode,
930 driver,
931 style: Some(style),
932 line_prefix: Some(
933 line_prefix.unwrap_or_else(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
934 ),
935 }
936 }
937 ReleaseMode::External | ReleaseMode::None => {
938 for (key, present, value_source) in [
945 ("profile.release.driver", driver.is_some(), driver_source),
946 ("profile.release.style", style.is_some(), style_source),
947 (
948 "profile.release.line_prefix",
949 line_prefix.is_some(),
950 prefix_source,
951 ),
952 ] {
953 if present
954 && matches!(value_source, Source::Flag | Source::Config)
955 && value_source.rank() <= mode_source.rank()
956 {
957 return Err(invalid_release(format!(
958 "{key} is set while profile.release.mode is {}",
959 mode.as_str()
960 )));
961 }
962 }
963 ReleaseIntent {
964 mode,
965 driver: None,
966 style: None,
967 line_prefix: None,
968 }
969 }
970 };
971
972 let adapter_known = forge
976 .as_deref()
977 .is_some_and(|name| crate::detect::Forge::parse(name).is_some());
978 let repo = match (forge.is_some(), adapter_known, repo) {
979 (false, _, _) => String::new(),
982 (true, false, repo) => repo.unwrap_or_default(),
983 (true, true, Some(repo)) => repo,
984 (true, true, None) if purpose == Purpose::Preview => {
985 crate::projection::REPO_PLACEHOLDER.to_owned()
986 }
987 (true, true, None) => return Err(crate::landing::repo_unresolved()),
988 };
989
990 let (trunk, source) = answered([
992 (flags.trunk.map(str::to_owned), Source::Flag),
993 (config.and_then(|c| c.git.trunk.clone()), Source::Config),
994 (record.map(|r| r.git.trunk.clone()), Source::Record),
995 (None, Source::Observation),
996 (
997 Some(crate::config::TRUNK_DEFAULT.to_owned()),
998 Source::Default,
999 ),
1000 ])
1001 .unwrap_or_else(|| (crate::config::TRUNK_DEFAULT.to_owned(), Source::Default));
1002 sources.insert("git.trunk", source);
1003 let (checkout_mode, source) = answered([
1004 (flags.checkout_mode, Source::Flag),
1005 (config.and_then(|c| c.git.checkout_mode), Source::Config),
1006 (record.map(|r| r.git.checkout_mode), Source::Record),
1007 (None, Source::Observation),
1008 (
1009 Some(if purpose == Purpose::Adopt {
1010 CheckoutMode::MainWorktree
1011 } else {
1012 CheckoutMode::LinkedWorktree
1013 }),
1014 Source::Default,
1015 ),
1016 ])
1017 .unwrap_or((CheckoutMode::LinkedWorktree, Source::Default));
1018 sources.insert("git.checkout_mode", source);
1019
1020 let (nix_packaging, source) = answered([
1022 (flags.nix, Source::Flag),
1023 (
1024 config.and_then(|c| c.capabilities.nix_packaging),
1025 Source::Config,
1026 ),
1027 (record.map(|r| r.capabilities.nix_packaging), Source::Record),
1028 (None, Source::Observation),
1029 (Some(false), Source::Default),
1030 ])
1031 .unwrap_or((false, Source::Default));
1032 sources.insert("capabilities.nix_packaging", source);
1033 let (reporting_policy, source) = answered([
1034 (flags.reporting_policy, Source::Flag),
1035 (
1036 config.and_then(|c| c.capabilities.reporting_policy),
1037 Source::Config,
1038 ),
1039 (
1040 record.map(|r| r.capabilities.reporting_policy),
1041 Source::Record,
1042 ),
1043 (None, Source::Observation),
1044 (
1047 Some(release.mode == ReleaseMode::Automatic),
1048 Source::Default,
1049 ),
1050 ])
1051 .unwrap_or((false, Source::Default));
1052 sources.insert("capabilities.reporting_policy", source);
1053 let (scorecard, source) = answered([
1054 (flags.scorecard, Source::Flag),
1055 (
1056 config.and_then(|c| c.capabilities.scorecard),
1057 Source::Config,
1058 ),
1059 (record.map(|r| r.capabilities.scorecard), Source::Record),
1060 (None, Source::Observation),
1061 (Some(false), Source::Default),
1062 ])
1063 .unwrap_or((false, Source::Default));
1064 sources.insert("capabilities.scorecard", source);
1065 let configured_scanning = config
1066 .and_then(|c| c.capabilities.code_scanning.as_deref())
1067 .map(Provider::parse)
1068 .transpose()?;
1069 let (code_scanning, source) = answered([
1070 (flags.code_scanning, Source::Flag),
1071 (configured_scanning, Source::Config),
1072 (record.map(|r| r.capabilities.code_scanning), Source::Record),
1073 (None, Source::Observation),
1074 (Some(None), Source::Default),
1075 ])
1076 .unwrap_or((None, Source::Default));
1077 sources.insert("capabilities.code_scanning", source);
1078 let (security_contact, source) = answered([
1086 (None, Source::Flag),
1087 (
1088 config.and_then(|c| c.security.contact.clone()),
1089 Source::Config,
1090 ),
1091 (
1092 record.map(|r| r.parameters.security_contact.clone()),
1093 Source::Record,
1094 ),
1095 (None, Source::Observation),
1096 (Some(String::new()), Source::Default),
1097 ])
1098 .unwrap_or((String::new(), Source::Default));
1099 let security_contact =
1100 crate::config::canonical_contact(&security_contact).map_err(crate::config::invalid)?;
1101 sources.insert("security.contact", source);
1102 let (security_response, source) = answered([
1103 (None, Source::Flag),
1104 (
1105 config.and_then(|c| c.security.response.clone()),
1106 Source::Config,
1107 ),
1108 (
1109 record.map(|r| r.parameters.security_response.clone()),
1110 Source::Record,
1111 ),
1112 (None, Source::Observation),
1113 (
1114 Some(crate::config::RESPONSE_DEFAULT.to_owned()),
1115 Source::Default,
1116 ),
1117 ])
1118 .unwrap_or_else(|| (crate::config::RESPONSE_DEFAULT.to_owned(), Source::Default));
1119 let security_response =
1120 crate::config::canonical_response(&security_response).map_err(crate::config::invalid)?;
1121 sources.insert("security.response", source);
1122
1123 let mut unknown: Vec<String> = technologies
1124 .iter()
1125 .filter(|name| !known_drivers.contains(name))
1126 .map(|name| format!("technology {name}"))
1127 .collect();
1128 if let Some(name) = &forge
1129 && crate::detect::Forge::parse(name).is_none()
1130 {
1131 unknown.push(format!("forge {name}"));
1132 }
1133
1134 Ok(Resolved {
1135 params: Params {
1136 profile: ProfileSnapshot {
1137 technologies,
1138 forge,
1139 release,
1140 },
1141 git: GitWorkflow {
1142 trunk,
1143 checkout_mode,
1144 },
1145 capabilities: CapabilityRequests {
1146 nix_packaging,
1147 reporting_policy,
1148 scorecard,
1149 code_scanning,
1150 },
1151 repo,
1152 security_contact,
1153 security_response,
1154 },
1155 sources,
1156 unknown,
1157 proposal,
1158 })
1159}