use serde::Serialize;
use speccoder_core::snapshot::ProjectSnapshot;
pub const PART_LIMIT: usize = 32 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Bundle {
pub feature: String,
pub constitution: String,
pub plan: String,
pub story: String,
pub task: TaskContext,
pub dependencies: Vec<DependencyContext>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TaskContext {
pub id: String,
pub title: String,
pub phase: String,
pub subsection: String,
pub story: String,
pub parallel: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DependencyContext {
pub id: String,
pub title: String,
pub done: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BundleError {
NoFeature,
NoTasks,
NotFound(String),
}
pub fn bundle(snapshot: &ProjectSnapshot, task_id: &str) -> Result<Bundle, BundleError> {
crate::format::reference().bundle(snapshot, task_id)
}
pub(crate) fn cap(text: &str) -> String {
if text.len() <= PART_LIMIT {
return text.to_owned();
}
let mut end = PART_LIMIT;
while !text.is_char_boundary(end) {
end -= 1;
}
format!(
"{}\n[… cut at {PART_LIMIT} bytes of {} …]",
&text[..end],
text.len()
)
}
#[derive(Debug, Default, Clone)]
pub struct Manifest {
entries: Vec<speccoder_core::context::ContextEntry>,
}
impl Manifest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(
&mut self,
kind: speccoder_core::context::ContextKind,
source: &str,
reference: &str,
bytes: &[u8],
) -> &mut Self {
if bytes.is_empty() {
return self;
}
self.entries.push(speccoder_core::context::ContextEntry {
kind,
source: source.to_owned(),
reference: reference.to_owned(),
sha256: crate::integrity::sha256_hex(bytes),
bytes: u64::try_from(bytes.len()).unwrap_or(u64::MAX),
cut: bytes.len() > PART_LIMIT,
});
self
}
#[must_use]
pub fn finish(&self) -> speccoder_core::context::ContextManifest {
speccoder_core::context::ContextManifest::of(
self.entries.clone(),
crate::integrity::sha256_hex,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_manifest_recomputes_from_what_it_lists() {
use speccoder_core::context::ContextKind;
let mut m = Manifest::new();
m.push(ContextKind::Issue, "github:o/r#42", "#42", b"title\nbody")
.push(ContextKind::Story, "person", "/triad", b"C\tx\n")
.push(ContextKind::Plan, "worktree", "/bundle/plan", b"");
let done = m.finish();
assert_eq!(done.entries.len(), 2, "an empty part is not listed");
let again = speccoder_core::context::ContextManifest::of(
done.entries.clone(),
crate::integrity::sha256_hex,
);
assert_eq!(again.digest, done.digest);
assert_eq!(
done.entries[0].sha256,
crate::integrity::sha256_hex(b"C\tx\n")
);
}
#[test]
fn cap_marks_the_cut() {
let long = "é".repeat(PART_LIMIT);
let capped = cap(&long);
assert!(capped.len() < long.len());
assert!(
capped.contains("[… cut at 32768 bytes of 65536 …]"),
"{}",
&capped[capped.len() - 40..]
);
}
}