use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct ProjectSnapshot {
pub root: String,
pub speckit: SpeckitMeta,
pub constitution: ConstitutionState,
pub features: Vec<FeatureRef>,
pub selection: Selection,
pub feature: Option<FeatureSnapshot>,
pub diagnostics: Vec<Diagnostic>,
#[serde(skip_serializing_if = "crate::spec::FormatKind::is_reference")]
pub format: crate::spec::FormatKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub openspec: Option<crate::spec::OpenSpecMeta>,
}
impl ProjectSnapshot {
#[must_use]
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| d.severity == Severity::Error)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct SpeckitMeta {
pub version: Option<String>,
pub integration: Option<String>,
pub script: Option<String>,
pub feature_numbering: Option<String>,
pub current_feature: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ConstitutionState {
Present,
Unfilled,
Absent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct FeatureRef {
pub dir: String,
pub name: String,
pub prefix: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Selection {
pub reason: SelectionReason,
pub feature: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SelectionReason {
Argument,
Environment,
Recorded,
OnlyFeature,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct FeatureSnapshot {
pub feature: FeatureRef,
pub artifacts: Artifacts,
pub tasks: Option<ParsedTasks>,
pub schedule: Option<Schedule>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[allow(clippy::struct_excessive_bools)] pub struct Artifacts {
pub spec: bool,
pub plan: bool,
pub tasks: bool,
pub research: bool,
pub data_model: bool,
pub contracts: bool,
pub quickstart: bool,
pub checklists: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct ParsedTasks {
pub path: String,
pub phases: Vec<Phase>,
pub tasks: Vec<Task>,
pub totals: Totals,
pub dependency_mode: DependencyMode,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DependencyMode {
#[default]
Structural,
Declared,
Chain,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Phase {
pub title: String,
pub line: u32,
pub section_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Task {
pub id: String,
pub done: bool,
pub parallel: bool,
pub story: Option<String>,
pub title: String,
pub depends_on: Vec<String>,
pub phase: Option<String>,
pub subsection: Option<String>,
pub line: u32,
pub byte_range: [usize; 2],
pub line_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Schedule {
pub tasks: Vec<ScheduledTask>,
pub totals: ScheduleTotals,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct ScheduledTask {
pub id: String,
pub state: TaskState,
pub depends_on: Vec<Dependency>,
pub blocked_by: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TaskState {
Done,
Ready,
Blocked,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Dependency {
pub id: String,
pub kind: DependencyKind,
pub missing: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DependencyKind {
Structural,
Explicit,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct ScheduleTotals {
pub done: usize,
pub ready: usize,
pub blocked: usize,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Totals {
pub total: usize,
pub done: usize,
pub open: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct Diagnostic {
pub severity: Severity,
pub code: String,
pub path: Option<String>,
pub line: Option<u32>,
pub message: String,
pub hint: Option<String>,
}
impl Diagnostic {
#[must_use]
pub fn new(severity: Severity, code: &str, message: impl Into<String>) -> Self {
Self {
severity,
code: code.to_owned(),
path: None,
line: None,
message: message.into(),
hint: None,
}
}
#[must_use]
pub fn at(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
#[must_use]
pub fn line(mut self, line: u32) -> Self {
self.line = Some(line);
self
}
#[must_use]
pub fn hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Error,
Warning,
Info,
}
#[must_use]
pub fn closest<'f>(features: &'f [FeatureRef], name: &str) -> Option<&'f FeatureRef> {
let slug = |s: &str| {
s.rsplit('/')
.next()
.unwrap_or(s)
.trim_start_matches(|c: char| c.is_ascii_digit() || c == '-')
.to_lowercase()
};
let target = slug(name);
features
.iter()
.map(|f| (distance(&slug(&f.name), &target), f))
.filter(|(d, f)| *d <= slug(&f.name).len().max(target.len()) / 2)
.min_by_key(|(d, _)| *d)
.map(|(_, f)| f)
}
fn distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut prev = row[0];
row[0] = i + 1;
for (j, cb) in b.iter().enumerate() {
let next = (prev + usize::from(ca != *cb))
.min(row[j] + 1)
.min(row[j + 1] + 1);
prev = row[j + 1];
row[j + 1] = next;
}
}
row[b.len()]
}