Skip to main content

majordomus_cli/quality/
model.rs

1//! The vocabulary of a quality report: what was measured, what is wrong, and what a
2//! reader — a person, a pipeline or an agent — is to do about it.
3//!
4//! # Why the finding is typed
5//!
6//! A gate that prints `quality failed` is hostile infrastructure. Every finding here
7//! carries a stable [`ViolationCode`], the [rule](ViolationCode::rule) that requires it,
8//! the file and line it happened at, the symbol it is about, and a remediation in the
9//! imperative. That is what lets one measurement render as a terminal report, a JSON
10//! document, an HTTP response, a Cockpit panel and a CI annotation without any of them
11//! deciding anything of their own — the same relationship every other projection in this
12//! executable has with the registry.
13//!
14//! # The shape
15//!
16//! ```text
17//! QualityReport
18//!   ├── public_api  : items measured, documented, exampled
19//!   ├── modules     : modules measured, documented, exampled, behaviourally tested
20//!   ├── operations  : canonical capabilities against CLI, HTTP, OpenAPI, MCP
21//!   └── violations  : every finding, most severe first
22//! ```
23//!
24//! ```
25//! use majordomus_cli::quality::{QualityReport, Severity};
26//!
27//! // An empty report is a clean report, and a clean report passes.
28//! let report = QualityReport::default();
29//! assert!(report.passes());
30//! assert_eq!(report.exit_code(), 0);
31//! assert_eq!(report.count(Severity::Error), 0);
32//! ```
33
34use serde::{Deserialize, Serialize};
35
36/// The schema of a quality report. Bumped when a consumer would have to change.
37pub const SCHEMA: &str = "majordomus/quality/v1";
38
39/// How much a finding matters.
40///
41/// There are two levels and not five, because the only question a gate can answer is
42/// whether the change may land. A warning is a finding the repository has decided not to
43/// block on; nothing else is a warning.
44#[derive(
45    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
46)]
47#[serde(rename_all = "snake_case")]
48#[schemars(rename = "QualitySeverity")]
49pub enum Severity {
50    /// The gate fails.
51    Error,
52    /// Reported, and the gate passes.
53    Warning,
54}
55
56impl Severity {
57    /// The word a rendering shows.
58    ///
59    /// ```
60    /// use majordomus_cli::quality::Severity;
61    /// assert_eq!(Severity::Error.as_str(), "error");
62    /// ```
63    pub fn as_str(self) -> &'static str {
64        match self {
65            Severity::Error => "error",
66            Severity::Warning => "warning",
67        }
68    }
69}
70
71/// What is wrong, as a stable machine-readable code.
72///
73/// The codes are the contract between the validator and everything downstream: CI
74/// annotations, Cockpit filters, documentation anchors and an agent deciding what to fix.
75/// A code is never renamed once it has shipped; a rule that stops existing takes its code
76/// with it.
77#[derive(
78    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
79)]
80#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
81pub enum ViolationCode {
82    /// An exported item carries no documentation at all.
83    RustPublicMissingDocs,
84    /// An exported item's documentation says nothing its signature had not already said.
85    RustPublicThinDocs,
86    /// An exported item that carries behaviour has no executable example.
87    RustPublicMissingExample,
88    /// Every example an item has is `ignore`d or is prose in a fenced box.
89    RustExampleNotExecutable,
90    /// An example asserts nothing, or asserts something that is true of any program.
91    RustExamplePlaceholder,
92    /// An example never names the item it is documenting.
93    RustExampleDoesNotNameSubject,
94    /// An exported module carries no module-level documentation.
95    RustModuleMissingDocs,
96    /// An exported module has no module-level executable example.
97    RustModuleMissingExample,
98    /// No test names an exported module, and it declares none of its own.
99    RustModuleMissingBehaviouralTest,
100    /// A command of the command line is neither a canonical capability nor classified as
101    /// belonging to the command line alone.
102    OperationCliUnclassified,
103    /// A command is classified as belonging to the command line alone, and the command
104    /// line no longer has it.
105    OperationClassificationStale,
106    /// A command is classified as local *and* bound to a capability: two answers to one
107    /// question.
108    OperationClassificationConflict,
109    /// A capability declares an HTTP route and the OpenAPI document does not describe it.
110    OperationMissingOpenapi,
111    /// A capability declares a projection that the projection itself does not carry.
112    OperationProjectionMissing,
113    /// A projection carries an entry the registry does not hold.
114    OperationProjectionOrphan,
115}
116
117impl ViolationCode {
118    /// The code as it is written everywhere: `RUST_PUBLIC_MISSING_EXAMPLE`.
119    ///
120    /// ```
121    /// use majordomus_cli::quality::ViolationCode;
122    /// assert_eq!(ViolationCode::RustPublicMissingExample.as_str(), "RUST_PUBLIC_MISSING_EXAMPLE");
123    /// ```
124    pub fn as_str(self) -> &'static str {
125        match self {
126            ViolationCode::RustPublicMissingDocs => "RUST_PUBLIC_MISSING_DOCS",
127            ViolationCode::RustPublicThinDocs => "RUST_PUBLIC_THIN_DOCS",
128            ViolationCode::RustPublicMissingExample => "RUST_PUBLIC_MISSING_EXAMPLE",
129            ViolationCode::RustExampleNotExecutable => "RUST_EXAMPLE_NOT_EXECUTABLE",
130            ViolationCode::RustExamplePlaceholder => "RUST_EXAMPLE_PLACEHOLDER",
131            ViolationCode::RustExampleDoesNotNameSubject => "RUST_EXAMPLE_DOES_NOT_NAME_SUBJECT",
132            ViolationCode::RustModuleMissingDocs => "RUST_MODULE_MISSING_DOCS",
133            ViolationCode::RustModuleMissingExample => "RUST_MODULE_MISSING_EXAMPLE",
134            ViolationCode::RustModuleMissingBehaviouralTest => {
135                "RUST_MODULE_MISSING_BEHAVIOURAL_TEST"
136            }
137            ViolationCode::OperationCliUnclassified => "OPERATION_CLI_UNCLASSIFIED",
138            ViolationCode::OperationClassificationStale => "OPERATION_CLASSIFICATION_STALE",
139            ViolationCode::OperationClassificationConflict => "OPERATION_CLASSIFICATION_CONFLICT",
140            ViolationCode::OperationMissingOpenapi => "OPERATION_MISSING_OPENAPI",
141            ViolationCode::OperationProjectionMissing => "OPERATION_PROJECTION_MISSING",
142            ViolationCode::OperationProjectionOrphan => "OPERATION_PROJECTION_ORPHAN",
143        }
144    }
145
146    /// The rule of the repository's layer that requires this, by canonical id.
147    ///
148    /// A finding that could not name a rule would be an opinion. Every code names one, and
149    /// the suite proves the rule exists.
150    ///
151    /// ```
152    /// use majordomus_cli::quality::ViolationCode;
153    /// assert_eq!(
154    ///     ViolationCode::RustModuleMissingExample.rule(),
155    ///     "project.rust-public-api-quality@1"
156    /// );
157    /// assert_eq!(
158    ///     ViolationCode::OperationCliUnclassified.rule(),
159    ///     "project.operation-transport-parity@1"
160    /// );
161    /// ```
162    pub fn rule(self) -> &'static str {
163        match self {
164            ViolationCode::OperationCliUnclassified
165            | ViolationCode::OperationClassificationStale
166            | ViolationCode::OperationClassificationConflict
167            | ViolationCode::OperationMissingOpenapi
168            | ViolationCode::OperationProjectionMissing
169            | ViolationCode::OperationProjectionOrphan => "project.operation-transport-parity@1",
170            _ => "project.rust-public-api-quality@1",
171        }
172    }
173
174    /// Why it matters, in one sentence: what goes wrong in the world if this stands.
175    ///
176    /// ```
177    /// use majordomus_cli::quality::ViolationCode;
178    /// assert!(ViolationCode::RustExamplePlaceholder.why().contains("nothing"));
179    /// ```
180    pub fn why(self) -> &'static str {
181        match self {
182            ViolationCode::RustPublicMissingDocs => {
183                "an exported item with no documentation is API somebody has to read the implementation to use"
184            }
185            ViolationCode::RustPublicThinDocs => {
186                "documentation that repeats the signature costs a reader a lookup and tells them nothing"
187            }
188            ViolationCode::RustPublicMissingExample => {
189                "prose about behaviour drifts; an executable example cannot, because the toolchain runs it"
190            }
191            ViolationCode::RustExampleNotExecutable => {
192                "an ignored or non-Rust block is compiled by nobody, so it documents whatever the API used to be"
193            }
194            ViolationCode::RustExamplePlaceholder => {
195                "an example that asserts nothing proves nothing and exists only to satisfy a counter"
196            }
197            ViolationCode::RustExampleDoesNotNameSubject => {
198                "an example that never uses the item it documents is not evidence about that item"
199            }
200            ViolationCode::RustModuleMissingDocs => {
201                "a module is a conceptual boundary, and one that does not explain itself is a directory"
202            }
203            ViolationCode::RustModuleMissingExample => {
204                "a reader arriving at a module needs one worked lifecycle, not a list of items"
205            }
206            ViolationCode::RustModuleMissingBehaviouralTest => {
207                "a module nothing exercises is a module whose behaviour no change can break visibly"
208            }
209            ViolationCode::OperationCliUnclassified => {
210                "a command that is neither a capability nor declared command-line-only is missing from the API by accident rather than by decision"
211            }
212            ViolationCode::OperationClassificationStale => {
213                "a classification naming a command that no longer exists is a statement about nothing, and hides the next one that goes stale"
214            }
215            ViolationCode::OperationClassificationConflict => {
216                "a command cannot be both projected to the API and declared local; one of the two is wrong"
217            }
218            ViolationCode::OperationMissingOpenapi => {
219                "a route absent from the document is a route no client, no Swagger UI and no generated reference knows about"
220            }
221            ViolationCode::OperationProjectionMissing => {
222                "a declared projection that does not exist is a promise the descriptor makes and the process does not keep"
223            }
224            ViolationCode::OperationProjectionOrphan => {
225                "an entry no descriptor declares is a definition living in a projection, which is the duplication the registry exists to prevent"
226            }
227        }
228    }
229
230    /// What to do about it, in the imperative. Read by a person and by an agent.
231    ///
232    /// ```
233    /// use majordomus_cli::quality::ViolationCode;
234    /// assert!(ViolationCode::OperationCliUnclassified.remediation().contains("CliExposure"));
235    /// ```
236    pub fn remediation(self) -> &'static str {
237        match self {
238            ViolationCode::RustPublicMissingDocs => {
239                "write a doc comment saying what the item is for, what it does with its inputs, and what it may fail with"
240            }
241            ViolationCode::RustPublicThinDocs => {
242                "say what the signature cannot: the invariant, the side effect, the failure, or why the item exists"
243            }
244            ViolationCode::RustPublicMissingExample => {
245                "add a ``` block that calls the item and asserts the result; or reduce the item's visibility to pub(crate) if no consumer needs it"
246            }
247            ViolationCode::RustExampleNotExecutable => {
248                "drop `ignore` so the toolchain compiles the block, or use `no_run` when only running is undesirable"
249            }
250            ViolationCode::RustExamplePlaceholder => {
251                "assert something the item computes, not something true of every program"
252            }
253            ViolationCode::RustExampleDoesNotNameSubject => {
254                "call the documented item in the example, by the name a caller would write"
255            }
256            ViolationCode::RustModuleMissingDocs => {
257                "write a //! header: what the module owns, its invariants, its error model, and how it relates to its neighbours"
258            }
259            ViolationCode::RustModuleMissingExample => {
260                "add a ``` block to the //! header showing the module's normal lifecycle end to end"
261            }
262            ViolationCode::RustModuleMissingBehaviouralTest => {
263                "add a #[cfg(test)] test beside the declaration, or exercise the module from a test under tests/"
264            }
265            ViolationCode::OperationCliUnclassified => {
266                "give the capability a CliExposure so the command is the projection of one, or declare the command in cli::LOCAL with the reason it belongs to the command line alone"
267            }
268            ViolationCode::OperationClassificationStale => {
269                "remove the entry from cli::LOCAL: the command it names is gone"
270            }
271            ViolationCode::OperationClassificationConflict => {
272                "remove the cli::LOCAL entry, or the CliExposure; the command has an answer already"
273            }
274            ViolationCode::OperationMissingOpenapi => {
275                "rebuild the OpenAPI document from the registry rather than editing it; a route it lacks is a route the generator did not see"
276            }
277            ViolationCode::OperationProjectionMissing => {
278                "check the descriptor's exposure against the projection builder; nothing but the descriptor may decide this"
279            }
280            ViolationCode::OperationProjectionOrphan => {
281                "delete the entry from the projection and declare it on the capability, where every projection reads it"
282            }
283        }
284    }
285
286    /// How bad it is. Every code this subsystem ships is an error: a warning-forever mode
287    /// is how a rule becomes decoration.
288    pub fn severity(self) -> Severity {
289        Severity::Error
290    }
291
292    /// Every code, for the projections that list them and the test that proves each one is
293    /// distinct.
294    ///
295    /// ```
296    /// use majordomus_cli::quality::ViolationCode;
297    /// let all = ViolationCode::all();
298    /// let mut codes: Vec<&str> = all.iter().map(|c| c.as_str()).collect();
299    /// codes.sort();
300    /// codes.dedup();
301    /// assert_eq!(codes.len(), all.len(), "every code is distinct");
302    /// ```
303    pub fn all() -> Vec<ViolationCode> {
304        use ViolationCode::*;
305        vec![
306            RustPublicMissingDocs,
307            RustPublicThinDocs,
308            RustPublicMissingExample,
309            RustExampleNotExecutable,
310            RustExamplePlaceholder,
311            RustExampleDoesNotNameSubject,
312            RustModuleMissingDocs,
313            RustModuleMissingExample,
314            RustModuleMissingBehaviouralTest,
315            OperationCliUnclassified,
316            OperationClassificationStale,
317            OperationClassificationConflict,
318            OperationMissingOpenapi,
319            OperationProjectionMissing,
320            OperationProjectionOrphan,
321        ]
322    }
323}
324
325/// One finding: what, where, why, and what to do.
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
327pub struct Violation {
328    /// The stable code.
329    pub code: ViolationCode,
330    /// How bad it is.
331    pub severity: Severity,
332    /// The canonical id of the rule that requires this.
333    pub rule: String,
334    /// Where it is, repository-relative; empty when the finding is not about a file.
335    pub path: String,
336    /// The line, 1-based; `None` when the finding is not about a line.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub line: Option<usize>,
339    /// The item's full path, or the command, or the capability id: what the finding is about.
340    pub symbol: String,
341    /// One sentence, specific to this occurrence.
342    pub message: String,
343    /// Why it matters, from the code.
344    pub why: String,
345    /// What to do, from the code.
346    pub remediation: String,
347}
348
349impl Violation {
350    /// A finding about an item at a place.
351    ///
352    /// The `why` and the `remediation` come from the code rather than from the caller, so
353    /// that two findings of one kind cannot explain themselves differently.
354    ///
355    /// ```
356    /// use majordomus_cli::quality::{Violation, ViolationCode};
357    /// let v = Violation::new(
358    ///     ViolationCode::RustModuleMissingExample,
359    ///     "majordomus_cli::graph",
360    ///     "apps/majordomus-cli/src/graph.rs",
361    ///     Some(1),
362    ///     "the module header carries no executable example",
363    /// );
364    /// assert_eq!(v.rule, "project.rust-public-api-quality@1");
365    /// assert!(v.remediation.contains("//!"));
366    /// ```
367    pub fn new(
368        code: ViolationCode,
369        symbol: impl Into<String>,
370        path: impl Into<String>,
371        line: Option<usize>,
372        message: impl Into<String>,
373    ) -> Self {
374        Violation {
375            code,
376            severity: code.severity(),
377            rule: code.rule().to_string(),
378            path: path.into(),
379            line,
380            symbol: symbol.into(),
381            message: message.into(),
382            why: code.why().to_string(),
383            remediation: code.remediation().to_string(),
384        }
385    }
386
387    /// The finding as one line, the way the terminal and a CI annotation show it.
388    ///
389    /// ```
390    /// use majordomus_cli::quality::{Violation, ViolationCode};
391    /// let v = Violation::new(
392    ///     ViolationCode::RustPublicMissingExample,
393    ///     "majordomus_cli::graph::build",
394    ///     "apps/majordomus-cli/src/graph.rs",
395    ///     Some(42),
396    ///     "carries behaviour and no executable example",
397    /// );
398    /// assert_eq!(
399    ///     v.line_summary(),
400    ///     "apps/majordomus-cli/src/graph.rs:42: RUST_PUBLIC_MISSING_EXAMPLE majordomus_cli::graph::build — carries behaviour and no executable example"
401    /// );
402    /// ```
403    pub fn line_summary(&self) -> String {
404        let at = match self.line {
405            Some(l) => format!("{}:{l}", self.path),
406            None if self.path.is_empty() => String::new(),
407            None => self.path.clone(),
408        };
409        let head = if at.is_empty() {
410            String::new()
411        } else {
412            format!("{at}: ")
413        };
414        format!(
415            "{head}{} {} — {}",
416            self.code.as_str(),
417            self.symbol,
418            self.message
419        )
420    }
421}
422
423/// What the exported item surface looks like.
424#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
425pub struct PublicApiQuality {
426    /// Items the crate exports, of every kind.
427    pub items: usize,
428    /// Of those, how many carry documentation.
429    pub documented: usize,
430    /// How many owe an executable example under the policy.
431    pub owe_example: usize,
432    /// Of those, how many have one that counts.
433    pub exampled: usize,
434    /// How many are exempt from the example policy for a structural reason, and why, by
435    /// reason, so that the exemptions are visible rather than implied by a subtraction.
436    pub exempt: Vec<Exemption>,
437}
438
439/// A group of items the example policy does not apply to, with the reason it does not.
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
441pub struct Exemption {
442    /// The reason, as the policy names it.
443    pub reason: String,
444    /// How many items it covers.
445    pub items: usize,
446}
447
448/// What the exported module surface looks like.
449#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
450pub struct ModuleQuality {
451    /// Modules the crate exports, the crate root included.
452    pub modules: usize,
453    /// Of those, how many carry a `//!` header.
454    pub documented: usize,
455    /// How many carry an executable example in that header.
456    pub exampled: usize,
457    /// How many something exercises: an in-file test, or a test that names them.
458    pub behaviourally_tested: usize,
459}
460
461/// What the canonical operations look like against the transports that project them.
462#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
463pub struct OperationParity {
464    /// Executable capabilities in the registry.
465    pub canonical: usize,
466    /// Of those, how many declare a command-line projection.
467    pub cli: usize,
468    /// How many declare an HTTP route.
469    pub http: usize,
470    /// How many the OpenAPI document describes.
471    pub openapi: usize,
472    /// How many declare an MCP tool.
473    pub mcp: usize,
474    /// Commands of the command line, leaves and runnable parents.
475    pub cli_commands: usize,
476    /// Of those, how many are the projection of a capability.
477    pub cli_from_capability: usize,
478    /// How many are declared to belong to the command line alone, with a reason.
479    pub cli_local: usize,
480}
481
482/// One measurement of one crate, and everything found wrong in it.
483#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
484pub struct QualityReport {
485    /// [`SCHEMA`].
486    #[serde(default)]
487    pub schema: String,
488    /// What was measured: the crate directory, repository-relative.
489    pub target: String,
490    /// The exported item surface.
491    pub public_api: PublicApiQuality,
492    /// The exported module surface.
493    pub modules: ModuleQuality,
494    /// The canonical operations against their transports.
495    pub operations: OperationParity,
496    /// Every finding, errors first, then by file and line.
497    pub violations: Vec<Violation>,
498}
499
500impl QualityReport {
501    /// Nothing wrong at any severity that blocks?
502    ///
503    /// ```
504    /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
505    /// let mut r = QualityReport::default();
506    /// assert!(r.passes());
507    /// r.violations.push(Violation::new(
508    ///     ViolationCode::RustModuleMissingDocs, "m", "src/m.rs", Some(1), "no header"));
509    /// assert!(!r.passes());
510    /// ```
511    pub fn passes(&self) -> bool {
512        self.count(Severity::Error) == 0
513    }
514
515    /// How many findings of a severity.
516    pub fn count(&self, severity: Severity) -> usize {
517        self.violations
518            .iter()
519            .filter(|v| v.severity == severity)
520            .count()
521    }
522
523    /// The process exit code: `0` clean, `10` contract unmet, which is the code every
524    /// other unmet contract in this executable uses.
525    ///
526    /// ```
527    /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
528    /// let mut r = QualityReport::default();
529    /// assert_eq!(r.exit_code(), 0);
530    /// r.violations.push(Violation::new(
531    ///     ViolationCode::OperationCliUnclassified, "majordomus x", "", None, "unclassified"));
532    /// assert_eq!(r.exit_code(), 10);
533    /// ```
534    pub fn exit_code(&self) -> u8 {
535        if self.passes() {
536            0
537        } else {
538            10
539        }
540    }
541
542    /// Findings of one code, in report order.
543    ///
544    /// ```
545    /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
546    /// let mut r = QualityReport::default();
547    /// r.violations.push(Violation::new(
548    ///     ViolationCode::RustModuleMissingExample, "m", "src/m.rs", Some(1), "none"));
549    /// assert_eq!(r.of(ViolationCode::RustModuleMissingExample).len(), 1);
550    /// assert!(r.of(ViolationCode::RustModuleMissingDocs).is_empty());
551    /// ```
552    pub fn of(&self, code: ViolationCode) -> Vec<&Violation> {
553        self.violations.iter().filter(|v| v.code == code).collect()
554    }
555
556    /// Sort the findings the way every rendering shows them: errors first, then by file,
557    /// then by line, then by code, so that two runs over one tree print the same report.
558    pub fn sort(&mut self) {
559        self.violations.sort_by(|a, b| {
560            a.severity
561                .cmp(&b.severity)
562                .then_with(|| a.path.cmp(&b.path))
563                .then_with(|| a.line.cmp(&b.line))
564                .then_with(|| a.code.cmp(&b.code))
565                .then_with(|| a.symbol.cmp(&b.symbol))
566        });
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn every_code_names_a_rule_a_reason_and_a_remedy() {
576        for code in ViolationCode::all() {
577            assert!(code.rule().starts_with("project."), "{code:?}");
578            assert!(code.why().len() > 30, "{code:?} explains nothing");
579            assert!(
580                code.remediation().len() > 20,
581                "{code:?} tells nobody what to do"
582            );
583            // the code as text is the enum name in screaming snake, and round-trips
584            let json = serde_json::to_string(&code).unwrap();
585            assert_eq!(json, format!("\"{}\"", code.as_str()));
586            assert_eq!(serde_json::from_str::<ViolationCode>(&json).unwrap(), code);
587        }
588    }
589
590    #[test]
591    fn a_report_with_a_finding_fails_and_says_so_in_both_renderings() {
592        let mut r = QualityReport::default();
593        r.violations.push(Violation::new(
594            ViolationCode::RustPublicMissingExample,
595            "majordomus_cli::a::b",
596            "apps/majordomus-cli/src/a.rs",
597            Some(7),
598            "carries behaviour and no executable example",
599        ));
600        assert!(!r.passes());
601        assert_eq!(r.exit_code(), 10);
602        // the JSON and the human rendering agree about the outcome: never one without the other
603        let json = serde_json::to_value(&r).unwrap();
604        assert_eq!(json["violations"][0]["code"], "RUST_PUBLIC_MISSING_EXAMPLE");
605        assert_eq!(
606            json["violations"][0]["rule"],
607            "project.rust-public-api-quality@1"
608        );
609        assert!(r.violations[0].line_summary().contains("src/a.rs:7"));
610    }
611
612    #[test]
613    fn the_order_of_findings_does_not_depend_on_the_order_they_were_found() {
614        let v = |path: &str, line: usize| {
615            Violation::new(
616                ViolationCode::RustModuleMissingDocs,
617                "m",
618                path,
619                Some(line),
620                "none",
621            )
622        };
623        let mut a = QualityReport {
624            violations: vec![v("b.rs", 2), v("a.rs", 9), v("a.rs", 1)],
625            ..QualityReport::default()
626        };
627        let mut b = QualityReport {
628            violations: vec![v("a.rs", 1), v("b.rs", 2), v("a.rs", 9)],
629            ..QualityReport::default()
630        };
631        a.sort();
632        b.sort();
633        assert_eq!(a.violations, b.violations);
634        assert_eq!(a.violations[0].path, "a.rs");
635        assert_eq!(a.violations[0].line, Some(1));
636    }
637}