memstead_base/engine/
check_ops.rs1use crate::check::{CheckKind, CheckLedger, CheckRecord, CheckState, Verdict, derive_state};
18use crate::vcs::{Actor, ClientId};
19
20use super::{Engine, error::EngineError};
21
22impl Engine {
23 #[allow(clippy::too_many_arguments)] pub fn record_check(
39 &mut self,
40 mem_name: &str,
41 entity_id: &str,
42 verdict: Verdict,
43 kind: CheckKind,
44 method: Option<&str>,
45 actor: Actor,
46 client: Option<&ClientId>,
47 ) -> Result<CheckRecord, EngineError> {
48 let mount_idx = self
49 .mounts
50 .iter()
51 .position(|m| m.mount.mem == mem_name)
52 .ok_or_else(|| self.unknown_mem_error(mem_name))?;
53 if self.mounts[mount_idx].mount.capability != crate::workspace::MountCapability::Write {
54 return Err(EngineError::ReadOnlyMount(mem_name.to_string()));
55 }
56 let schema_ref = match kind {
57 CheckKind::Verification => None,
58 CheckKind::Conformance => Some(
59 self.mounts[mount_idx]
60 .mount
61 .schema
62 .as_ref()
63 .map(|s| s.as_display())
64 .ok_or_else(|| {
65 EngineError::InvalidInput(format!(
66 "a conformance check binds to the mem's schema pin, and mem \
67 `{mem_name}` declares none"
68 ))
69 })?,
70 ),
71 };
72 let entity_hash = self
73 .store
74 .all_entities()
75 .find(|e| e.mem == mem_name && e.id.0 == entity_id)
76 .map(|e| e.content_hash.clone())
77 .ok_or_else(|| EngineError::NotFound {
78 id: entity_id.to_string(),
79 })?;
80 let Some(root) = self.workspace_root() else {
81 return Err(EngineError::CheckNotRecorded {
82 reason: "engine has no workspace root — no durable check store".to_string(),
83 });
84 };
85 let ledger = CheckLedger::for_workspace(root);
86 let record = CheckRecord {
87 ts: std::time::SystemTime::now()
88 .duration_since(std::time::UNIX_EPOCH)
89 .map(|d| d.as_secs())
90 .unwrap_or(0),
91 entity: entity_id.to_string(),
92 verdict: verdict.as_str().to_string(),
93 method: method
94 .map(str::trim)
95 .filter(|m| !m.is_empty())
96 .map(str::to_string),
97 entity_hash,
98 actor: actor.as_trailer().to_string(),
99 client: client.map(|c| format!("{}@{}", c.name, c.version)),
100 role: self
101 .current_role()
102 .as_trailer()
103 .unwrap_or("unspecified")
104 .to_string(),
105 kind: match kind {
109 CheckKind::Verification => None,
110 CheckKind::Conformance => Some(kind.as_str().to_string()),
111 },
112 schema_ref,
113 };
114 ledger
115 .record(&record)
116 .map_err(|e| EngineError::CheckNotRecorded {
117 reason: format!("ledger append failed: {e}"),
118 })?;
119 Ok(record)
120 }
121
122 pub fn entity_check_state(
127 &self,
128 mem_name: &str,
129 entity_id: &str,
130 ) -> Result<(CheckState, Option<CheckRecord>), EngineError> {
131 self.find_mount(mem_name)?;
132 let current_hash = self
133 .store
134 .all_entities()
135 .find(|e| e.mem == mem_name && e.id.0 == entity_id)
136 .map(|e| e.content_hash.clone())
137 .ok_or_else(|| EngineError::NotFound {
138 id: entity_id.to_string(),
139 })?;
140 let latest = self
141 .workspace_root()
142 .map(CheckLedger::for_workspace)
143 .and_then(|l| l.latest_for_kind(entity_id, CheckKind::Verification));
144 Ok((derive_state(latest.as_ref(), ¤t_hash), latest))
145 }
146
147 pub fn entity_conformance_state(
153 &self,
154 mem_name: &str,
155 entity_id: &str,
156 ) -> Result<(CheckState, Option<CheckRecord>), EngineError> {
157 let mount = self.find_mount(mem_name)?;
158 let current_pin = mount.mount.schema.as_ref().map(|s| s.as_display());
159 let current_hash = self
160 .store
161 .all_entities()
162 .find(|e| e.mem == mem_name && e.id.0 == entity_id)
163 .map(|e| e.content_hash.clone())
164 .ok_or_else(|| EngineError::NotFound {
165 id: entity_id.to_string(),
166 })?;
167 let latest = self
168 .workspace_root()
169 .map(CheckLedger::for_workspace)
170 .and_then(|l| l.latest_for_kind(entity_id, CheckKind::Conformance));
171 Ok((
172 crate::check::derive_state_pinned(
173 latest.as_ref(),
174 ¤t_hash,
175 current_pin.as_deref(),
176 ),
177 latest,
178 ))
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use crate::check::{CheckKind, Verdict};
185 use crate::vcs::Actor;
186 use crate::workspace::MountCapability;
187
188 #[test]
195 fn conformance_refuses_without_a_schema_pin() {
196 let tmp = tempfile::TempDir::new().unwrap();
197 std::fs::write(
198 tmp.path().join("anything.md"),
199 "---\nid: anything\ntitle: Anything\ntype: note\n---\n\nBody.\n",
200 )
201 .unwrap();
202 let mut mount = crate::engine::test_helpers::folder_mount("m", tmp.path().to_path_buf());
203 mount.schema = None;
204 let mut engine = crate::Engine::from_mounts(vec![(
205 mount,
206 Box::new(crate::storage::FilesystemMemWriter::new(
207 tmp.path().to_path_buf(),
208 )) as Box<dyn crate::backend::MemBackend>,
209 )])
210 .unwrap();
211 let err = engine
212 .record_check(
213 "m",
214 "m--anything",
215 Verdict::Ok,
216 CheckKind::Conformance,
217 None,
218 Actor::Cli,
219 None,
220 )
221 .unwrap_err();
222 assert_eq!(err.code(), "MEM_QUARANTINED");
223 }
224
225 #[test]
229 fn check_refuses_read_only_mounts_typed() {
230 let tmp = tempfile::TempDir::new().unwrap();
231 let mut mount = crate::engine::test_helpers::folder_mount("ro", tmp.path().to_path_buf());
232 mount.capability = MountCapability::ReadOnly;
233 let mut engine = crate::Engine::from_mounts(vec![(
234 mount,
235 Box::new(crate::storage::FilesystemMemWriter::new(
236 tmp.path().to_path_buf(),
237 )) as Box<dyn crate::backend::MemBackend>,
238 )])
239 .unwrap();
240 let err = engine
241 .record_check(
242 "ro",
243 "ro--anything",
244 Verdict::Ok,
245 crate::check::CheckKind::Verification,
246 None,
247 Actor::Cli,
248 None,
249 )
250 .unwrap_err();
251 assert_eq!(err.code(), "READ_ONLY_MOUNT");
252 }
253}