1use serde::{Deserialize, Serialize};
9
10use crate::actions::Via;
11
12#[derive(
14 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
15)]
16#[serde(rename_all = "snake_case")]
17pub enum RunMode {
18 #[default]
21 Ask,
22 Autopilot,
25}
26
27impl RunMode {
28 #[must_use]
30 pub const fn as_str(self) -> &'static str {
31 match self {
32 Self::Ask => "ask",
33 Self::Autopilot => "autopilot",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
40#[serde(rename_all = "snake_case")]
41pub enum OracleOp {
42 Write,
45 Reject,
47 Again,
49 Edit,
51 Reading,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
57pub enum Reading {
58 A,
60 B,
62}
63
64impl Reading {
65 #[must_use]
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::A => "A",
70 Self::B => "B",
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
77pub struct Expectation {
78 pub scenario: String,
80 pub given: String,
82 pub expect: String,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
88pub struct Untestable {
89 pub scenario: String,
91 pub why: String,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
97pub struct ReadingText {
98 pub given: String,
100 pub expect: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
106pub struct Resolution {
107 pub reading: Reading,
109 pub by: String,
111 pub at: u64,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
117pub struct Ambiguity {
118 pub scenario: String,
120 pub a: ReadingText,
122 pub b: ReadingText,
124 pub resolution: Option<Resolution>,
126}
127
128impl Ambiguity {
129 #[must_use]
131 pub const fn open(&self) -> bool {
132 self.resolution.is_none()
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
138#[serde(rename_all = "snake_case")]
139pub enum AuthoredBy {
140 Model,
142 Person,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
148pub struct OracleTest {
149 pub path: String,
151 pub content: String,
153 pub scenarios: Vec<String>,
155 pub red_class: String,
158 pub red: bool,
160 pub authored_by: AuthoredBy,
162 pub edited_by: Option<String>,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
168pub struct OracleDiscarded {
169 pub path: String,
171 pub why: String,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
177pub struct OracleRejected {
178 pub path: String,
180 pub reason: String,
182 pub by: String,
184 pub at: u64,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
191pub struct HeldOutSummary {
192 pub state: String,
194 pub count: usize,
196 pub manifest_sha256: Option<String>,
198 pub reason: Option<String>,
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
204#[serde(rename_all = "snake_case")]
205pub enum OracleState {
206 None,
208 Written,
210 NotRed,
212 Failed,
214 Ambiguous,
216 Frozen,
218 Lapsed,
220 FrozenElsewhere,
222}
223
224impl OracleState {
225 #[must_use]
227 pub const fn as_str(self) -> &'static str {
228 match self {
229 Self::None => "none",
230 Self::Written => "written",
231 Self::NotRed => "not_red",
232 Self::Failed => "failed",
233 Self::Ambiguous => "ambiguous",
234 Self::Frozen => "frozen",
235 Self::Lapsed => "lapsed",
236 Self::FrozenElsewhere => "frozen_elsewhere",
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
243pub struct OracleView {
244 pub req: String,
246 pub title: String,
248 pub scenarios: Vec<crate::spec::ScenarioRef>,
250 pub state: OracleState,
252 pub frozen_on: Option<String>,
254 pub frozen_by: Option<String>,
256 pub frozen_at: Option<u64>,
258 pub tests: Vec<OracleTest>,
260 pub discarded: Vec<OracleDiscarded>,
262 pub rejected: Vec<OracleRejected>,
264 pub uncovered: Vec<String>,
266 pub untestable: Vec<Untestable>,
268 pub ambiguities: Vec<Ambiguity>,
270 pub held_out: HeldOutSummary,
272 pub strength_target: Option<u8>,
274 pub calls: u8,
276 pub ambiguity_check: bool,
278 pub manifest_sha256: Option<String>,
280 pub error: Option<String>,
282}
283
284impl OracleView {
285 pub fn open_ambiguities(&self) -> impl Iterator<Item = &Ambiguity> {
287 self.ambiguities.iter().filter(|a| a.open())
288 }
289
290 #[must_use]
292 pub fn freezes(&self) -> String {
293 let red = self.tests.iter().filter(|t| t.red).count();
294 if let Some(a) = self.open_ambiguities().next() {
295 return format!("blocked: two readings of {}", a.scenario);
296 }
297 match self.state {
298 OracleState::Frozen => {
299 format!("{red} {} frozen", if red == 1 { "test" } else { "tests" })
300 }
301 _ if red == 0 => "tests written at run time".to_owned(),
302 _ => {
303 let mut parts = vec![format!("{red} {}", if red == 1 { "test" } else { "tests" })];
304 if self.held_out.state == "kept" {
305 parts.push(format!("{} held out", self.held_out.count));
306 }
307 if let Some(t) = self.strength_target {
308 parts.push(format!("target {t}%"));
309 }
310 parts.join(" · ")
311 }
312 }
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
318pub struct ReviewApproval {
319 pub sha256: String,
322 pub mode: RunMode,
324 pub by: String,
326 pub via: Via,
328 pub at: u64,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
335pub struct OfferedGrant {
336 pub id: String,
338 pub class: String,
340 pub object: String,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
347pub struct ApproveSheet {
348 #[serde(default)]
352 pub sha256: Option<String>,
353 pub approves: Vec<String>,
355 pub blocked: Vec<String>,
358 pub autopilot_blocked: Option<String>,
360 pub sandbox: Option<String>,
362 pub grants: Vec<OfferedGrant>,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
368pub struct AutopilotDenial {
369 pub task: String,
371 pub turn: u32,
373 pub class: String,
375 pub object: String,
377 pub reason: String,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
383#[serde(tag = "kind", rename_all = "snake_case")]
384pub enum CommentAnchor {
385 Passage {
387 path: String,
389 start_line: usize,
391 end_line: usize,
393 quote: String,
395 #[serde(default)]
397 quote_sha256: String,
398 #[serde(default)]
400 section: String,
401 },
402 DiffLine {
404 path: String,
406 side: String,
408 line: usize,
410 quote: String,
412 },
413 TestLine {
415 req: String,
417 path: String,
419 line: usize,
421 quote: String,
423 },
424 Finding {
426 source: String,
428 id: String,
430 text: String,
432 },
433}
434
435impl CommentAnchor {
436 #[must_use]
438 pub fn quote(&self) -> &str {
439 match self {
440 Self::Passage { quote, .. }
441 | Self::DiffLine { quote, .. }
442 | Self::TestLine { quote, .. } => quote,
443 Self::Finding { text, .. } => text,
444 }
445 }
446
447 #[must_use]
449 pub fn place(&self) -> String {
450 match self {
451 Self::Passage {
452 path,
453 start_line,
454 end_line,
455 ..
456 } if start_line == end_line => format!("{path}:{start_line}"),
457 Self::Passage {
458 path,
459 start_line,
460 end_line,
461 ..
462 } => format!("{path}:{start_line}-{end_line}"),
463 Self::DiffLine {
464 path, side, line, ..
465 } => format!("{path}:{line} ({side})"),
466 Self::TestLine {
467 req, path, line, ..
468 } => format!("{req} {path}:{line}"),
469 Self::Finding { source, id, .. } => format!("{source} {id}"),
470 }
471 }
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
476#[serde(rename_all = "snake_case")]
477pub enum DraftState {
478 Draft,
480 Outdated,
482 Sent,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
488pub struct CommentDraft {
489 pub id: String,
491 pub anchor: CommentAnchor,
493 pub text: String,
495 pub by: String,
497 pub at: u64,
499 pub state: DraftState,
501 #[serde(default)]
503 pub kept: bool,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
508pub struct RevisionView {
509 pub id: String,
511 pub comments: Vec<String>,
513 pub sections: Vec<String>,
515 pub skill: String,
517 pub changed: Vec<String>,
519 pub outside: Vec<String>,
521 pub outcome: String,
523 pub files: Vec<String>,
525 #[serde(default)]
527 pub asked_again: Vec<String>,
528 #[serde(default)]
531 pub beside: Vec<String>,
532 #[serde(default)]
535 pub diffs: Vec<SectionDiff>,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
541pub struct BugTests {
542 pub task: String,
544 pub title: String,
546 pub confirmation: crate::bug::Confirmation,
548 pub reproductions: Vec<BugTest>,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
554pub struct BugTest {
555 pub statement: String,
557 pub file: String,
559 pub test: String,
561 pub red_class: String,
563 pub content: Option<String>,
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
571pub struct SectionDiff {
572 pub path: String,
574 pub heading: String,
576 pub before: String,
578 pub after: String,
580 pub outside: bool,
582}
583
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
586pub struct CommentsView {
587 pub feature: String,
589 pub drafts: Vec<CommentDraft>,
591 pub turn: Option<RevisionView>,
593 pub revise_with: Option<String>,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
599pub struct CodeComment {
600 pub path: String,
602 pub line: usize,
604 pub quote: String,
606 pub text: String,
608}
609
610impl CodeComment {
611 #[must_use]
613 pub fn nudge_line(&self) -> String {
614 let quote: String = self.quote.trim().chars().take(80).collect();
615 format!(
616 "{}:{} \"{}\" — {}",
617 self.path,
618 self.line,
619 quote,
620 self.text.trim()
621 )
622 }
623}
624
625#[must_use]
628pub fn normalise(text: &str) -> String {
629 text.split_whitespace()
630 .collect::<Vec<_>>()
631 .join(" ")
632 .to_lowercase()
633}
634
635#[must_use]
640pub fn compare(
641 a: &[Expectation],
642 a_untestable: &[Untestable],
643 b: &[Expectation],
644 b_untestable: &[Untestable],
645) -> Vec<Ambiguity> {
646 let mut out: Vec<Ambiguity> = Vec::new();
647 let mut push = |scenario: &str, ra: ReadingText, rb: ReadingText| {
648 if !out.iter().any(|x| x.scenario == scenario) {
649 out.push(Ambiguity {
650 scenario: scenario.to_owned(),
651 a: ra,
652 b: rb,
653 resolution: None,
654 });
655 }
656 };
657 let reading = |e: &Expectation| ReadingText {
658 given: e.given.clone(),
659 expect: e.expect.clone(),
660 };
661 let untestable = |u: &Untestable| ReadingText {
662 given: String::new(),
663 expect: format!("untestable: {}", u.why),
664 };
665 for ea in a {
666 for eb in b.iter().filter(|eb| eb.scenario == ea.scenario) {
667 if normalise(&ea.given) == normalise(&eb.given)
668 && normalise(&ea.expect) != normalise(&eb.expect)
669 {
670 push(&ea.scenario, reading(ea), reading(eb));
671 }
672 }
673 if let Some(u) = b_untestable.iter().find(|u| u.scenario == ea.scenario) {
674 push(&ea.scenario, reading(ea), untestable(u));
675 }
676 }
677 for eb in b {
678 if let Some(u) = a_untestable.iter().find(|u| u.scenario == eb.scenario) {
679 push(&eb.scenario, untestable(u), reading(eb));
680 }
681 }
682 out
683}
684
685#[cfg(test)]
686mod tests {
687 use proptest::prelude::*;
688
689 use super::*;
690
691 fn e(s: &str, given: &str, expect: &str) -> Expectation {
692 Expectation {
693 scenario: s.to_owned(),
694 given: given.to_owned(),
695 expect: expect.to_owned(),
696 }
697 }
698
699 #[test]
700 fn the_same_given_with_another_outcome_is_ambiguous() {
701 let a = [
702 e("US1-1", "name Ada", "Hello, Ada!"),
703 e("US1-2", "\"\"", "Hello!"),
704 ];
705 let b = [
706 e("US1-1", " Name ada ", "hello, ada!"),
707 e("US1-2", "\"\"", "an error"),
708 ];
709 let found = compare(&a, &[], &b, &[]);
710 assert_eq!(found.len(), 1);
711 assert_eq!(found[0].scenario, "US1-2");
712 assert_eq!(found[0].a.expect, "Hello!");
713 assert_eq!(found[0].b.expect, "an error");
714 assert!(found[0].open());
715 }
716
717 #[test]
718 fn different_givens_are_different_examples() {
719 let a = [e("US1-1", "name Ada", "Hello, Ada!")];
720 let b = [e("US1-1", "name Bob", "Hello, Bob!")];
721 assert!(compare(&a, &[], &b, &[]).is_empty());
722 }
723
724 #[test]
725 fn covered_by_one_and_untestable_for_the_other_is_ambiguous() {
726 let a = [e("US1-3", "offline", "an error")];
727 let u = [Untestable {
728 scenario: "US1-3".to_owned(),
729 why: "needs a network".to_owned(),
730 }];
731 let found = compare(&a, &[], &[], &u);
732 assert_eq!(found.len(), 1);
733 assert!(found[0].b.expect.starts_with("untestable"));
734 let back = compare(&[], &u, &a, &[]);
735 assert_eq!(back.len(), 1);
736 assert!(back[0].a.expect.starts_with("untestable"));
737 }
738
739 proptest! {
740 #[test]
741 fn agreeing_authors_raise_nothing(given in "[a-zA-Z ]{1,20}", expect in "[a-zA-Z ]{1,20}", pad in " {0,3}") {
742 let a = [e("S", &given, &expect)];
743 let b = [e("S", &format!("{pad}{}{pad}", given.to_uppercase()), &expect.to_lowercase())];
744 prop_assert!(compare(&a, &[], &b, &[]).is_empty());
745 }
746
747 #[test]
748 fn the_rule_is_symmetric_in_whether_it_fires(g in "[a-c]{1,3}", x in "[a-c]{1,3}", y in "[a-c]{1,3}") {
749 let a = [e("S", &g, &x)];
750 let b = [e("S", &g, &y)];
751 prop_assert_eq!(compare(&a, &[], &b, &[]).len(), compare(&b, &[], &a, &[]).len());
752 prop_assert_eq!(compare(&a, &[], &b, &[]).is_empty(), normalise(&x) == normalise(&y));
753 }
754 }
755
756 #[test]
757 fn the_sheet_says_what_approval_freezes() {
758 let mut v = OracleView {
759 req: "US1".to_owned(),
760 title: "Greet".to_owned(),
761 scenarios: Vec::new(),
762 state: OracleState::Written,
763 frozen_on: None,
764 frozen_by: None,
765 frozen_at: None,
766 tests: vec![OracleTest {
767 path: "tests/a.rs".to_owned(),
768 content: String::new(),
769 scenarios: vec!["US1-1".to_owned()],
770 red_class: "assertion".to_owned(),
771 red: true,
772 authored_by: AuthoredBy::Model,
773 edited_by: None,
774 }],
775 discarded: Vec::new(),
776 rejected: Vec::new(),
777 uncovered: Vec::new(),
778 untestable: Vec::new(),
779 ambiguities: Vec::new(),
780 held_out: HeldOutSummary {
781 state: "kept".to_owned(),
782 count: 2,
783 manifest_sha256: None,
784 reason: None,
785 },
786 strength_target: Some(80),
787 calls: 2,
788 ambiguity_check: true,
789 manifest_sha256: None,
790 error: None,
791 };
792 assert_eq!(v.freezes(), "1 test · 2 held out · target 80%");
793 v.tests[0].red = false;
794 assert_eq!(v.freezes(), "tests written at run time");
795 let line = CodeComment {
796 path: "src/a.rs".to_owned(),
797 line: 12,
798 quote: " let x = 1;".to_owned(),
799 text: " use a constant ".to_owned(),
800 };
801 assert_eq!(
802 line.nudge_line(),
803 "src/a.rs:12 \"let x = 1;\" — use a constant"
804 );
805 }
806}