Skip to main content

speccoder_runtime/
quick.rs

1//! `speccoder do "…"`: a small change without a planning ceremony,
2//! and without skipping the evidence. The request becomes a story, a
3//! requirement (approved: the person wrote it) and a task in the format's
4//! standing unit for quick changes; those planning files are committed;
5//! then the task runs like any other — worktree, failing acceptance tests
6//! first, checks, a verdict, a commit with its evidence.
7
8use std::path::Path;
9
10use speccoder_core::{CommandResult, CommandStatus};
11
12/// Records `request` (and its acceptance `checks`) and, when `run`, runs it.
13#[must_use]
14pub fn change(
15    path: &Path,
16    request: &str,
17    checks: &[String],
18    run: bool,
19    via: &str,
20    options: &crate::RunOptions,
21) -> CommandResult {
22    let format = crate::format::at(path);
23    let Some(root) = format.detect(path) else {
24        return CommandResult::new(
25            "do",
26            CommandStatus::NotAProject,
27            &(format.messages().not_a_project)(path),
28        )
29        .with_code("NO-PROJECT");
30    };
31    let rec = match format.record_change(&root, request, checks) {
32        Ok(r) => r,
33        Err(e) => {
34            return CommandResult::new("do", CommandStatus::UsageError, &e).with_code("USAGE");
35        }
36    };
37    // The person's own words: approved as they wrote them.
38    let approved = crate::review::decide(
39        &root,
40        Some(&rec.feature),
41        std::slice::from_ref(&rec.requirement),
42        "approve",
43        None,
44        via,
45    );
46    if approved.status() != CommandStatus::Ok {
47        return CommandResult::carried("do", &approved);
48    }
49    // A run reads committed files: exactly this unit's planning files.
50    let files = crate::hosting::uncommitted(&root, Some(&rec.feature));
51    if !files.is_empty() {
52        let committed = crate::hosting::commit_planning_as(
53            &root,
54            Some(&rec.feature),
55            &files,
56            Some(&format!("quick change: {}", rec.title)),
57        );
58        if committed.status() != CommandStatus::Ok {
59            return CommandResult::carried("do", &committed);
60        }
61    }
62    let said = format!(
63        "recorded \"{}\" as {} and {} in {}",
64        rec.title, rec.requirement, rec.task, rec.feature
65    );
66    if !run {
67        return CommandResult::ok(
68            "do",
69            &format!(
70                "{said}; run it: speccoder run --feature {} --task {}",
71                rec.feature, rec.task
72            ),
73        )
74        .with_next(vec![speccoder_core::next::NextStep {
75            command: format!(
76                "speccoder run --feature {} --task {}",
77                rec.feature, rec.task
78            ),
79            why: "the change, judged on evidence".to_owned(),
80        }]);
81    }
82    let result = crate::run(&root, Some(&rec.feature), &rec.task, options);
83    let status = result.status();
84    let message = format!("{said}; {}", result.message());
85    result.reworded(status, &message)
86}