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