Skip to main content

skill_evidence/
host.rs

1//! The repository this crate is running inside.
2//!
3//! Everything the lifecycle writes to a caller is either neutral or branded.
4//! Neutral output — the event stream, the gate projection — is identified by
5//! `schema_version` and reads the same in every repository, which is why the
6//! recorded evidence under `reports/skill-evidence/` needed no migration when
7//! this crate left its first host. Branded output is the receipts a command
8//! prints, the recovery instructions it gives, and the skill packages it
9//! installs, all of which name a repository and a binary that this crate cannot
10//! know.
11//!
12//! One fact here is not about output at all. The lifecycle refuses to let a
13//! workflow review its own skill package, and deciding that means knowing where
14//! *the running binary's* repository keeps its skills — not where the `--root`
15//! under audit keeps its. A crate compiled into someone else's dependency graph
16//! cannot infer that from its own source location, so the host states it.
17//!
18//! [`Host`] is where a repository states all of it, once.
19
20use std::path::{Path, PathBuf};
21
22/// One repository's identity to this crate.
23///
24/// The fields are public and there is no constructor, so adding one here is a
25/// compile error at every host rather than a silent default. There are no
26/// invariants to protect: each field is a name this crate repeats back, and a
27/// wrong one is visible in the first line of output.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Host {
30    /// Prefixes every schema identity in a receipt, e.g. `playbench` in
31    /// `playbench.skill-evidence.record.v1`.
32    pub namespace: String,
33    /// The binary an operator actually types. Usually, but not necessarily,
34    /// the same word as `namespace`.
35    pub command: String,
36    /// The Cargo package that provides `command`, as `cargo run -p` wants it.
37    /// Installed skill packages quote it in their runnable examples.
38    pub cargo_package: String,
39    /// Where *this* repository keeps its own skill packages.
40    ///
41    /// Resolve it however the host resolves it — typically from the host's own
42    /// `CARGO_MANIFEST_DIR` at compile time. It is deliberately not derived
43    /// from the audited `--root`:
44    /// `lifecycle_self_target_checks_canonical_paths_instead_of_basenames`
45    /// pins that a same-named package in the audited tree is a different
46    /// package, not a self-target.
47    pub skills_directory: PathBuf,
48}
49
50impl Host {
51    /// The schema identity on a `hash` receipt.
52    #[must_use]
53    pub fn hash_schema(&self) -> String {
54        format!("{}.skill-evidence.hash.v1", self.namespace)
55    }
56
57    /// The schema identity on a `record` receipt.
58    #[must_use]
59    pub fn record_schema(&self) -> String {
60        format!("{}.skill-evidence.record.v1", self.namespace)
61    }
62
63    /// The schema identity on a method-gap research inventory.
64    #[must_use]
65    pub fn method_gap_inventory_schema(&self) -> String {
66        format!("{}.method-gap-research-status.inventory/v1", self.namespace)
67    }
68
69    /// The package this repository would be running when it drives
70    /// `operator_skill_name`, used to refuse a workflow that targets itself.
71    #[must_use]
72    pub fn operator_skill(&self, operator_skill_name: &str) -> PathBuf {
73        self.skills_directory.join(operator_skill_name)
74    }
75
76    /// Where this repository keeps its own skill packages.
77    #[must_use]
78    pub fn skills_directory(&self) -> &Path {
79        &self.skills_directory
80    }
81
82    /// Renders the sentence that tells an operator how to finish an operation
83    /// that stopped part-way.
84    ///
85    /// This is the half of an [`Error`](crate::Error) the library deliberately
86    /// does not write: it knows the action, and only the host knows what to call
87    /// the binary that performs it.
88    #[must_use]
89    pub fn recovery_instruction(&self, recovery: Recovery) -> String {
90        match recovery {
91            Recovery::RederiveGate => format!(
92                "Run `{} skills evidence derive --target <skill-dir>`.",
93                self.command
94            ),
95            Recovery::ReleaseEvidenceLock => format!(
96                "Once no other `{}` run is writing to that evidence store, remove the `.lock` directory named above and run this command again.",
97                self.command
98            ),
99        }
100    }
101}
102
103/// The operator action that finishes an operation this crate could not finish.
104///
105/// An [`Error`](crate::Error) carrying one of these is not merely describing a
106/// failure — it is naming work the caller must still do, and the caller's own
107/// output surface is responsible for saying so in its own words. See
108/// [`Host::recovery_instruction`].
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum Recovery {
112    /// The evidence event was durably appended, but the gate projection beside
113    /// it was not replaced. The stream is intact and the projection is stale
114    /// until it is derived again.
115    RederiveGate,
116    /// A lock on the evidence store outlived the run that took it, so every
117    /// command on that store now stops before doing anything.
118    ///
119    /// Releasing it is the operator's act and not this crate's: a lock that
120    /// looks abandoned may belong to a run still working, and taking it would
121    /// put two writers on one append-only stream. The crate says what it found
122    /// and who holds it; the operator decides that the holder is gone.
123    ReleaseEvidenceLock,
124}