1use crate::ir_nodes::IRProgram;
26
27use super::generate::{
28 artifact_digest, derive_capability_containment_witness,
29 derive_capability_isolation_witness, derive_compliance_coverage_witness,
30 derive_effect_row_soundness_witness, derive_endpoint_retry_witness,
31 derive_shield_halt_witness, derive_socket_credit_witness,
32 derive_tool_call_soundness_witness,
33};
34use super::proof_term::{ProofTerm, PropertyClass, ResourceBoundsWitness, Witness};
35
36#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum CheckOutcome {
40 Verified,
43 Refuted { reason: String },
48 DigestMismatch,
51 UnknownProperty,
54}
55
56pub fn check_proof(proof: &ProofTerm, ir: &IRProgram) -> CheckOutcome {
59 if proof.artifact_digest != artifact_digest(ir) {
62 return CheckOutcome::DigestMismatch;
63 }
64 match (&proof.property, &proof.witness) {
70 (PropertyClass::ComplianceCoverage, Witness::ComplianceCoverage(w)) => {
71 check_compliance_coverage(w, ir)
72 }
73 (PropertyClass::EffectRowSoundness, Witness::EffectRowSoundness(w)) => {
74 check_effect_row_soundness(w, ir)
75 }
76 (PropertyClass::CapabilityIsolation, Witness::CapabilityIsolation(w)) => {
77 check_capability_isolation(w, ir)
78 }
79 (PropertyClass::ResourceBounds, Witness::ResourceBounds(w)) => {
80 check_resource_bounds(w, ir)
81 }
82 (PropertyClass::ShieldHaltGuarantee, Witness::ShieldHaltGuarantee(w)) => {
83 check_shield_halt_guarantee(w, ir)
84 }
85 (PropertyClass::CapabilityContainment, Witness::CapabilityContainment(w)) => {
86 check_capability_containment(w, ir)
87 }
88 (PropertyClass::ToolCallSoundness, Witness::ToolCallSoundness(w)) => {
89 check_tool_call_soundness(w, ir)
90 }
91 _ => CheckOutcome::UnknownProperty,
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct ProofCheck {
98 pub index: usize,
100 pub property: PropertyClass,
102 pub subject: String,
104 pub outcome: CheckOutcome,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct BundleReport {
117 pub results: Vec<ProofCheck>,
119}
120
121impl BundleReport {
122 pub fn all_verified(&self) -> bool {
127 self.results
128 .iter()
129 .all(|r| r.outcome == CheckOutcome::Verified)
130 }
131
132 pub fn refutations(&self) -> Vec<&ProofCheck> {
135 self.results
136 .iter()
137 .filter(|r| r.outcome != CheckOutcome::Verified)
138 .collect()
139 }
140}
141
142pub fn check_bundle(bundle: &super::proof_term::ProofBundle, ir: &IRProgram) -> BundleReport {
149 let results = bundle
150 .proofs
151 .iter()
152 .enumerate()
153 .map(|(index, proof)| ProofCheck {
154 index,
155 property: proof.property.clone(),
156 subject: proof.witness.subject_name().to_string(),
157 outcome: check_proof(proof, ir),
158 })
159 .collect();
160 BundleReport { results }
161}
162
163fn check_compliance_coverage(
171 claimed: &super::proof_term::ComplianceCoverageWitness,
172 ir: &IRProgram,
173) -> CheckOutcome {
174 let ep = match ir.endpoints.iter().find(|e| e.name == claimed.endpoint_name) {
177 Some(e) => e,
178 None => {
179 return CheckOutcome::Refuted {
180 reason: format!(
181 "endpoint '{}' not present in artifact",
182 claimed.endpoint_name
183 ),
184 }
185 }
186 };
187
188 let actual = derive_compliance_coverage_witness(
190 &ep.name,
191 &ep.compliance,
192 &ep.shield_ref,
193 ir,
194 );
195
196 if actual != *claimed {
200 return CheckOutcome::Refuted {
201 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
202 .to_string(),
203 };
204 }
205
206 if !actual.shield_present {
208 return CheckOutcome::Refuted {
209 reason: format!(
210 "endpoint '{}' declares compliance {:?} but has no resolvable shield (shield_ref={:?})",
211 actual.endpoint_name, actual.required_classes, actual.shield_ref
212 ),
213 };
214 }
215 if !actual.unknown_classes.is_empty() {
216 return CheckOutcome::Refuted {
217 reason: format!(
218 "endpoint '{}' declares unknown regulatory class(es) {:?} (not in the closed registry)",
219 actual.endpoint_name, actual.unknown_classes
220 ),
221 };
222 }
223 if !actual.uncovered_classes.is_empty() {
224 return CheckOutcome::Refuted {
225 reason: format!(
226 "endpoint '{}' requires regulatory class(es) {:?} that its shield '{}' does not provide (shield provides {:?})",
227 actual.endpoint_name,
228 actual.uncovered_classes,
229 actual.shield_ref,
230 actual.provided_classes
231 ),
232 };
233 }
234 CheckOutcome::Verified
235}
236
237fn check_effect_row_soundness(
243 claimed: &super::proof_term::EffectRowSoundnessWitness,
244 ir: &IRProgram,
245) -> CheckOutcome {
246 let tool = match ir.tools.iter().find(|t| t.name == claimed.tool_name) {
247 Some(t) => t,
248 None => {
249 return CheckOutcome::Refuted {
250 reason: format!(
251 "tool '{}' not present in artifact",
252 claimed.tool_name
253 ),
254 }
255 }
256 };
257
258 let ext_members = super::generate::extension_effect_members(ir);
265 let actual =
266 derive_effect_row_soundness_witness(&tool.name, &tool.effect_row, &ext_members);
267
268 if actual != *claimed {
269 return CheckOutcome::Refuted {
270 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
271 .to_string(),
272 };
273 }
274
275 if !actual.unknown_bases.is_empty() {
277 return CheckOutcome::Refuted {
278 reason: format!(
279 "tool '{}' declares effect entr(ies) with unknown base(s) {:?} (not in the closed effect catalog)",
280 actual.tool_name, actual.unknown_bases
281 ),
282 };
283 }
284 if !actual.missing_qualifier.is_empty() {
285 return CheckOutcome::Refuted {
286 reason: format!(
287 "tool '{}' declares qualifier-requiring effect(s) {:?} without a qualifier (bare stream/trust is unenforceable)",
288 actual.tool_name, actual.missing_qualifier
289 ),
290 };
291 }
292 if !actual.invalid_stream_qualifier.is_empty() {
293 return CheckOutcome::Refuted {
294 reason: format!(
295 "tool '{}' declares stream effect(s) {:?} with an invalid backpressure policy qualifier",
296 actual.tool_name, actual.invalid_stream_qualifier
297 ),
298 };
299 }
300 if actual.purity_violation {
301 return CheckOutcome::Refuted {
302 reason: format!(
303 "tool '{}' declares `pure` alongside other effects {:?} — a pure tool cannot be effectful",
304 actual.tool_name, actual.declared_effects
305 ),
306 };
307 }
308 CheckOutcome::Verified
309}
310
311fn check_capability_isolation(
318 claimed: &super::proof_term::CapabilityIsolationWitness,
319 ir: &IRProgram,
320) -> CheckOutcome {
321 let store = match ir
322 .axonstore_specs
323 .iter()
324 .find(|s| s.name == claimed.store_name)
325 {
326 Some(s) => s,
327 None => {
328 return CheckOutcome::Refuted {
329 reason: format!(
330 "axonstore '{}' not present in artifact",
331 claimed.store_name
332 ),
333 }
334 }
335 };
336
337 let actual = derive_capability_isolation_witness(&store.name, &store.capability);
339
340 if actual != *claimed {
341 return CheckOutcome::Refuted {
342 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
343 .to_string(),
344 };
345 }
346
347 if actual.malformed {
348 return CheckOutcome::Refuted {
349 reason: format!(
350 "axonstore '{}' declares a malformed capability gate {:?} (not a valid §32.g scope: ^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$)",
351 actual.store_name, actual.capability
352 ),
353 };
354 }
355 CheckOutcome::Verified
356}
357
358fn check_resource_bounds(
365 claimed: &ResourceBoundsWitness,
366 ir: &IRProgram,
367) -> CheckOutcome {
368 match claimed {
369 ResourceBoundsWitness::EndpointRetry { endpoint_name, .. } => {
370 let ep = match ir.endpoints.iter().find(|e| e.name == *endpoint_name) {
371 Some(e) => e,
372 None => {
373 return CheckOutcome::Refuted {
374 reason: format!(
375 "endpoint '{}' not present in artifact",
376 endpoint_name
377 ),
378 }
379 }
380 };
381 let actual = derive_endpoint_retry_witness(&ep.name, ep.retries);
382 if actual != *claimed {
383 return CheckOutcome::Refuted {
384 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
385 .to_string(),
386 };
387 }
388 if let ResourceBoundsWitness::EndpointRetry { retries, in_bounds, .. } = &actual {
389 if !in_bounds {
390 return CheckOutcome::Refuted {
391 reason: format!(
392 "endpoint '{}' declares retries={} outside the bound [0, {}] (negative is nonsensical; above the ceiling is a retry storm)",
393 ep.name,
394 retries,
395 super::proof_term::MAX_RETRIES
396 ),
397 };
398 }
399 }
400 CheckOutcome::Verified
401 }
402 ResourceBoundsWitness::SocketCredit { socket_name, .. } => {
403 let socket = match ir.sockets.iter().find(|s| s.name == *socket_name) {
404 Some(s) => s,
405 None => {
406 return CheckOutcome::Refuted {
407 reason: format!(
408 "socket '{}' not present in artifact",
409 socket_name
410 ),
411 }
412 }
413 };
414 let credit = match socket.backpressure_credit {
418 Some(k) => k,
419 None => {
420 return CheckOutcome::Refuted {
421 reason: format!(
422 "socket '{}' has no declared backpressure credit in the artifact, but the witness claims one (forged or stale proof)",
423 socket_name
424 ),
425 }
426 }
427 };
428 let actual = derive_socket_credit_witness(&socket.name, credit);
429 if actual != *claimed {
430 return CheckOutcome::Refuted {
431 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
432 .to_string(),
433 };
434 }
435 if let ResourceBoundsWitness::SocketCredit { credit, positive, .. } = &actual {
436 if !positive {
437 return CheckOutcome::Refuted {
438 reason: format!(
439 "socket '{}' declares backpressure credit({}) — a window < 1 deadlocks the §Fase 41.b typed-resource gate",
440 socket.name, credit
441 ),
442 };
443 }
444 }
445 CheckOutcome::Verified
446 }
447 }
448}
449
450fn check_shield_halt_guarantee(
457 claimed: &super::proof_term::ShieldHaltGuaranteeWitness,
458 ir: &IRProgram,
459) -> CheckOutcome {
460 let shield = match ir.shields.iter().find(|s| s.name == claimed.shield_name) {
461 Some(s) => s,
462 None => {
463 return CheckOutcome::Refuted {
464 reason: format!(
465 "shield '{}' not present in artifact",
466 claimed.shield_name
467 ),
468 }
469 }
470 };
471
472 let actual =
474 derive_shield_halt_witness(&shield.name, &shield.on_breach, &shield.scan);
475
476 if actual != *claimed {
477 return CheckOutcome::Refuted {
478 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
479 .to_string(),
480 };
481 }
482
483 if !actual.known_policy {
484 return CheckOutcome::Refuted {
485 reason: format!(
486 "shield '{}' declares an unknown on_breach policy {:?} (not in the closed breach-policy catalog)",
487 actual.shield_name, actual.on_breach
488 ),
489 };
490 }
491 if actual.vacuous_halt {
492 return CheckOutcome::Refuted {
493 reason: format!(
494 "shield '{}' declares `on_breach: halt` but an empty `scan: []` — the halt can never fire (no scan ⟹ no breach ⟹ no halt): a vacuous guarantee",
495 actual.shield_name
496 ),
497 };
498 }
499 CheckOutcome::Verified
500}
501
502fn check_capability_containment(
510 claimed: &super::proof_term::CapabilityContainmentWitness,
511 ir: &IRProgram,
512) -> CheckOutcome {
513 let ep = match ir.endpoints.iter().find(|e| e.name == claimed.endpoint_name) {
514 Some(e) => e,
515 None => {
516 return CheckOutcome::Refuted {
517 reason: format!(
518 "endpoint '{}' not present in artifact",
519 claimed.endpoint_name
520 ),
521 }
522 }
523 };
524
525 let actual = derive_capability_containment_witness(
527 &ep.name,
528 &ep.execute_flow,
529 &ep.requires_capabilities,
530 ir,
531 );
532
533 if actual != *claimed {
534 return CheckOutcome::Refuted {
535 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
536 .to_string(),
537 };
538 }
539
540 if !actual.flow_resolved {
544 return CheckOutcome::Refuted {
545 reason: format!(
546 "endpoint '{}' executes flow '{}' which is not present in the artifact — cannot certify capability containment",
547 actual.endpoint_name, actual.execute_flow
548 ),
549 };
550 }
551 if !actual.uncovered_gates.is_empty() {
552 return CheckOutcome::Refuted {
553 reason: format!(
554 "endpoint '{}' reaches store(s) gated by capabilit(ies) {:?} that its declared `requires:` {:?} does not cover — a capability leak (a request satisfying the declared requires could reach a store it is not authorized for)",
555 actual.endpoint_name, actual.uncovered_gates, actual.declared_requires
556 ),
557 };
558 }
559 CheckOutcome::Verified
560}
561
562fn check_tool_call_soundness(
570 claimed: &super::proof_term::ToolCallSoundnessWitness,
571 ir: &IRProgram,
572) -> CheckOutcome {
573 let actual = match derive_tool_call_soundness_witness(
577 &claimed.flow_name,
578 claimed.call_index,
579 ir,
580 ) {
581 Some(w) => w,
582 None => {
583 return CheckOutcome::Refuted {
584 reason: format!(
585 "flow '{}' has no structured `use` call at index {} in this artifact",
586 claimed.flow_name, claimed.call_index
587 ),
588 }
589 }
590 };
591
592 if actual != *claimed {
593 return CheckOutcome::Refuted {
594 reason: "witness disagrees with artifact re-derivation (forged or stale proof)"
595 .to_string(),
596 };
597 }
598
599 if !actual.schema_present {
601 return CheckOutcome::Verified;
606 }
607 if !actual.unknown_args.is_empty() {
608 return CheckOutcome::Refuted {
609 reason: format!(
610 "call to tool '{}' in flow '{}' passes argument(s) {:?} the tool does not declare (declared parameters: {:?})",
611 actual.tool_name, actual.flow_name, actual.unknown_args, actual.declared_params
612 ),
613 };
614 }
615 if !actual.duplicate_args.is_empty() {
616 return CheckOutcome::Refuted {
617 reason: format!(
618 "call to tool '{}' in flow '{}' supplies duplicate argument(s) {:?}",
619 actual.tool_name, actual.flow_name, actual.duplicate_args
620 ),
621 };
622 }
623 if !actual.missing_required.is_empty() {
624 return CheckOutcome::Refuted {
625 reason: format!(
626 "call to tool '{}' in flow '{}' omits required argument(s) {:?}",
627 actual.tool_name, actual.flow_name, actual.missing_required
628 ),
629 };
630 }
631 if !actual.type_mismatches.is_empty() {
632 return CheckOutcome::Refuted {
633 reason: format!(
634 "call to tool '{}' in flow '{}' has literal-argument type mismatch(es) {:?} (each `arg:expected:got`)",
635 actual.tool_name, actual.flow_name, actual.type_mismatches
636 ),
637 };
638 }
639 CheckOutcome::Verified
640}