majordomus_cli/release/model.rs
1//! The typed value every release surface answers with.
2//!
3//! Published through the capability registry, so these types share the one OpenAPI component
4//! namespace with every other module's: each carries a `Release`-prefixed schema name, the
5//! way `distribution` prefixes its own for exactly the same reason.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// The schema of the changelog document.
11pub const CHANGELOG_SCHEMA: &str = "majordomus/changelog/v1";
12
13/// What a conventional commit says it did.
14///
15/// The set is the one the repository's own commit convention uses; a commit whose subject
16/// does not parse is [`ChangeKind::Other`] and still appears, because a changelog that
17/// silently drops what it cannot classify is a changelog that lies by omission.
18#[derive(
19 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
20)]
21#[serde(rename_all = "snake_case")]
22#[schemars(rename = "ReleaseChangeKind")]
23pub enum ChangeKind {
24 /// A capability a person did not have before.
25 Feat,
26 /// Behaviour that was wrong and is not any more.
27 Fix,
28 /// Performance, with the behaviour unchanged.
29 Perf,
30 /// Structure, with the behaviour unchanged.
31 Refactor,
32 /// Documentation.
33 Docs,
34 /// Tests.
35 Test,
36 /// The build, the pipeline, the tooling.
37 Chore,
38 /// Continuous integration.
39 Ci,
40 /// Formatting, with the behaviour unchanged.
41 Style,
42 /// The build system or its dependencies.
43 Build,
44 /// A change that undoes another.
45 Revert,
46 /// An integration: one line of work brought into another.
47 ///
48 /// Not in the conventional-commit specification, and in this repository's history 38
49 /// times and counting, because integration here is a deliberate act with a subject
50 /// somebody writes — `merge: bring origin/master into feature/x` — and not the sentence
51 /// `git merge` composes. Declaring it is what lets the validator hold those subjects to
52 /// the same width and the same references as every other commit, instead of either
53 /// refusing a convention this project practises or exempting it from being checked at
54 /// all. The changelog never renders it: `commits::in_range` reads the log with
55 /// `--no-merges`, so a merge commit is not a release entry however its subject is
56 /// spelled.
57 Merge,
58 /// A commit whose subject does not parse as a conventional commit.
59 Other,
60}
61
62impl ChangeKind {
63 /// Every type word the convention defines, with the kind it names.
64 ///
65 /// One list, read in both directions: [`ChangeKind::parse`] maps a word to a kind, and
66 /// [`ChangeKind::WORDS`] is what a validator quotes back to somebody who typed a word
67 /// that is not one. A second copy of these eleven words in the validator is exactly the
68 /// duplication that lets a type added here go unmentioned there.
69 const TABLE: &'static [(&'static str, ChangeKind)] = &[
70 ("feat", ChangeKind::Feat),
71 ("fix", ChangeKind::Fix),
72 ("perf", ChangeKind::Perf),
73 ("refactor", ChangeKind::Refactor),
74 ("docs", ChangeKind::Docs),
75 ("test", ChangeKind::Test),
76 ("chore", ChangeKind::Chore),
77 ("ci", ChangeKind::Ci),
78 ("style", ChangeKind::Style),
79 ("build", ChangeKind::Build),
80 ("revert", ChangeKind::Revert),
81 ("merge", ChangeKind::Merge),
82 ];
83
84 /// The type words, in the order `Self::TABLE` declares them.
85 ///
86 /// ```
87 /// use majordomus_cli::release::ChangeKind;
88 /// assert_eq!(ChangeKind::WORDS.first(), Some(&"feat"));
89 /// assert!(ChangeKind::WORDS.iter().all(|w| ChangeKind::parse(w) != ChangeKind::Other));
90 /// ```
91 pub const WORDS: &'static [&'static str] = &[
92 "feat", "fix", "perf", "refactor", "docs", "test", "chore", "ci", "style", "build",
93 "revert", "merge",
94 ];
95
96 /// The kind one conventional-commit type word names.
97 pub fn parse(word: &str) -> ChangeKind {
98 ChangeKind::TABLE
99 .iter()
100 .find(|(w, _)| *w == word)
101 .map(|(_, k)| *k)
102 .unwrap_or(ChangeKind::Other)
103 }
104
105 /// The word this kind is written as, or `None` for [`ChangeKind::Other`], which is not
106 /// a type anybody writes.
107 ///
108 /// The inverse of [`ChangeKind::parse`] over the one table, so the two cannot disagree
109 /// about which words exist:
110 ///
111 /// ```
112 /// use majordomus_cli::release::model::ChangeKind;
113 ///
114 /// assert_eq!(ChangeKind::parse("feat").word(), Some("feat"));
115 /// assert_eq!(ChangeKind::Perf.word(), Some("perf"));
116 ///
117 /// // `Other` is what an unknown word parses to, and it is not itself a word: a
118 /// // validator quoting the vocabulary back must not offer it as something to type.
119 /// assert_eq!(ChangeKind::parse("nonsense"), ChangeKind::Other);
120 /// assert_eq!(ChangeKind::Other.word(), None);
121 ///
122 /// // every word the vocabulary advertises round-trips through both directions
123 /// for word in ChangeKind::WORDS {
124 /// assert_eq!(ChangeKind::parse(word).word(), Some(*word), "{word} round-trips");
125 /// }
126 /// ```
127 pub fn word(self) -> Option<&'static str> {
128 ChangeKind::TABLE
129 .iter()
130 .find(|(_, k)| *k == self)
131 .map(|(w, _)| *w)
132 }
133
134 /// The heading this kind is rendered under, and the order the headings appear in.
135 ///
136 /// Ordered by what a reader of a changelog came for: what is new, what is fixed, what is
137 /// faster, then the rest. Nothing is hidden — a section with no entries is simply absent.
138 pub fn heading(self) -> &'static str {
139 match self {
140 ChangeKind::Feat => "Added",
141 ChangeKind::Fix => "Fixed",
142 ChangeKind::Perf => "Performance",
143 ChangeKind::Refactor => "Changed",
144 ChangeKind::Docs => "Documentation",
145 ChangeKind::Test => "Tests",
146 ChangeKind::Ci => "Pipeline",
147 ChangeKind::Chore => "Housekeeping",
148 ChangeKind::Style => "Formatting",
149 ChangeKind::Build => "Build",
150 ChangeKind::Revert => "Reverted",
151 ChangeKind::Merge => "Integration",
152 ChangeKind::Other => "Other",
153 }
154 }
155
156 /// Where the heading sorts.
157 pub fn rank(self) -> u8 {
158 match self {
159 ChangeKind::Feat => 0,
160 ChangeKind::Fix => 1,
161 ChangeKind::Perf => 2,
162 ChangeKind::Refactor => 3,
163 ChangeKind::Docs => 4,
164 ChangeKind::Test => 5,
165 ChangeKind::Ci => 6,
166 ChangeKind::Chore => 7,
167 // A revert is what a reader most needs to see and least expects, so it sits with
168 // the fixes rather than at the bottom with the housekeeping.
169 ChangeKind::Revert => 2,
170 ChangeKind::Build => 6,
171 ChangeKind::Style => 7,
172 ChangeKind::Merge => 8,
173 ChangeKind::Other => 8,
174 }
175 }
176}
177
178/// A record of the layer that a commit names in its own text.
179///
180/// Inferred, never declared beside the commit: an issue id or a milestone id appearing in a
181/// subject or a body is a reference, and the layer already holds the object it refers to. A
182/// reference to something the layer does not have is not carried — a link to a record that
183/// does not exist is worse than no link, because the reader cannot tell until they follow it.
184///
185/// ```
186/// use majordomus_cli::release::model::Reference;
187/// let r = Reference {
188/// kind: "issue".into(),
189/// id: "I1305".into(),
190/// title: "An observed contract has a fingerprint".into(),
191/// route: Some("/plan/i1305/".into()),
192/// };
193/// let json = serde_json::to_value(&r).unwrap();
194/// assert_eq!(json["id"], "I1305");
195/// assert_eq!(json["route"], "/plan/i1305/");
196/// // a reference the site has no page for carries no route rather than an invented one
197/// let bare = Reference { route: None, ..r };
198/// assert!(serde_json::to_value(&bare).unwrap().get("route").is_none());
199/// ```
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
201#[schemars(rename = "ReleaseReference")]
202pub struct Reference {
203 /// `issue` or `milestone`.
204 pub kind: String,
205 /// `I1305`, `M000`.
206 pub id: String,
207 /// What it is, from the record itself.
208 pub title: String,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 /// Where it is published on this site, when it has a page.
211 pub route: Option<String>,
212}
213
214/// One change, from one commit.
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
216#[schemars(rename = "ReleaseChange")]
217pub struct Change {
218 /// What it did.
219 pub kind: ChangeKind,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 /// The area it did it in, when the subject named one: `commands`, `ci`, `site`.
222 pub scope: Option<String>,
223 /// The subject, without the type and scope that prefixed it.
224 pub subject: String,
225 /// Whether the commit marked itself breaking, with `!` or a `BREAKING CHANGE:` trailer.
226 pub breaking: bool,
227 /// The abbreviated commit.
228 pub commit: String,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 /// Where that commit can be read, when the repository's own URL is known. Derived from
231 /// `about::REPOSITORY`, never written beside each entry.
232 pub url: Option<String>,
233 #[serde(default, skip_serializing_if = "Vec::is_empty")]
234 /// The records of the layer this commit names — issues, milestones — resolved against
235 /// what the layer actually holds.
236 pub references: Vec<Reference>,
237}
238
239/// One decision, as the layer's own ADR object states it.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
241#[schemars(rename = "ReleaseDecision")]
242pub struct Decision {
243 /// `adr-0027`.
244 pub id: String,
245 /// The decision, in its own words.
246 pub title: String,
247 /// `proposed`, `accepted`, `superseded`.
248 pub status: String,
249 /// The date the record carries.
250 pub date: String,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 /// Where the decision itself can be read: the file in the repository. The site has no
253 /// per-ADR page, so this is the honest destination rather than an invented route.
254 pub url: Option<String>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 /// The commit that added the file, which is what decides the release it belongs to. The
257 /// front-matter date says when the decision was made; those are different questions.
258 pub added: Option<String>,
259}
260
261/// One published artifact, from the release record's own evidence.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
263#[schemars(rename = "ReleaseChangelogArtifact")]
264pub struct Artifact {
265 /// The platform triple the record names.
266 pub target: String,
267 /// The file name.
268 pub name: String,
269 /// Its SHA-256, as the record read it off the file.
270 pub sha256: String,
271}
272
273/// The changes of one kind, with the heading and the rank that decide where they are shown.
274///
275/// The grouping is in the document rather than in each renderer. It was in the Markdown
276/// renderer alone, and the site — which cannot see a Rust function — grouped alphabetically
277/// instead, so the same changelog read in two orders depending on which surface showed it.
278/// A presentation order stated once and carried is the only kind that survives a projection.
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
280#[schemars(rename = "ReleaseChangeGroup")]
281pub struct ChangeGroup {
282 /// What the changes in it did.
283 pub kind: ChangeKind,
284 /// The heading it is shown under.
285 pub heading: String,
286 /// Where it sorts among the other groups; ascending.
287 pub rank: u8,
288 /// The changes, in the order the commits were read.
289 pub changes: Vec<Change>,
290}
291
292/// One version's worth of changelog.
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
294#[schemars(rename = "ReleaseSection")]
295pub struct ReleaseSection {
296 /// The version, or `unreleased`.
297 pub version: String,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 /// The tag, when one was published.
300 pub tag: Option<String>,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 /// When it was published, from the record.
303 pub date: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 /// The commit it was published from.
306 pub commit: Option<String>,
307 /// Whether this section is the work that has not been released.
308 pub unreleased: bool,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 /// The release notes the record names — the published release itself.
311 pub notes_url: Option<String>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 /// Every commit between the previous release and this one, as the forge renders it.
314 pub compare_url: Option<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 /// The tree at this release.
317 pub tree_url: Option<String>,
318 /// The decisions dated inside this release's window.
319 pub decisions: Vec<Decision>,
320 /// The changes, from the commits in this release's range, grouped by what they did and
321 /// ordered by the rank each group carries. The flat list is the concatenation of the
322 /// groups' own; nothing holds it twice.
323 pub groups: Vec<ChangeGroup>,
324 /// What was published, when this section is a release.
325 pub artifacts: Vec<Artifact>,
326}
327
328/// Which capability answered with this document, and where that capability is projected.
329///
330/// The document carries its own provenance so that a reader — a person on the site, a client
331/// over MCP — can find the same value elsewhere without anything having to enumerate the
332/// routes. A page that listed them would be declaring them a second time, which is the
333/// failure `site-check`'s `registry` and `cli` assertions exist to catch; it caught this one.
334///
335/// ```
336/// use majordomus_cli::release::model::ProducedBy;
337/// let by = ProducedBy {
338/// capability: "release.changelog".into(),
339/// cli: Some("majordomus release changelog".into()),
340/// http: Some("/api/v1/changelog".into()),
341/// mcp_tool: None,
342/// mcp_resource: Some("majordomus://changelog".into()),
343/// };
344/// let json = serde_json::to_value(&by).unwrap();
345/// assert_eq!(json["capability"], "release.changelog");
346/// // a surface the capability is not projected on is absent, not null
347/// assert!(json.get("mcp_tool").is_none());
348/// assert_eq!(ProducedBy::default().capability, "");
349/// ```
350#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
351#[schemars(rename = "ReleaseProducedBy")]
352pub struct ProducedBy {
353 /// The capability id.
354 pub capability: String,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 /// The command line that renders it.
357 pub cli: Option<String>,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 /// The HTTP route that answers with it.
360 pub http: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
362 /// The MCP tool.
363 pub mcp_tool: Option<String>,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 /// The MCP resource URI.
366 pub mcp_resource: Option<String>,
367}
368
369/// The whole changelog.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
371#[schemars(rename = "ReleaseChangelog")]
372pub struct Changelog {
373 /// The schema this document satisfies.
374 pub schema: String,
375 /// The version the tree currently declares.
376 pub current: String,
377 /// Newest first, the unreleased section leading when there is one.
378 pub sections: Vec<ReleaseSection>,
379 /// What could not be read, said rather than hidden: a repository with no git history,
380 /// a release record that names no commit, a tag that is not in this clone.
381 pub diagnostics: Vec<String>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 /// Which capability answered, and where else the same value can be had.
384 pub produced_by: Option<ProducedBy>,
385}
386
387/// Who answered a version report's `next`: the public contract, the contract with the
388/// commits — or no one, because the contract could not be measured.
389///
390/// ADR 0051 makes the contract the authority and demotes conventional commits to evidence,
391/// so [`DecidedBy::Contract`] is the normal answer. An unmeasurable contract is refused, not
392/// guessed: [`DecidedBy::Undecided`] leaves `next` absent and carries the reason, and the
393/// commit inference stays what it always is, evidence — never the answer in its place.
394///
395/// ```
396/// use majordomus_cli::release::model::DecidedBy;
397/// assert_eq!(serde_json::to_value(DecidedBy::Contract).unwrap(), "contract");
398/// assert_eq!(DecidedBy::Undecided.as_str(), "undecided");
399/// assert_eq!(DecidedBy::default(), DecidedBy::Undecided);
400/// ```
401#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
402#[serde(rename_all = "snake_case")]
403#[schemars(rename = "ReleaseVersionDecidedBy")]
404pub enum DecidedBy {
405 /// `release analyze` measured the public contract against the last release, and `next`
406 /// is the version it requires: the declared version when that already satisfies the
407 /// contract, otherwise the smallest one it allows.
408 Contract,
409 /// The contract was measured and requires no release over the last one — only behaviour
410 /// behind the public boundary changed — while the commits carry changes: `next` is the
411 /// smallest release above the last, a patch, however much the commit subjects claim.
412 ContractAndCommits,
413 /// The contract could not be measured — no published baseline carries a registry, or a
414 /// version is not three numbers — so nothing decides `next`: it is absent, the reason is
415 /// in `contract_unreadable`, and `release bump` without an explicit target refuses.
416 #[default]
417 Undecided,
418}
419
420impl DecidedBy {
421 /// The word the JSON value carries for who answered.
422 ///
423 /// ```
424 /// use majordomus_cli::release::model::DecidedBy;
425 /// for by in [DecidedBy::Contract, DecidedBy::ContractAndCommits, DecidedBy::Undecided] {
426 /// assert_eq!(serde_json::to_value(by).unwrap(), by.as_str());
427 /// }
428 /// ```
429 pub fn as_str(self) -> &'static str {
430 match self {
431 DecidedBy::Contract => "contract",
432 DecidedBy::ContractAndCommits => "contract_and_commits",
433 DecidedBy::Undecided => "undecided",
434 }
435 }
436
437 /// Who answered, as the text renderings print it in parentheses after `next`.
438 ///
439 /// ```
440 /// use majordomus_cli::release::model::DecidedBy;
441 /// assert_eq!(DecidedBy::Contract.phrase(), "decided by the contract");
442 /// let both = DecidedBy::ContractAndCommits.phrase();
443 /// assert_eq!(both, "decided by the contract and the commits");
444 /// let none = DecidedBy::Undecided.phrase();
445 /// assert_eq!(none, "undecided: the contract could not be measured");
446 /// ```
447 pub fn phrase(self) -> &'static str {
448 match self {
449 DecidedBy::Contract => "decided by the contract",
450 DecidedBy::ContractAndCommits => "decided by the contract and the commits",
451 DecidedBy::Undecided => "undecided: the contract could not be measured",
452 }
453 }
454}
455
456/// What the version is, what the public contract requires it to become, and what the
457/// commits since the last release say about themselves.
458///
459/// `next` is the contract's answer (ADR 0051): the analysis' `required_version`, or the
460/// smallest release above the last when that requirement is the last release itself. The
461/// commit inference is evidence — `bump` and `commits_imply` — and never the answer: when the
462/// contract cannot be measured `next` is absent, `decided_by` is `undecided` and
463/// `contract_unreadable` says why. The selection is [`crate::release::version::select`], the
464/// one function `release bump` also takes its default target from.
465#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
466#[schemars(rename = "ReleaseVersionReport")]
467pub struct VersionReport {
468 /// The version the crate manifest declares — the one place it is authored.
469 pub declared: String,
470 /// The version `bin/majordomus` prints: what its projection, `share/version.txt`, states.
471 pub tool: String,
472 /// Whether that projection is current. `generate --check` refuses a stale one and
473 /// `scripts/release-version --check` proves the tool prints it; this is the same question
474 /// asked by the executable, so every surface can show the answer.
475 pub agree: bool,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 /// The last release: the highest version the layer records (the newest version tag when
478 /// it records none) — the baseline `release analyze` measures the contract from, so the
479 /// contract and the commit evidence describe one window.
480 pub last_release: Option<String>,
481 /// What the commit subjects since it imply: `major`, `minor`, `patch`, or `none`.
482 /// Evidence only — the contract decides `next`; `release analyze` reports where the two
483 /// disagree.
484 pub bump: String,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 /// The version that bump would produce from the last release (from the declared version
487 /// when nothing was released yet). Evidence only, even when the contract cannot answer.
488 pub commits_imply: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none")]
490 /// The next version. When the contract could be measured, the version it requires: the
491 /// declared version when that already satisfies it, otherwise the smallest one it allows
492 /// — and, when that is the last release itself, the smallest release above it. Absent
493 /// when nothing would be released, and when the contract could not be measured
494 /// (`decided_by: undecided`): an unmeasurable baseline is refused, not guessed.
495 pub next: Option<String>,
496 #[serde(default)]
497 /// Who answered `next`.
498 pub decided_by: DecidedBy,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 /// Why the contract could not answer, when `decided_by` is `undecided`.
501 pub contract_unreadable: Option<String>,
502 /// How many commits since the last release, and of what kind — the evidence for the
503 /// bump, so that a surprising answer can be checked rather than believed.
504 pub changes: Vec<Change>,
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn the_type_vocabulary_reads_the_same_in_both_directions() {
513 // `WORDS` is what a validator quotes and `TABLE` is what the parser reads. They are
514 // two spellings of one list, and this is the assertion that keeps them one: a type
515 // added to the table and forgotten in the words, or the reverse, fails here rather
516 // than in a validator that quietly stops mentioning it.
517 let from_table: Vec<&str> = ChangeKind::TABLE.iter().map(|(w, _)| *w).collect();
518 assert_eq!(from_table, ChangeKind::WORDS);
519 for word in ChangeKind::WORDS {
520 let kind = ChangeKind::parse(word);
521 assert_ne!(kind, ChangeKind::Other, "{word} parses to nothing");
522 assert_eq!(kind.word(), Some(*word), "{word} does not render back");
523 }
524 // every kind but `Other` is reachable from a word, so no variant is orphaned
525 assert_eq!(ChangeKind::TABLE.len(), ChangeKind::WORDS.len());
526 assert_eq!(ChangeKind::Other.word(), None);
527 assert_eq!(ChangeKind::parse("wip"), ChangeKind::Other);
528 }
529
530 /// Who decided `next` is one word on the wire and one phrase in the text renderings, and
531 /// the word is the serialization itself: a variant renamed in one place and not the
532 /// other would make the JSON and the terminal name different deciders.
533 #[test]
534 fn who_decided_reads_the_same_on_the_wire_and_in_the_text() {
535 let all = [
536 DecidedBy::Contract,
537 DecidedBy::ContractAndCommits,
538 DecidedBy::Undecided,
539 ];
540 for by in all {
541 assert_eq!(serde_json::to_value(by).unwrap(), by.as_str());
542 let back: DecidedBy = serde_json::from_value(by.as_str().into()).unwrap();
543 assert_eq!(back, by);
544 }
545 assert_eq!(
546 all.map(DecidedBy::as_str),
547 ["contract", "contract_and_commits", "undecided"]
548 );
549 assert_eq!(DecidedBy::Contract.phrase(), "decided by the contract");
550 assert_eq!(
551 DecidedBy::ContractAndCommits.phrase(),
552 "decided by the contract and the commits"
553 );
554 // the undecided phrase says why nothing was decided, never a version's provenance
555 assert_eq!(
556 DecidedBy::Undecided.phrase(),
557 "undecided: the contract could not be measured"
558 );
559 // the absent field is the undecided one: an answer that names no decider decided nothing
560 assert_eq!(DecidedBy::default(), DecidedBy::Undecided);
561 }
562}