Skip to main content

ought_spec/
types.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6/// A parsed `.ought.md` spec file.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Spec {
9    pub name: String,
10    pub metadata: Metadata,
11    pub sections: Vec<Section>,
12    pub source_path: PathBuf,
13}
14
15/// Frontmatter metadata from the top of a spec file.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct Metadata {
18    pub context: Option<String>,
19    pub sources: Vec<String>,
20    pub schemas: Vec<String>,
21    pub requires: Vec<SpecRef>,
22}
23
24/// A reference to another spec file (from `requires:` or inline links).
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct SpecRef {
27    pub label: String,
28    pub path: PathBuf,
29    pub anchor: Option<String>,
30}
31
32/// A section within a spec (maps to markdown headings).
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Section {
35    pub title: String,
36    pub depth: u8,
37    pub prose: String,
38    pub clauses: Vec<Clause>,
39    pub subsections: Vec<Section>,
40}
41
42/// The core IR type — a single testable clause.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Clause {
45    pub id: ClauseId,
46    pub keyword: Keyword,
47    pub severity: Severity,
48    pub text: String,
49    pub condition: Option<String>,
50    pub otherwise: Vec<Clause>,
51    pub temporal: Option<Temporal>,
52    pub hints: Vec<String>,
53    pub source_location: SourceLocation,
54    pub content_hash: String,
55    /// Clause is declared with a `PENDING` prefix: the author has committed to
56    /// the obligation strength but the implementation is deferred. The
57    /// generator must skip pending clauses and the runner reports them as
58    /// `pending` rather than passed/failed/skipped.
59    #[serde(default)]
60    pub pending: bool,
61}
62
63/// Stable identifier for a clause, derived from section path + clause text.
64/// e.g. `auth::login::must_return_jwt`
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub struct ClauseId(pub String);
67
68impl std::fmt::Display for ClauseId {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.write_str(&self.0)
71    }
72}
73
74/// Deontic keyword — the operator on a clause.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76pub enum Keyword {
77    Must,
78    MustNot,
79    Should,
80    ShouldNot,
81    May,
82    Wont,
83    Given,
84    Otherwise,
85    MustAlways,
86    MustBy,
87}
88
89/// Severity level derived from the keyword.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
91pub enum Severity {
92    Required,
93    Recommended,
94    Optional,
95    NegativeConfirmation,
96}
97
98impl Keyword {
99    pub fn severity(self) -> Severity {
100        match self {
101            Keyword::Must | Keyword::MustNot | Keyword::MustAlways | Keyword::MustBy => {
102                Severity::Required
103            }
104            Keyword::Should | Keyword::ShouldNot => Severity::Recommended,
105            Keyword::May => Severity::Optional,
106            Keyword::Wont => Severity::NegativeConfirmation,
107            Keyword::Given | Keyword::Otherwise => Severity::Required,
108        }
109    }
110}
111
112/// Temporal qualifier for MUST ALWAYS and MUST BY clauses.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub enum Temporal {
115    /// Must hold across all states/inputs. Generates property-based tests.
116    Invariant,
117    /// Must complete within the given duration. Generates timed assertions.
118    Deadline(Duration),
119}
120
121/// Location of a clause in a source file.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SourceLocation {
124    pub file: PathBuf,
125    pub line: usize,
126}
127
128/// An error encountered during parsing.
129#[derive(Debug, Clone, thiserror::Error)]
130#[error("{file}:{line}: {message}")]
131pub struct ParseError {
132    pub file: PathBuf,
133    pub line: usize,
134    pub message: String,
135}