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: speccoder_core::actions::Via,
20    options: &crate::RunOptions,
21    asked: Option<(&str, &Path)>,
22) -> CommandResult {
23    let format = crate::format::at(path);
24    let Some(root) = format.detect(path) else {
25        return CommandResult::new(
26            "do",
27            CommandStatus::NotAProject,
28            &(format.messages().not_a_project)(path),
29        )
30        .with_code("NO-PROJECT");
31    };
32    let rec = match format.record_change(&root, request, checks) {
33        Ok(r) => r,
34        Err(e) => {
35            return CommandResult::new("do", CommandStatus::UsageError, &e).with_code("USAGE");
36        }
37    };
38    // The person's own words: approved as they wrote them.
39    let approved = crate::review::decide(
40        &root,
41        Some(&rec.feature),
42        std::slice::from_ref(&rec.requirement),
43        "approve",
44        None,
45        via,
46    );
47    if approved.status() != CommandStatus::Ok {
48        return CommandResult::carried("do", &approved);
49    }
50    // Suggested in Ask: the conversation is where it came from.
51    if let Some((conversation, state)) = asked {
52        let _ = crate::ladder::record_origin(
53            &crate::worktree::project_state_dir(state, &root),
54            &speccoder_core::ladder::subject(&rec.feature, &rec.requirement),
55            vec![speccoder_core::ladder::asked(
56                conversation,
57                crate::bug::confirm::now(),
58            )],
59        );
60    }
61    // A run reads committed files: exactly this unit's planning files.
62    let files = crate::hosting::uncommitted(&root, Some(&rec.feature));
63    if !files.is_empty() {
64        let committed = crate::hosting::commit_planning_as(
65            &root,
66            Some(&rec.feature),
67            &files,
68            Some(&format!("quick change: {}", rec.title)),
69        );
70        if committed.status() != CommandStatus::Ok {
71            return CommandResult::carried("do", &committed);
72        }
73    }
74    let said = format!(
75        "recorded \"{}\" as {} and {} in {}",
76        rec.title, rec.requirement, rec.task, rec.feature
77    );
78    if !run {
79        return CommandResult::ok(
80            "do",
81            &format!(
82                "{said}; run it: speccoder run --feature {} --task {}",
83                rec.feature, rec.task
84            ),
85        )
86        .with_next(vec![speccoder_core::next::NextStep {
87            command: format!(
88                "speccoder run --feature {} --task {}",
89                rec.feature, rec.task
90            ),
91            why: "the change, judged on evidence".to_owned(),
92        }]);
93    }
94    let result = crate::run(&root, Some(&rec.feature), &rec.task, options);
95    let status = result.status();
96    let message = format!("{said}; {}", result.message());
97    result.reworded(status, &message)
98}