Skip to main content

speccoder_runtime/
sample.rs

1//! Try the sample: a small project and a run of it recorded with
2//! a real model, bundled into release builds, copied into speccoder's own
3//! state directory (never the person's workspace) and verified by strict
4//! replay — no model, no key, no network. A run this build cannot
5//! reproduce is reported as such, never shown as if it had worked; a
6//! build without the recording says so (`SAMPLE-MISSING`), never shows an
7//! empty playback.
8//!
9//! The recording is made by the maintainers with `just record-sample`
10//! (`scripts/record-sample.sh`), never in CI.
11
12use std::path::{Path, PathBuf};
13
14use serde::{Deserialize, Serialize};
15use speccoder_core::{
16    CommandData, CommandResult, CommandStatus, SampleRecording, SampleReport, SampleRun,
17};
18
19const COMMAND: &str = "sample";
20
21/// The recording's manifest (`manifest.toml`).
22#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
23pub struct Manifest {
24    /// The project's name (`wordcount`).
25    pub case: String,
26    /// The recorded runs, in order.
27    pub run_ids: Vec<String>,
28    /// The provider it was recorded with.
29    pub provider: String,
30    /// The model.
31    pub model: String,
32    /// The agentplane version that recorded it.
33    pub agentplane: String,
34    /// The speccoder version that recorded it.
35    pub speccoder: String,
36    /// When (RFC 3339).
37    pub recorded_at: String,
38    /// `reproduced`.
39    pub expect: String,
40}
41
42/// A sample: the project's history as a git bundle, the journal, the manifest.
43#[derive(Debug, Clone)]
44pub struct Asset {
45    /// `git bundle create --all` of the project.
46    pub bundle: Vec<u8>,
47    /// The agentplane journal of the recorded runs.
48    pub journal: Vec<u8>,
49    /// `manifest.toml`.
50    pub manifest: String,
51}
52
53/// The sample bundled into this build, if it has one.
54#[must_use]
55pub fn bundled() -> Option<Asset> {
56    bundled_asset()
57}
58
59#[cfg(speccoder_sample)]
60fn bundled_asset() -> Option<Asset> {
61    Some(Asset {
62        bundle: include_bytes!(concat!(
63            env!("CARGO_MANIFEST_DIR"),
64            "/assets/sample/sample.bundle"
65        ))
66        .to_vec(),
67        // The journal is mostly preallocated space: bundled compressed.
68        journal: gunzip(include_bytes!(concat!(
69            env!("CARGO_MANIFEST_DIR"),
70            "/assets/sample/journal.redb.gz"
71        )))?,
72        manifest: include_str!(concat!(
73            env!("CARGO_MANIFEST_DIR"),
74            "/assets/sample/manifest.toml"
75        ))
76        .to_owned(),
77    })
78}
79
80/// A gzip stream's contents; `None` when it is not one.
81#[cfg_attr(not(speccoder_sample), allow(dead_code))]
82fn gunzip(bytes: &[u8]) -> Option<Vec<u8>> {
83    use std::io::Read as _;
84    let mut out = Vec::new();
85    flate2::read::GzDecoder::new(bytes)
86        .read_to_end(&mut out)
87        .ok()?;
88    Some(out)
89}
90
91#[cfg(not(speccoder_sample))]
92const fn bundled_asset() -> Option<Asset> {
93    None
94}
95
96/// Whether this build has the sample.
97#[must_use]
98pub fn available() -> bool {
99    cfg!(speccoder_sample)
100}
101
102/// Where the sample's copies live.
103#[must_use]
104pub fn home(state: &Path) -> PathBuf {
105    state.join("sample").join(env!("CARGO_PKG_VERSION"))
106}
107
108/// The copy of the project, if one was made.
109#[must_use]
110pub fn copy(state: &Path) -> Option<PathBuf> {
111    let text = std::fs::read_to_string(home(state).join("sample.json")).ok()?;
112    let v: serde_json::Value = serde_json::from_str(&text).ok()?;
113    let p = PathBuf::from(v["path"].as_str()?);
114    p.is_dir().then_some(p)
115}
116
117/// The sample is missing from this build.
118#[must_use]
119pub fn missing() -> CommandResult {
120    CommandResult::new(
121        COMMAND,
122        CommandStatus::NotFound,
123        "this build has no sample: its recorded run is bundled into release builds once it is recorded",
124    )
125    .with_code("SAMPLE-MISSING")
126}
127
128/// `speccoder sample [--fresh]`: the bundled sample, copied and replayed.
129#[must_use]
130pub fn open(state: &Path, fresh: bool) -> CommandResult {
131    match bundled() {
132        Some(a) => open_from(&a, state, fresh),
133        None => missing(),
134    }
135}
136
137fn failed(m: &str) -> CommandResult {
138    CommandResult::new(COMMAND, CommandStatus::Error, m)
139}
140
141/// [`open`] for `asset`: a copy under `<state>/sample/<version>/` (an
142/// existing one is reopened unless `fresh`), its journal placed in the
143/// copy's project state, every recorded run replayed strictly.
144#[must_use]
145pub fn open_from(asset: &Asset, state: &Path, fresh: bool) -> CommandResult {
146    let manifest: Manifest = match toml::from_str(&asset.manifest) {
147        Ok(m) => m,
148        Err(e) => return failed(&format!("the sample's manifest is unreadable: {e}")),
149    };
150    let base = home(state);
151    let dir = base.join(&manifest.case);
152    let made = fresh || !dir.join(".git").exists();
153    if made {
154        if dir.exists()
155            && let Err(e) = std::fs::remove_dir_all(&dir)
156        {
157            return failed(&format!("cannot replace the sample's copy: {e}"));
158        }
159        if let Err(e) = std::fs::create_dir_all(&base) {
160            return failed(&format!("{}: {e}", base.display()));
161        }
162        let bundle = base.join("sample.bundle");
163        if let Err(e) = std::fs::write(&bundle, &asset.bundle) {
164            return failed(&format!("{}: {e}", bundle.display()));
165        }
166        let cloned = crate::git::Git::at(&base).run(&[
167            "clone",
168            "--quiet",
169            "--",
170            &bundle.display().to_string(),
171            &dir.display().to_string(),
172        ]);
173        // A clone makes only the default branch local: the ticks, their
174        // notes and the full-tier record come in as they were recorded.
175        let cloned = cloned.and_then(|_| {
176            crate::git::Git::at(&dir).run(&[
177                "fetch",
178                "--quiet",
179                "--update-head-ok",
180                &bundle.display().to_string(),
181                "+refs/*:refs/*",
182            ])
183        });
184        let _ = std::fs::remove_file(&bundle);
185        if let Err(e) = cloned {
186            return failed(&format!(
187                "the sample's history could not be unpacked: {}",
188                e.0
189            ));
190        }
191        let plane = crate::worktree::project_state_dir(state, &dir);
192        if let Err(e) = std::fs::create_dir_all(&plane)
193            .and_then(|()| std::fs::write(plane.join(crate::substrate::JOURNAL), &asset.journal))
194        {
195            return failed(&format!("the sample's journal could not be placed: {e}"));
196        }
197    }
198    let runs: Vec<SampleRun> = manifest
199        .run_ids
200        .iter()
201        .map(|id| {
202            let r = crate::replay_with(&dir, id, state);
203            let result = match r.data() {
204                Some(CommandData::Replay(rep)) => rep.result.clone(),
205                _ => "not_replayable".to_owned(),
206            };
207            SampleRun {
208                id: id.clone(),
209                finding: (result != "reproduced").then(|| r.message().to_owned()),
210                result,
211            }
212        })
213        .collect();
214    let report = SampleReport {
215        path: dir.display().to_string(),
216        fresh: made,
217        recorded: SampleRecording {
218            model: manifest.model.clone(),
219            recorded_at: manifest.recorded_at.clone(),
220            agentplane: manifest.agentplane.clone(),
221        },
222        runs,
223    };
224    if let Err(e) = std::fs::write(
225        base.join("sample.json"),
226        serde_json::json!({
227            "version": env!("CARGO_PKG_VERSION"),
228            "path": report.path,
229            "recorded": report.recorded,
230            "runs": report.runs,
231        })
232        .to_string(),
233    ) {
234        return failed(&format!(
235            "the sample's record (sample.json) could not be written, so the app cannot show it: {e}"
236        ));
237    }
238    let diverged = report.runs.iter().find(|r| r.result != "reproduced");
239    let result = match diverged {
240        Some(r) => CommandResult::new(
241            COMMAND,
242            CommandStatus::ReplayDiverged,
243            &format!(
244                "this build cannot reproduce the sample's recorded run — please report it: {}",
245                r.finding.clone().unwrap_or_default()
246            ),
247        )
248        .with_code("SAMPLE-DIVERGED"),
249        None => CommandResult::new(
250            COMMAND,
251            CommandStatus::Ok,
252            &format!(
253                "sample ready at {} — {} recorded run{} reproduced (no model called)",
254                crate::welcome::tilde(&dir),
255                report.runs.len(),
256                if report.runs.len() == 1 { "" } else { "s" }
257            ),
258        )
259        .with_next(vec![
260            speccoder_core::next::NextStep {
261                command: format!("speccoder ui -C {}", crate::welcome::tilde(&dir)),
262                why: "watch the recorded run play back".to_owned(),
263            },
264            speccoder_core::next::NextStep {
265                command: "speccoder new \"…\"".to_owned(),
266                why: "start your own".to_owned(),
267            },
268        ]),
269    };
270    result.with_data(CommandData::Sample(Box::new(report)))
271}
272
273/// The tool the sample's checks need that is not on the `PATH`, with how
274/// to get it (`None`: they can run here).
275#[must_use]
276pub fn missing_tool(copy: &Path) -> Option<String> {
277    let text = std::fs::read_to_string(copy.join(crate::verify::config::FILE)).ok()?;
278    let checks = crate::verify::config::parse(&text).ok()?;
279    let env: Vec<(String, String)> = std::env::vars().collect();
280    checks
281        .iter()
282        .filter_map(|c| c.argv.first())
283        .find(|p| crate::sandbox::resolve_executable(p, copy, &env).is_none())
284        .map(|p| match p.as_str() {
285            "cargo" => "cargo is not installed — install it: https://rustup.rs".to_owned(),
286            other => format!("{other} is not installed"),
287        })
288}
289
290/// *Run the checks yourself*: the sample's own fast checks, in its copy,
291/// confined like a run's (no network). Refused anywhere but the sample.
292#[must_use]
293pub fn run_checks(state: &Path, root: &Path) -> CommandResult {
294    let copy = copy(state).and_then(|c| std::fs::canonicalize(c).ok());
295    if copy.is_none() || copy != std::fs::canonicalize(root).ok() {
296        return CommandResult::new(
297            COMMAND,
298            CommandStatus::UsageError,
299            "these are the sample's checks: run them in its copy (speccoder sample)",
300        )
301        .with_code("USAGE");
302    }
303    if let Some(why) = missing_tool(root) {
304        return CommandResult::new(COMMAND, CommandStatus::ConfigError, &why).with_code("CONFIG");
305    }
306    let runner = match crate::sandbox::runner_at(root) {
307        Ok((r, _)) => r,
308        Err(e) => return CommandResult::new(COMMAND, CommandStatus::ConfigError, &e),
309    };
310    match crate::verify::run_checks(root, Some(crate::verify::config::Tier::Fast), &runner) {
311        Ok(report) => {
312            let (status, message) = if report.passed {
313                (CommandStatus::Ok, "the sample's checks pass here, offline")
314            } else {
315                (
316                    CommandStatus::VerificationFailed,
317                    "a check of the sample fails here",
318                )
319            };
320            CommandResult::new(COMMAND, status, message)
321                .with_data(CommandData::Verify(Box::new(report)))
322        }
323        Err(e) => CommandResult::new(COMMAND, CommandStatus::Error, &e),
324    }
325}
326
327#[cfg(test)]
328mod gzip_tests {
329    #[test]
330    fn a_compressed_journal_unpacks_and_anything_else_is_refused() {
331        use std::io::Write as _;
332        let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
333        gz.write_all(b"journal bytes").unwrap();
334        let packed = gz.finish().unwrap();
335        assert_eq!(
336            super::gunzip(&packed).as_deref(),
337            Some(&b"journal bytes"[..])
338        );
339        assert_eq!(super::gunzip(b"not gzip"), None);
340    }
341}