Skip to main content

speccoder_runtime/
service.rs

1//! The web host's one way into the runtime: every entry point it uses,
2//! re-exported or delegated without changing a result. A write the host
3//! serves goes through [`actions::perform`], like every interface's; a
4//! later feature adds read views here as delegates, never a second write
5//! path. The layering test holds the host to this module.
6
7use std::path::Path;
8
9pub use crate::RunOptions;
10
11/// The action list and the one dispatcher.
12pub mod actions {
13    pub use crate::actions::{Dispatch, Interface, Offer, available, list, offered, perform};
14}
15
16/// The person's own commands, run for them (a fix speccoder printed), and
17/// a terminal at the project.
18pub mod person {
19    pub use crate::person::{Stream, TIMEOUT, open_terminal, run, sandboxed, shown, speccoder};
20}
21
22/// What went wrong, as a block to copy; the setup as a bug report.
23pub mod copy {
24    pub use crate::copy::block;
25    pub use crate::doctor::bug_report;
26}
27
28/// Calls waiting for a person, and who answers them.
29pub mod approvals {
30    pub use crate::approvals::{Answer, Approver, Presentation, Who, list};
31}
32
33/// A run's progress, as it happens and as another process left it.
34pub mod progress {
35    pub use crate::progress::{Event, Progress, read_live};
36}
37
38/// Read views.
39pub mod status {
40    pub use crate::status::report;
41}
42
43/// Shape: a person's words made into a requirement, judged by code; the
44/// writing check. Writes nothing.
45pub mod shape {
46    pub use crate::shape::{FIELDS, findings, judge, shape};
47}
48
49/// Ask: a read-only conversation; a turn writes only the Ask plane's
50/// journal and the conversation's thread.
51pub mod ask {
52    pub use crate::ask::chips::{ChipRef, resolve as chip};
53    pub use crate::ask::proposal::revalidate as proposal;
54    pub use crate::ask::{Request, changed_since, conversations, new_id, stop, thread, turn};
55}
56
57/// Explore: an idea's sessions, read; writes are actions.
58pub mod explore {
59    pub use crate::explore::{list, session};
60}
61
62/// The next steps a result names.
63pub mod next {
64    pub use crate::next::attach;
65}
66
67/// The person's review of the spec.
68pub mod review {
69    /// The comment tray and the last revision turn.
70    pub use crate::comments::list as comments;
71    pub use crate::review::{readiness, report};
72}
73
74/// Planning a new feature: the rung a description suggests, the
75/// requirements it is close to (read-only, no model).
76pub mod planning {
77    pub use crate::planning_chain::{similar_command, similar_to, suggest};
78}
79
80/// A new project: its plan (read-only), the idea offered once.
81pub mod new_project {
82    pub use crate::new_project::{Creation, creation, first_feature, plan, undo_offer};
83    pub use speccoder_core::new_project::{NewChoices, Stack, slug};
84}
85
86/// What a host outside a project shows.
87pub mod welcome {
88    pub use crate::welcome::{
89        Recent, RecentView, default_parent, folders, forget, is_project, remember, tilde, views,
90    };
91}
92
93/// The bundled sample.
94pub mod sample {
95    pub use crate::sample::{available, copy, home, missing_tool};
96}
97
98/// Needs You.
99pub mod inbox {
100    pub use crate::inbox::inbox;
101}
102
103/// A bug as speccoder keeps it (read-only; a bug's actions are kinds).
104pub mod bug {
105    pub use crate::bug::show;
106}
107
108/// A task's evidence.
109pub mod evidence {
110    pub use crate::evidence::{account_of, banner, rows, statements};
111}
112
113/// A task's evidence tour (`evidence_tour`), read as recorded.
114pub mod tour {
115    use std::path::Path;
116
117    use speccoder_core::{CommandData, CommandResult, CommandStatus};
118
119    /// The tour of `task` of `feature` (default: the selected feature) on
120    /// its `speccoder/<feature>` branch, else at `HEAD`: [`crate::tour::command`] without trust, so it shows what the
121    /// statements and the commit say.
122    #[must_use]
123    pub fn of_task(root: &Path, feature: Option<&str>, task: &str) -> CommandResult {
124        let selected = || match crate::inspect(root, None).data() {
125            Some(CommandData::Inspect(snap)) => {
126                snap.feature.as_ref().map(|f| f.feature.name.clone())
127            }
128            _ => None,
129        };
130        let Some(feature) = feature.map(str::to_owned).or_else(selected) else {
131            return CommandResult::new(
132                "tour",
133                CommandStatus::SelectionNeeded,
134                "no feature selected",
135            );
136        };
137        // The ticks are on the feature's speccoder branch until it merges.
138        let branch = format!("speccoder/{feature}");
139        let head = if crate::git::Git::at(root)
140            .line(&[
141                "rev-parse",
142                "--verify",
143                "--quiet",
144                &format!("refs/heads/{branch}"),
145            ])
146            .is_ok_and(|l| !l.trim().is_empty())
147        {
148            branch
149        } else {
150            "HEAD".to_owned()
151        };
152        crate::tour::command(
153            root,
154            &feature,
155            task,
156            &crate::attest::branch::Options {
157                head,
158                ..crate::attest::branch::Options::default()
159            },
160            "tour",
161        )
162    }
163}
164
165/// The feature's documents.
166pub mod documents {
167    pub use crate::documents::{of_feature, render};
168}
169
170/// The machine and the project, checked.
171pub mod doctor {
172    pub use crate::doctor::{agents, run};
173}
174
175/// `speccoder.toml`.
176pub mod settings {
177    /// What a prefix entry would newly allow (`config coverage`).
178    pub use crate::approvals::coverage::show as coverage;
179    pub use crate::settings::{apply, commit, show};
180}
181
182/// Tokens and money.
183pub mod controls {
184    pub use crate::controls::{SkillCost, cost, skill_cost};
185}
186
187/// The code view: the branch's change, its files.
188pub mod code {
189    pub use crate::code::{changes, file, tree, worktree_file, worktree_view};
190}
191
192/// Setting up a project.
193pub mod init {
194    pub use crate::init::{commit_written, finish, prepare, preview, root_of};
195}
196
197/// The features.
198pub mod current {
199    pub use crate::current::list;
200}
201
202/// The pull request, previewed.
203pub mod handoff {
204    pub use crate::handoff::{PrOptions, pr};
205}
206
207/// Where speccoder keeps its state.
208pub mod worktree {
209    pub use crate::worktree::state_dir;
210}
211
212/// GitHub: sign-in, issues, the branch's pull request.
213pub mod github {
214    pub use crate::github::{
215        Ctx, RepoEntry, description, issue, issues, login_with_token, pr_for, reach, refused, repo,
216        repositories, started, status, valid_slug,
217    };
218
219    /// Signing in on a device.
220    pub mod auth {
221        pub use crate::github::auth::{Pending, client_id, start, wait};
222    }
223
224    /// GitHub's addresses.
225    pub mod http {
226        pub use crate::github::http::oauth;
227    }
228}
229
230/// How the project's format words things, for the page's header.
231#[derive(Debug, Clone, Copy)]
232pub struct FormatWords {
233    /// The format (`Spec Kit`).
234    pub name: &'static str,
235    /// What it calls a unit of work (`feature`, `change`).
236    pub unit: &'static str,
237}
238
239/// The words of the format of the project at `root`.
240#[must_use]
241pub fn format_words(root: &Path) -> FormatWords {
242    let format = crate::format::at(root);
243    FormatWords {
244        name: format.wording().name,
245        unit: format.messages().unit,
246    }
247}
248
249/// The format's planning chain, by installed skill name, for the New
250/// Feature sheet: what creates a unit, what leads to the task list, what
251/// checks it and what revises a written spec.
252#[derive(Debug, Clone, serde::Serialize)]
253pub struct PlanningWords {
254    /// The creating skill (`speckit-specify`).
255    pub creates: Option<String>,
256    /// The skills toward the task list after it (`speckit-plan`, `speckit-tasks`).
257    pub toward: Vec<String>,
258    /// The checking skill (`speckit-analyze`).
259    pub checks: Option<String>,
260    /// The revising skill (`speckit-clarify`).
261    pub revises: Option<String>,
262}
263
264/// [`PlanningWords`] of the project at `root`.
265#[must_use]
266pub fn planning_words(root: &Path) -> PlanningWords {
267    let p = crate::format::at(root).planning();
268    let full = |s: &str| format!("{}{s}", p.prefix);
269    let creates = p.creates.first().map(|c| full(c));
270    PlanningWords {
271        toward: (p.toward_tasks)(false)
272            .iter()
273            .map(|s| full(s))
274            .filter(|s| Some(s) != creates.as_ref())
275            .collect(),
276        creates,
277        checks: p.checks.map(full),
278        revises: p.revises.map(full),
279    }
280}
281
282/// The installed name of the format's skill that creates a unit, if it has one.
283#[must_use]
284pub fn create_skill(root: &Path) -> Option<String> {
285    let planning = crate::format::at(root).planning();
286    planning
287        .creates
288        .first()
289        .map(|c| format!("{}{c}", planning.prefix))
290}
291
292/// One commit's patch, if `commit` names a commit (a blob or a tree id
293/// from elsewhere in the repository is never shown); empty otherwise.
294#[must_use]
295pub fn commit_patch(root: &Path, commit: &str) -> String {
296    commit_patch_of(root, commit)
297}
298
299/// Whether `id` is reached from `HEAD`, a branch or a saved attempt.
300fn reachable(git: &crate::git::Git, id: &str) -> bool {
301    if git
302        .succeeds(&["merge-base", "--is-ancestor", id, "HEAD"])
303        .unwrap_or(false)
304    {
305        return true;
306    }
307    git.line(&[
308        "for-each-ref",
309        "--count=1",
310        "--format=%(refname)",
311        "--contains",
312        id,
313        "refs/heads",
314        "refs/speccoder/attempts",
315    ])
316    .is_ok_and(|l| !l.trim().is_empty())
317}
318
319fn commit_patch_of(root: &Path, commit: &str) -> String {
320    let git = crate::git::Git::at(root);
321    // `^{commit}` peels or fails.
322    let Ok(id) = git.line(&[
323        "rev-parse",
324        "--verify",
325        "-q",
326        &format!("{commit}^{{commit}}"),
327    ]) else {
328        return String::new();
329    };
330    // Only what a branch, `HEAD` or a saved attempt reaches: a commit the
331    // page could only name by its id (a sealed or unreachable object) shows
332    // nothing.
333    if !reachable(&git, &id) {
334        return String::new();
335    }
336    git.run(&["show", "--no-color", "--patch", "--format=%h %s%n%n%b", &id])
337        .map(|b| String::from_utf8_lossy(&b).into_owned())
338        .unwrap_or_default()
339}