1use serde::Serialize;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
12pub struct ProjectSnapshot {
13 pub root: String,
15 pub speckit: SpeckitMeta,
17 pub constitution: ConstitutionState,
19 pub features: Vec<FeatureRef>,
21 pub selection: Selection,
23 pub feature: Option<FeatureSnapshot>,
25 pub diagnostics: Vec<Diagnostic>,
27 #[serde(skip_serializing_if = "crate::spec::FormatKind::is_reference")]
29 pub format: crate::spec::FormatKind,
30 #[serde(skip_serializing_if = "Option::is_none")]
33 pub openspec: Option<crate::spec::OpenSpecMeta>,
34}
35
36impl ProjectSnapshot {
37 #[must_use]
39 pub fn has_errors(&self) -> bool {
40 self.diagnostics
41 .iter()
42 .any(|d| d.severity == Severity::Error)
43 }
44}
45
46#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
48pub struct SpeckitMeta {
49 pub version: Option<String>,
51 pub integration: Option<String>,
53 pub script: Option<String>,
55 pub feature_numbering: Option<String>,
57 pub current_feature: Option<String>,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
63#[serde(rename_all = "snake_case")]
64pub enum ConstitutionState {
65 Present,
67 Unfilled,
69 Absent,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
75pub struct FeatureRef {
76 pub dir: String,
78 pub name: String,
80 pub prefix: String,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
86pub struct Selection {
87 pub reason: SelectionReason,
89 pub feature: Option<String>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
95#[serde(rename_all = "snake_case")]
96pub enum SelectionReason {
97 Argument,
99 Environment,
101 Recorded,
103 OnlyFeature,
105 None,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
111pub struct FeatureSnapshot {
112 pub feature: FeatureRef,
114 pub artifacts: Artifacts,
116 pub tasks: Option<ParsedTasks>,
118 pub schedule: Option<Schedule>,
120}
121
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
124#[allow(clippy::struct_excessive_bools)] pub struct Artifacts {
126 pub spec: bool,
128 pub plan: bool,
130 pub tasks: bool,
132 pub research: bool,
134 pub data_model: bool,
136 pub contracts: bool,
138 pub quickstart: bool,
140 pub checklists: bool,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
146pub struct ParsedTasks {
147 pub path: String,
149 pub phases: Vec<Phase>,
151 pub tasks: Vec<Task>,
153 pub totals: Totals,
155 pub dependency_mode: DependencyMode,
157}
158
159#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
161#[serde(rename_all = "snake_case")]
162pub enum DependencyMode {
163 #[default]
165 Structural,
166 Declared,
169 Chain,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
176pub struct Phase {
177 pub title: String,
179 pub line: u32,
181 pub section_hash: String,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
187pub struct Task {
188 pub id: String,
190 pub done: bool,
192 pub parallel: bool,
194 pub story: Option<String>,
196 pub title: String,
198 pub depends_on: Vec<String>,
200 pub phase: Option<String>,
202 pub subsection: Option<String>,
204 pub line: u32,
206 pub byte_range: [usize; 2],
208 pub line_hash: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
214pub struct Schedule {
215 pub tasks: Vec<ScheduledTask>,
217 pub totals: ScheduleTotals,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
223pub struct ScheduledTask {
224 pub id: String,
226 pub state: TaskState,
228 pub depends_on: Vec<Dependency>,
230 pub blocked_by: Vec<String>,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
236#[serde(rename_all = "snake_case")]
237pub enum TaskState {
238 Done,
240 Ready,
242 Blocked,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
248pub struct Dependency {
249 pub id: String,
251 pub kind: DependencyKind,
253 pub missing: bool,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
259#[serde(rename_all = "snake_case")]
260pub enum DependencyKind {
261 Structural,
263 Explicit,
265}
266
267#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
269pub struct ScheduleTotals {
270 pub done: usize,
272 pub ready: usize,
274 pub blocked: usize,
276}
277
278#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
280pub struct Totals {
281 pub total: usize,
283 pub done: usize,
285 pub open: usize,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
291pub struct Diagnostic {
292 pub severity: Severity,
294 pub code: String,
296 pub path: Option<String>,
298 pub line: Option<u32>,
300 pub message: String,
302 pub hint: Option<String>,
304}
305
306impl Diagnostic {
307 #[must_use]
309 pub fn new(severity: Severity, code: &str, message: impl Into<String>) -> Self {
310 Self {
311 severity,
312 code: code.to_owned(),
313 path: None,
314 line: None,
315 message: message.into(),
316 hint: None,
317 }
318 }
319
320 #[must_use]
322 pub fn at(mut self, path: impl Into<String>) -> Self {
323 self.path = Some(path.into());
324 self
325 }
326
327 #[must_use]
329 pub fn line(mut self, line: u32) -> Self {
330 self.line = Some(line);
331 self
332 }
333
334 #[must_use]
336 pub fn hint(mut self, hint: impl Into<String>) -> Self {
337 self.hint = Some(hint.into());
338 self
339 }
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, schemars::JsonSchema)]
344#[serde(rename_all = "snake_case")]
345pub enum Severity {
346 Error,
348 Warning,
350 Info,
352}
353
354#[must_use]
357pub fn closest<'f>(features: &'f [FeatureRef], name: &str) -> Option<&'f FeatureRef> {
358 let slug = |s: &str| {
359 s.rsplit('/')
360 .next()
361 .unwrap_or(s)
362 .trim_start_matches(|c: char| c.is_ascii_digit() || c == '-')
363 .to_lowercase()
364 };
365 let target = slug(name);
366 features
367 .iter()
368 .map(|f| (distance(&slug(&f.name), &target), f))
369 .filter(|(d, f)| *d <= slug(&f.name).len().max(target.len()) / 2)
370 .min_by_key(|(d, _)| *d)
371 .map(|(_, f)| f)
372}
373
374fn distance(a: &str, b: &str) -> usize {
376 let b: Vec<char> = b.chars().collect();
377 let mut row: Vec<usize> = (0..=b.len()).collect();
378 for (i, ca) in a.chars().enumerate() {
379 let mut prev = row[0];
380 row[0] = i + 1;
381 for (j, cb) in b.iter().enumerate() {
382 let next = (prev + usize::from(ca != *cb))
383 .min(row[j] + 1)
384 .min(row[j + 1] + 1);
385 prev = row[j + 1];
386 row[j + 1] = next;
387 }
388 }
389 row[b.len()]
390}