1use std::collections::{BTreeMap, BTreeSet};
2
3use sim_codec_bridge::{BridgeBook, BridgePacket, BridgeVotePayload, content_id_string};
4use sim_kernel::{ContentId, Cx, Error, Expr, Result, Symbol};
5use sim_lib_bridge::{effective_caps, rx_check};
6use sim_value::{access::field, build::entry};
7
8use crate::{CompiledIntent, lift::content_id_for_expr};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum Verifier {
13 Assertion {
15 predicate: Expr,
18 },
19 Judge {
21 seat: String,
23 packet: Box<BridgePacket>,
25 reply_to: Option<Box<BridgePacket>>,
27 target: String,
29 min_votes: u32,
31 },
32 Evidence {
34 cites: Vec<String>,
37 },
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum ProbeOracle {
43 Expected(Expr),
45 Evidence(ContentId),
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct VerifyProbe {
52 pub args: Expr,
54 pub oracle: ProbeOracle,
56 pub verifier_ids: Vec<Symbol>,
58}
59
60impl VerifyProbe {
61 pub fn content_id(&self) -> Result<ContentId> {
63 content_id_for_expr(&self.to_expr())
64 }
65
66 fn to_expr(&self) -> Expr {
67 Expr::Map(vec![
68 entry("args", self.args.clone()),
69 entry("oracle", self.oracle.to_expr()),
70 entry(
71 "verifiers",
72 Expr::Vector(
73 self.verifier_ids
74 .iter()
75 .cloned()
76 .map(Expr::Symbol)
77 .collect(),
78 ),
79 ),
80 ])
81 }
82}
83
84impl ProbeOracle {
85 fn to_expr(&self) -> Expr {
86 match self {
87 Self::Expected(expected) => Expr::Map(vec![
88 entry("kind", Expr::Symbol(Symbol::qualified("forge", "Expected"))),
89 entry("answer", expected.clone()),
90 ]),
91 Self::Evidence(id) => Expr::Map(vec![
92 entry("kind", Expr::Symbol(Symbol::qualified("forge", "Evidence"))),
93 entry("content-id", Expr::String(content_id_string(id))),
94 ]),
95 }
96 }
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct VerifyFailure {
102 pub id: Symbol,
104 pub reason: String,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
110pub struct VerifyReport {
111 pub passed: Vec<Symbol>,
113 pub failed: Vec<VerifyFailure>,
115}
116
117impl VerifyReport {
118 pub fn accepted(&self) -> bool {
120 self.failed.is_empty()
121 }
122
123 fn pass(&mut self, id: Symbol) {
124 self.passed.push(id);
125 }
126
127 fn fail(&mut self, id: Symbol, reason: impl Into<String>) {
128 self.failed.push(VerifyFailure {
129 id,
130 reason: reason.into(),
131 });
132 }
133
134 fn extend(&mut self, mut other: VerifyReport) {
135 self.passed.append(&mut other.passed);
136 self.failed.append(&mut other.failed);
137 }
138}
139
140#[derive(Clone, Debug, Default)]
142pub struct VerifyCatalog {
143 verifiers: BTreeMap<Symbol, Verifier>,
144 probes: BTreeMap<ContentId, VerifyProbe>,
145 intent_probes: BTreeMap<Symbol, BTreeSet<ContentId>>,
146 evidence: BTreeMap<String, Expr>,
147}
148
149impl VerifyCatalog {
150 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn register_verifier(&mut self, id: Symbol, verifier: Verifier) {
157 self.verifiers.insert(id, verifier);
158 }
159
160 pub fn verifier(&self, id: &Symbol) -> Option<&Verifier> {
162 self.verifiers.get(id)
163 }
164
165 pub fn register_probe(&mut self, intent: Symbol, probe: VerifyProbe) -> Result<ContentId> {
167 let id = probe.content_id()?;
168 self.insert_probe(intent, id.clone(), probe);
169 Ok(id)
170 }
171
172 pub fn insert_probe(&mut self, intent: Symbol, id: ContentId, probe: VerifyProbe) {
174 self.probes.insert(id.clone(), probe);
175 self.intent_probes.entry(intent).or_default().insert(id);
176 }
177
178 pub fn probe_ids_for(&self, intent: &Symbol) -> Vec<ContentId> {
180 self.intent_probes
181 .get(intent)
182 .into_iter()
183 .flat_map(|ids| ids.iter().cloned())
184 .collect()
185 }
186
187 pub fn insert_evidence(&mut self, cite: impl Into<String>, value: Expr) {
189 self.evidence.insert(cite.into(), value);
190 }
191
192 pub fn insert_content_evidence(&mut self, id: &ContentId, value: Expr) {
194 self.insert_evidence(content_id_string(id), value);
195 }
196
197 pub fn verify_answer(
199 &self,
200 cx: &mut Cx,
201 intent: &CompiledIntent,
202 answer: &Expr,
203 ) -> Result<VerifyReport> {
204 let mut report = VerifyReport::default();
205 for id in &intent.verifiers {
206 match self.verifiers.get(id) {
207 Some(verifier) => match self.run_verifier(cx, verifier, answer) {
208 Ok(()) => report.pass(id.clone()),
209 Err(reason) => report.fail(id.clone(), reason),
210 },
211 None => report.fail(id.clone(), "semantic verifier is not registered"),
212 }
213 }
214 Ok(report)
215 }
216
217 pub fn verify_probe(
219 &self,
220 cx: &mut Cx,
221 intent: &CompiledIntent,
222 probe: &VerifyProbe,
223 ) -> Result<VerifyReport> {
224 let answer = self.answer_for_oracle(&probe.oracle)?;
225 let mut scoped = intent.clone();
226 scoped.verifiers = probe.verifier_ids.clone();
227 self.verify_answer(cx, &scoped, &answer)
228 }
229
230 pub fn verify_intent_probes(
233 &self,
234 cx: &mut Cx,
235 intent: &CompiledIntent,
236 ) -> Result<VerifyReport> {
237 let mut report = VerifyReport::default();
238 if intent.verifiers.is_empty() {
239 report.fail(
240 Symbol::qualified("forge", "verifier"),
241 "intent declares no semantic verifiers",
242 );
243 return Ok(report);
244 }
245 if intent.probes.is_empty() {
246 report.fail(
247 Symbol::qualified("forge", "probe"),
248 "intent declares no verification probes",
249 );
250 return Ok(report);
251 }
252
253 let covered = self.covered_verifiers(intent);
254 for required in &intent.verifiers {
255 if !covered.contains(required) {
256 report.fail(
257 required.clone(),
258 "no verification probe requires this verifier",
259 );
260 }
261 }
262
263 for id in &intent.probes {
264 match self.probes.get(id) {
265 Some(probe) => report.extend(self.verify_probe(cx, intent, probe)?),
266 None => report.fail(
267 Symbol::qualified("forge", "probe"),
268 format!(
269 "verification probe {} is not registered",
270 content_id_string(id)
271 ),
272 ),
273 }
274 }
275 Ok(report)
276 }
277
278 fn covered_verifiers(&self, intent: &CompiledIntent) -> BTreeSet<Symbol> {
279 intent
280 .probes
281 .iter()
282 .filter_map(|id| self.probes.get(id))
283 .flat_map(|probe| probe.verifier_ids.iter().cloned())
284 .collect()
285 }
286
287 fn answer_for_oracle(&self, oracle: &ProbeOracle) -> Result<Expr> {
288 match oracle {
289 ProbeOracle::Expected(answer) => Ok(answer.clone()),
290 ProbeOracle::Evidence(id) => self
291 .evidence
292 .get(&content_id_string(id))
293 .cloned()
294 .ok_or_else(|| {
295 Error::Eval(format!(
296 "probe evidence {} is absent",
297 content_id_string(id)
298 ))
299 }),
300 }
301 }
302
303 fn run_verifier(
304 &self,
305 cx: &mut Cx,
306 verifier: &Verifier,
307 answer: &Expr,
308 ) -> std::result::Result<(), String> {
309 match verifier {
310 Verifier::Assertion { predicate } => {
311 let narrowed = sim_kernel::CapabilitySet::new();
312 cx.with_capabilities(narrowed, |_| check_assertion(predicate, answer))
313 .map_err(|err| err.to_string())
314 .and_then(|result| result)
315 }
316 Verifier::Judge {
317 seat,
318 packet,
319 reply_to,
320 target,
321 min_votes,
322 } => {
323 let narrowed = effective_caps(cx, packet).map_err(|err| err.to_string())?;
324 cx.with_capabilities(narrowed, |scoped| {
325 check_judge(
326 scoped,
327 seat,
328 packet,
329 reply_to.as_deref(),
330 target,
331 *min_votes,
332 )
333 })
334 .map_err(|err| err.to_string())
335 .and_then(|result| result)
336 }
337 Verifier::Evidence { cites } => {
338 let narrowed = sim_kernel::CapabilitySet::new();
339 cx.with_capabilities(narrowed, |_| Ok(self.check_evidence(cites, answer)))
340 .map_err(|err| err.to_string())
341 .and_then(|result| result)
342 }
343 }
344 }
345
346 fn check_evidence(&self, cites: &[String], answer: &Expr) -> std::result::Result<(), String> {
347 if cites.is_empty() {
348 return Err("evidence verifier must cite at least one source".to_owned());
349 }
350 let mut matched = false;
351 for cite in cites {
352 let Some(evidence) = self.evidence.get(cite) else {
353 return Err(format!("cited evidence {cite} is absent"));
354 };
355 matched |= answer.canonical_eq(evidence);
356 }
357 if matched {
358 Ok(())
359 } else {
360 Err("answer does not match cited ground truth".to_owned())
361 }
362 }
363}
364
365pub fn verify_answer(cx: &mut Cx, intent: &CompiledIntent, answer: &Expr) -> Result<VerifyReport> {
370 VerifyCatalog::new().verify_answer(cx, intent, answer)
371}
372
373fn check_assertion(predicate: &Expr, answer: &Expr) -> Result<std::result::Result<(), String>> {
374 let kind = match field(predicate, "predicate") {
375 Some(Expr::Symbol(symbol)) => symbol,
376 _ => {
377 return Ok(Err(
378 "assertion predicate must name a predicate symbol".to_owned()
379 ));
380 }
381 };
382
383 if *kind == Symbol::qualified("forge", "equals") {
384 let Some(expected) = field(predicate, "expected") else {
385 return Ok(Err("forge/equals predicate is missing expected".to_owned()));
386 };
387 return if answer.canonical_eq(expected) {
388 Ok(Ok(()))
389 } else {
390 Ok(Err("answer did not equal expected expression".to_owned()))
391 };
392 }
393
394 if *kind == Symbol::qualified("forge", "number-between") {
395 return check_number_between(predicate, answer);
396 }
397
398 if *kind == Symbol::qualified("forge", "field-number-between") {
399 let field_name = match field(predicate, "field") {
400 Some(Expr::String(name)) => name.as_str(),
401 Some(Expr::Symbol(symbol)) if symbol.namespace.is_none() => symbol.name.as_ref(),
402 _ => {
403 return Ok(Err(
404 "field-number-between predicate is missing field".to_owned()
405 ));
406 }
407 };
408 let Some(value) = field(answer, field_name) else {
409 return Ok(Err(format!("answer is missing field {field_name}")));
410 };
411 return check_number_between(predicate, value);
412 }
413
414 Ok(Err(format!("unsupported assertion predicate {kind}")))
415}
416
417fn check_number_between(predicate: &Expr, value: &Expr) -> Result<std::result::Result<(), String>> {
418 let Some(value) = integer_value(value)? else {
419 return Ok(Err("answer value is not an integer number".to_owned()));
420 };
421 let min = optional_i64(predicate, "min")?;
422 let max = optional_i64(predicate, "max")?;
423 if let Some(min) = min
424 && value < min
425 {
426 return Ok(Err(format!("answer value {value} is below minimum {min}")));
427 }
428 if let Some(max) = max
429 && value > max
430 {
431 return Ok(Err(format!("answer value {value} is above maximum {max}")));
432 }
433 Ok(Ok(()))
434}
435
436fn optional_i64(expr: &Expr, name: &str) -> Result<Option<i64>> {
437 match field(expr, name) {
438 Some(value) => integer_value(value)?
439 .map(Some)
440 .ok_or_else(|| Error::Eval(format!("{name} must be an integer number when present"))),
441 None => Ok(None),
442 }
443}
444
445fn integer_value(expr: &Expr) -> Result<Option<i64>> {
446 match expr {
447 Expr::Number(number) => number
448 .canonical
449 .parse::<i64>()
450 .map(Some)
451 .map_err(|_| Error::Eval(format!("{} is not an integer", number.canonical))),
452 _ => Ok(None),
453 }
454}
455
456fn check_judge(
457 cx: &mut Cx,
458 seat: &str,
459 packet: &BridgePacket,
460 reply_to: Option<&BridgePacket>,
461 target: &str,
462 min_votes: u32,
463) -> Result<std::result::Result<(), String>> {
464 if min_votes == 0 {
465 return Ok(Err("judge quorum must require at least one vote".to_owned()));
466 }
467 if packet.header.from != seat {
468 return Ok(Err(format!(
469 "judge packet came from {}, expected {seat}",
470 packet.header.from
471 )));
472 }
473 let report = rx_check(cx, &BridgeBook::standard(), packet, reply_to)?;
474 if !report.accepted() {
475 return Ok(Err(format!(
476 "judge packet failed BRIDGE rx_check: {:?}",
477 report.obligations
478 )));
479 }
480
481 let mut votes = 0u32;
482 for part in &packet.body {
483 if part.kind != Symbol::qualified("bridge", "Vote") {
484 continue;
485 }
486 let vote = BridgeVotePayload::from_expr(&part.payload)?;
487 if vote.target == target && vote.scores.iter().any(|score| score.value > 0) {
488 votes = votes.saturating_add(1);
489 }
490 }
491
492 if votes >= min_votes {
493 Ok(Ok(()))
494 } else {
495 Ok(Err(format!(
496 "judge quorum for {target} has {votes} vote(s), needs {min_votes}"
497 )))
498 }
499}