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}