1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//! The format-neutral view of spec-driven work: which format
//! a project is written in, and the requirements and acceptance scenarios a
//! unit of work states. The Spec Kit and OpenSpec adapters produce it;
//! the judge reads only it.
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
/// A spec-driven format.
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum FormatKind {
/// GitHub Spec Kit (`.specify/`, `specs/NNN-name/`).
#[default]
Speckit,
/// OpenSpec (`openspec/changes/<id>/`).
Openspec,
/// Kiro and cc-sdd (`.kiro/specs/<name>/`).
Kiro,
}
impl FormatKind {
/// Every format, in the order detection reports them.
pub const ALL: [Self; 3] = [Self::Speckit, Self::Openspec, Self::Kiro];
/// The name in `--format` and `[spec] format`.
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Speckit => "speckit",
Self::Openspec => "openspec",
Self::Kiro => "kiro",
}
}
/// The name a person reads.
#[must_use]
pub const fn title(self) -> &'static str {
match self {
Self::Speckit => "Spec Kit",
Self::Openspec => "OpenSpec",
Self::Kiro => "Kiro",
}
}
}
impl FormatKind {
/// Whether this is the reference format (Spec Kit).
#[must_use]
pub fn is_reference(&self) -> bool {
*self == Self::Speckit
}
}
impl fmt::Display for FormatKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
impl FromStr for FormatKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|k| k.name() == s)
.ok_or_else(|| format!("`{s}` is not a format (speckit, openspec or kiro)"))
}
}
/// Where a part of an artifact is: its file and byte range.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SourceRange {
/// Path relative to the project root.
pub path: String,
/// First byte.
pub start: usize,
/// One past the last byte.
pub end: usize,
}
/// One acceptance scenario of a requirement.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Scenario {
/// Stable within the unit: `US1/AC2`, `greeting/Default greeting/No name given`, `1.2`.
pub key: String,
/// Its text as written.
pub raw: String,
/// Where it is.
pub source: SourceRange,
}
/// A scenario a task's acceptance tests cite: `US1-2` (Spec Kit), or
/// `capability: requirement / scenario` (OpenSpec), with the spec's words.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ScenarioRef {
/// Stable within the unit.
pub id: String,
/// The scenario as written in the spec.
pub text: String,
}
/// One requirement (a Spec Kit user story, an OpenSpec requirement, a Kiro
/// requirement) and its scenarios.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
pub struct Requirement {
/// Stable within the unit: `US1`, `greeting/Default greeting`, `3`.
pub key: String,
/// Its title.
pub title: String,
/// Its whole text.
pub text: String,
/// Its acceptance scenarios, in document order.
pub scenarios: Vec<Scenario>,
/// Where it is.
pub source: SourceRange,
}
/// What an OpenSpec project adds to its snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct OpenSpecMeta {
/// The workflow schema (`spec-driven` is the one speccoder runs).
pub schema: String,
/// `context` of `openspec/config.yaml`, if any.
pub context: Option<String>,
/// What a unit of work is called.
pub unit_kind: String,
/// The selected change's delta requirements, in file order.
pub requirements: Vec<DeltaRequirement>,
}
/// One requirement of a change's spec delta.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct DeltaRequirement {
/// The capability (`openspec/specs/<capability>/spec.md`).
pub capability: String,
/// `added`, `modified`, `removed` or `renamed`.
pub operation: String,
/// The requirement's name (the new name for a rename).
pub name: String,
/// Its scenarios' names.
pub scenarios: Vec<String>,
/// The delta file, relative to the project root.
pub path: String,
/// Its heading's line.
pub line: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_parse_and_serialize_by_name() {
for k in FormatKind::ALL {
assert_eq!(k.name().parse::<FormatKind>(), Ok(k));
assert_eq!(serde_json::to_value(k).unwrap_or_default(), k.name());
}
let err = "bogus".parse::<FormatKind>().unwrap_err();
assert_eq!(err, "`bogus` is not a format (speckit, openspec or kiro)");
}
}