1use cortex_core::{
8 AllowedClaimLanguage, AxiomConstraint, AxiomConstraintKind, AxiomConstraintSeverity,
9 BoundaryContradictionState, BoundaryQuarantineState, BoundaryRedactionState, ClaimCeiling,
10 ClaimProofState, ContextPackId, CortexAxiomConstraintEnvelopeV1, FailingEdge, PolicyOutcome,
11 ProofClosureReport, ProofEdgeFailure, ProofEdgeKind, ProvenanceClass,
12};
13use serde::{Deserialize, Serialize};
14
15use crate::pack::{ContextPack, ContextRefId};
16use crate::redaction::{ContentRedaction, PackMode, RawEventPayloadPolicy};
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct AxiomContextExport {
21 pub context_pack_id: ContextPackId,
23 pub task: String,
25 pub claim_ceiling: ClaimCeiling,
27 pub policy_outcome: PolicyOutcome,
29 pub unknown_refs: Vec<ContextRefId>,
31 pub limited_proof_refs: Vec<ContextRefId>,
33 pub conflict_refs: Vec<String>,
35 pub redaction_limits: Vec<String>,
37 pub constraints: Vec<AxiomConstraint>,
39}
40
41#[must_use]
43pub fn axiom_export_for_pack(pack: &ContextPack) -> AxiomContextExport {
44 let unknown_refs = pack
45 .selected_refs
46 .iter()
47 .filter(|selected| selected.proof_state == ClaimProofState::Unknown)
48 .map(|selected| selected.ref_id.clone())
49 .collect::<Vec<_>>();
50 let limited_proof_refs = pack
51 .selected_refs
52 .iter()
53 .filter(|selected| selected.proof_state != ClaimProofState::FullChainVerified)
54 .map(|selected| selected.ref_id.clone())
55 .collect::<Vec<_>>();
56 let conflict_refs = pack
57 .conflicts
58 .iter()
59 .map(|conflict| conflict.contradiction_id.to_string())
60 .collect::<Vec<_>>();
61 let redaction_limits = redaction_limits(pack);
62 let claim_ceiling = pack_claim_ceiling(pack);
63 let policy = pack.policy_decision();
64 let mut constraints = Vec::new();
65
66 constraints.push(AxiomConstraint::new(
67 AxiomConstraintKind::NoExecutionAuthority,
68 AxiomConstraintSeverity::Hard,
69 "context pack constraints do not grant execution authority or tool permission",
70 ));
71 constraints.push(AxiomConstraint::new(
72 AxiomConstraintKind::TruthCeiling,
73 AxiomConstraintSeverity::Limit,
74 format!("AXIOM output must not claim above {claim_ceiling:?} for this pack"),
75 ));
76
77 if !unknown_refs.is_empty() {
78 constraints.push(
79 AxiomConstraint::new(
80 AxiomConstraintKind::ProofStateLimit,
81 AxiomConstraintSeverity::Limit,
82 "one or more selected refs have unknown proof state",
83 )
84 .with_refs(refs_to_strings(&unknown_refs)),
85 );
86 }
87
88 if !limited_proof_refs.is_empty() {
89 constraints.push(
90 AxiomConstraint::new(
91 AxiomConstraintKind::ForbidPromotionShapedOutput,
92 AxiomConstraintSeverity::Hard,
93 "limited proof state forbids promotion-shaped output",
94 )
95 .with_refs(refs_to_strings(&limited_proof_refs)),
96 );
97 }
98
99 if !conflict_refs.is_empty() {
100 constraints.push(
101 AxiomConstraint::new(
102 AxiomConstraintKind::ConflictPresent,
103 AxiomConstraintSeverity::Hard,
104 "context pack contains unresolved or surfaced conflicts",
105 )
106 .with_refs(conflict_refs.clone()),
107 );
108 constraints.push(AxiomConstraint::new(
109 AxiomConstraintKind::ForbidPromotionShapedOutput,
110 AxiomConstraintSeverity::Hard,
111 "conflicting context forbids promotion-shaped output",
112 ));
113 }
114
115 if claim_ceiling < ClaimCeiling::SignedLocalLedger {
116 constraints.push(AxiomConstraint::new(
117 AxiomConstraintKind::LowTrust,
118 AxiomConstraintSeverity::Hard,
119 "low trust or unsigned context forbids durable authority claims",
120 ));
121 constraints.push(AxiomConstraint::new(
122 AxiomConstraintKind::ForbidPromotionShapedOutput,
123 AxiomConstraintSeverity::Hard,
124 "low-trust context forbids promotion-shaped output",
125 ));
126 }
127
128 if !redaction_limits.is_empty() {
129 constraints.push(AxiomConstraint::new(
130 AxiomConstraintKind::RedactionBoundary,
131 AxiomConstraintSeverity::Hard,
132 "redaction policy forbids inferring or reconstructing omitted raw context",
133 ));
134 }
135
136 AxiomContextExport {
137 context_pack_id: pack.context_pack_id,
138 task: pack.task.clone(),
139 claim_ceiling,
140 policy_outcome: policy.final_outcome,
141 unknown_refs,
142 limited_proof_refs,
143 conflict_refs,
144 redaction_limits,
145 constraints,
146 }
147}
148
149#[must_use]
155pub fn constraint_envelope_for_pack(pack: &ContextPack) -> CortexAxiomConstraintEnvelopeV1 {
156 let export = axiom_export_for_pack(pack);
157 let proof_report = proof_report_for_pack(pack);
158 let provenance_class = weakest_provenance_for_pack(pack);
159 let semantic_trust = weakest_semantic_trust_for_pack(pack);
160
161 let mut envelope = CortexAxiomConstraintEnvelopeV1::new(
162 pack.context_pack_id,
163 proof_report,
164 export.claim_ceiling,
165 semantic_trust,
166 provenance_class,
167 );
168 envelope.contradiction_state = contradiction_state_for_pack(pack);
169 envelope.quarantine_state = quarantine_state_for_pack(pack, export.policy_outcome);
170 envelope.redaction_state = redaction_state_for_pack(pack);
171 envelope.allowed_claim_language =
172 allowed_claim_language_for_quarantine(envelope.quarantine_state);
173 envelope.constraints = export.constraints;
174 envelope
175}
176
177fn pack_claim_ceiling(pack: &ContextPack) -> ClaimCeiling {
178 let selected_ceiling = ClaimCeiling::weakest(pack.selected_refs.iter().map(|selected| {
179 selected
180 .claim_ceiling
181 .min(selected.proof_state.claim_ceiling())
182 .min(selected.runtime_mode.claim_ceiling())
183 .min(selected.authority_class.claim_ceiling())
184 .min(selected.provenance_class.claim_ceiling())
185 .min(selected.semantic_trust.claim_ceiling())
186 }))
187 .unwrap_or(ClaimCeiling::DevOnly);
188
189 if pack.conflicts.is_empty() {
190 selected_ceiling
191 } else {
192 selected_ceiling.min(ClaimCeiling::DevOnly)
193 }
194}
195
196fn proof_report_for_pack(pack: &ContextPack) -> ProofClosureReport {
197 if pack
198 .selected_refs
199 .iter()
200 .all(|selected| selected.proof_state == ClaimProofState::FullChainVerified)
201 {
202 return ProofClosureReport::full_chain_verified(Vec::new());
203 }
204
205 let mut failures = Vec::new();
206 for selected in &pack.selected_refs {
207 match selected.proof_state {
208 ClaimProofState::FullChainVerified => {}
209 ClaimProofState::Broken => failures.push(FailingEdge::broken(
210 ProofEdgeKind::ContextPackLink,
211 ref_to_string(&selected.ref_id),
212 pack.context_pack_id.to_string(),
213 ProofEdgeFailure::Mismatch,
214 "selected context ref has broken proof state",
215 )),
216 ClaimProofState::Partial => failures.push(FailingEdge::unresolved(
217 ProofEdgeKind::ContextPackLink,
218 ref_to_string(&selected.ref_id),
219 "selected context ref has partial proof state",
220 )),
221 ClaimProofState::Unknown => failures.push(FailingEdge::missing(
222 ProofEdgeKind::ContextPackLink,
223 ref_to_string(&selected.ref_id),
224 "selected context ref has unknown proof state",
225 )),
226 }
227 }
228
229 ProofClosureReport::from_edges(Vec::new(), failures)
230}
231
232fn weakest_provenance_for_pack(pack: &ContextPack) -> ProvenanceClass {
233 pack.selected_refs
234 .iter()
235 .map(|selected| selected.provenance_class)
236 .min()
237 .unwrap_or(ProvenanceClass::UnknownProvenance)
238}
239
240fn weakest_semantic_trust_for_pack(pack: &ContextPack) -> cortex_core::SemanticTrustClass {
241 pack.selected_refs
242 .iter()
243 .map(|selected| selected.semantic_trust)
244 .min()
245 .unwrap_or(cortex_core::SemanticTrustClass::Unknown)
246}
247
248fn contradiction_state_for_pack(pack: &ContextPack) -> BoundaryContradictionState {
249 pack.contradiction_posture()
250}
251
252fn quarantine_state_for_pack(
253 pack: &ContextPack,
254 policy_outcome: PolicyOutcome,
255) -> BoundaryQuarantineState {
256 match policy_outcome {
257 PolicyOutcome::Reject => BoundaryQuarantineState::Contaminated,
258 PolicyOutcome::Quarantine => BoundaryQuarantineState::Quarantined,
259 PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => {
260 if pack.conflicts.is_empty() {
261 BoundaryQuarantineState::Clean
262 } else {
263 BoundaryQuarantineState::DiagnosticOnly
264 }
265 }
266 }
267}
268
269fn allowed_claim_language_for_quarantine(
270 quarantine_state: BoundaryQuarantineState,
271) -> Vec<AllowedClaimLanguage> {
272 match quarantine_state {
273 BoundaryQuarantineState::Clean | BoundaryQuarantineState::DiagnosticOnly => {
274 cortex_core::default_allowed_claim_language()
275 }
276 BoundaryQuarantineState::Quarantined | BoundaryQuarantineState::Contaminated => vec![
277 AllowedClaimLanguage::Constraint,
278 AllowedClaimLanguage::ResidualRisk,
279 AllowedClaimLanguage::VerificationRequest,
280 AllowedClaimLanguage::Refusal,
281 ],
282 }
283}
284
285fn redaction_state_for_pack(pack: &ContextPack) -> BoundaryRedactionState {
286 if pack.pack_mode == PackMode::Operator
287 && pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::OperatorOptIn
288 {
289 BoundaryRedactionState::RawOperatorOptIn
290 } else if pack.redaction_policy.content == ContentRedaction::Abstracted {
291 BoundaryRedactionState::Abstracted
292 } else if pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::Excluded {
293 BoundaryRedactionState::Redacted
294 } else {
295 BoundaryRedactionState::ExportSafe
296 }
297}
298
299fn redaction_limits(pack: &ContextPack) -> Vec<String> {
300 let mut limits = Vec::new();
301 if pack.redaction_policy.content == ContentRedaction::Abstracted {
302 limits.push("content_abstracted".to_string());
303 }
304 if pack.redaction_policy.raw_event_payloads == RawEventPayloadPolicy::Excluded {
305 limits.push("raw_event_payloads_excluded".to_string());
306 }
307 if !pack.exclusions.is_empty() {
308 limits.push("explicit_exclusions_present".to_string());
309 }
310 limits
311}
312
313fn refs_to_strings(refs: &[ContextRefId]) -> Vec<String> {
314 refs.iter().map(ref_to_string).collect()
315}
316
317fn ref_to_string(ref_id: &ContextRefId) -> String {
318 match ref_id {
319 ContextRefId::Memory { memory_id } => format!("memory:{memory_id}"),
320 ContextRefId::Principle { principle_id } => format!("principle:{principle_id}"),
321 ContextRefId::Event { event_id } => format!("event:{event_id}"),
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use cortex_core::{
328 AllowedClaimLanguage, AuthorityClass, BoundaryContradictionState, BoundaryQuarantineState,
329 BoundaryRedactionState, ClaimCeiling, ClaimProofState, ContradictionId, EventId,
330 ForbiddenBoundaryUse, ProvenanceClass, RuntimeMode, SemanticTrustClass,
331 CORTEX_TO_AXIOM_CONSTRAINT_ENVELOPE_V1,
332 };
333
334 use super::*;
335 use crate::{ContextPackBuilder, ContextRefCandidate, PackConflict, Sensitivity};
336
337 fn event_ref() -> ContextRefId {
338 ContextRefId::Event {
339 event_id: EventId::new(),
340 }
341 }
342
343 #[test]
344 fn axiom_export_includes_truth_ceiling_and_unknowns() {
345 let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
346 .select_ref(ContextRefCandidate::new(
347 event_ref(),
348 "unverified candidate context",
349 ))
350 .build()
351 .expect("build pack");
352
353 let export = axiom_export_for_pack(&pack);
354
355 assert_eq!(export.claim_ceiling, ClaimCeiling::DevOnly);
356 assert_eq!(export.policy_outcome, PolicyOutcome::Allow);
357 assert_eq!(export.unknown_refs.len(), 1);
358 assert!(export
359 .constraints
360 .iter()
361 .any(|constraint| constraint.kind == AxiomConstraintKind::TruthCeiling));
362 assert!(export
363 .constraints
364 .iter()
365 .any(|constraint| constraint.kind == AxiomConstraintKind::ProofStateLimit));
366 }
367
368 #[test]
369 fn low_trust_conflicting_pack_forbids_promotion_shaped_output() {
370 let ref_id = event_ref();
371 let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
372 .select_ref(
373 ContextRefCandidate::new(ref_id.clone(), "conflicting candidate context")
374 .with_claim_metadata(
375 RuntimeMode::LocalUnsigned,
376 AuthorityClass::Derived,
377 ClaimProofState::Partial,
378 ClaimCeiling::AuthorityGrade,
379 ),
380 )
381 .conflict(PackConflict {
382 contradiction_id: ContradictionId::new(),
383 posture: BoundaryContradictionState::Blocked,
384 refs: vec![ref_id],
385 summary: "candidate context conflicts with another memory".to_string(),
386 })
387 .build()
388 .expect("build pack");
389
390 let export = axiom_export_for_pack(&pack);
391
392 assert_eq!(export.claim_ceiling, ClaimCeiling::DevOnly);
393 assert_eq!(export.policy_outcome, PolicyOutcome::Quarantine);
394 assert!(!export.conflict_refs.is_empty());
395 assert!(export.constraints.iter().any(|constraint| {
396 constraint.kind == AxiomConstraintKind::ForbidPromotionShapedOutput
397 && constraint.severity == AxiomConstraintSeverity::Hard
398 }));
399 }
400
401 #[test]
402 fn exported_constraints_do_not_grant_execution_authority() {
403 let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
404 .select_ref(
405 ContextRefCandidate::new(event_ref(), "verified context")
406 .with_claim_metadata(
407 RuntimeMode::AuthorityGrade,
408 AuthorityClass::Operator,
409 ClaimProofState::FullChainVerified,
410 ClaimCeiling::AuthorityGrade,
411 )
412 .with_semantic_metadata(
413 ProvenanceClass::OperatorAttested,
414 SemanticTrustClass::FalsificationTested,
415 ),
416 )
417 .build()
418 .expect("build pack");
419
420 let export = axiom_export_for_pack(&pack);
421
422 assert!(export.constraints.iter().any(|constraint| {
423 constraint.kind == AxiomConstraintKind::NoExecutionAuthority
424 && constraint.severity == AxiomConstraintSeverity::Hard
425 }));
426 assert_eq!(export.claim_ceiling, ClaimCeiling::AuthorityGrade);
427 }
428
429 #[test]
430 fn redaction_policy_exports_boundary_constraints() {
431 let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
432 .select_ref(
433 ContextRefCandidate::new(event_ref(), "private context")
434 .with_sensitivity(Sensitivity::Personal),
435 )
436 .build()
437 .expect("build pack");
438
439 let export = axiom_export_for_pack(&pack);
440
441 assert!(export
442 .redaction_limits
443 .contains(&"raw_event_payloads_excluded".to_string()));
444 assert!(export
445 .constraints
446 .iter()
447 .any(|constraint| constraint.kind == AxiomConstraintKind::RedactionBoundary));
448 }
449
450 #[test]
451 fn constraint_envelope_for_conflicted_pack_blocks_authority_uses() {
452 let ref_id = event_ref();
453 let pack = ContextPackBuilder::new("prepare constrained AXIOM work", 512)
454 .select_ref(
455 ContextRefCandidate::new(ref_id.clone(), "conflicted candidate context")
456 .with_claim_metadata(
457 RuntimeMode::LocalUnsigned,
458 AuthorityClass::Derived,
459 ClaimProofState::Partial,
460 ClaimCeiling::AuthorityGrade,
461 ),
462 )
463 .conflict(PackConflict {
464 contradiction_id: ContradictionId::new(),
465 posture: BoundaryContradictionState::MultiHypothesis,
466 refs: vec![ref_id],
467 summary: "candidate context conflicts with another memory".to_string(),
468 })
469 .build()
470 .expect("build pack");
471
472 let envelope = constraint_envelope_for_pack(&pack);
473
474 assert_eq!(
475 envelope.envelope_type,
476 CORTEX_TO_AXIOM_CONSTRAINT_ENVELOPE_V1
477 );
478 assert_eq!(
479 envelope.contradiction_state,
480 BoundaryContradictionState::MultiHypothesis
481 );
482 assert_eq!(
483 envelope.quarantine_state,
484 BoundaryQuarantineState::Quarantined
485 );
486 assert_eq!(envelope.redaction_state, BoundaryRedactionState::Abstracted);
487 assert_eq!(envelope.semantic_trust, SemanticTrustClass::CandidateOnly);
488 assert_eq!(envelope.provenance_class, ProvenanceClass::RuntimeDerived);
489 assert_eq!(envelope.truth_ceiling, ClaimCeiling::DevOnly);
490 assert!(envelope
491 .forbidden_uses
492 .contains(&ForbiddenBoundaryUse::Promotion));
493 assert!(envelope
494 .forbidden_uses
495 .contains(&ForbiddenBoundaryUse::TrustedHistory));
496 assert!(envelope
497 .forbidden_uses
498 .contains(&ForbiddenBoundaryUse::Release));
499 assert!(!envelope
500 .allowed_claim_language
501 .contains(&AllowedClaimLanguage::CandidateClaim));
502 assert!(!envelope
503 .allowed_claim_language
504 .contains(&AllowedClaimLanguage::EvidenceReference));
505 assert!(envelope
506 .allowed_claim_language
507 .contains(&AllowedClaimLanguage::VerificationRequest));
508 assert!(envelope
509 .allowed_claim_language
510 .contains(&AllowedClaimLanguage::Refusal));
511 }
512
513 #[test]
514 fn constraint_envelope_carries_operator_raw_redaction_state() {
515 let pack = ContextPackBuilder::new("prepare operator-only AXIOM work", 512)
516 .pack_mode(crate::PackMode::Operator)
517 .include_raw_event_payloads_in_operator_mode()
518 .select_ref(
519 ContextRefCandidate::new(event_ref(), "operator raw context")
520 .with_sensitivity(Sensitivity::Internal)
521 .with_raw_event_payload(serde_json::json!({"payload": "operator local"}))
522 .with_claim_metadata(
523 RuntimeMode::AuthorityGrade,
524 AuthorityClass::Operator,
525 ClaimProofState::FullChainVerified,
526 ClaimCeiling::AuthorityGrade,
527 )
528 .with_semantic_metadata(
529 ProvenanceClass::OperatorAttested,
530 SemanticTrustClass::FalsificationTested,
531 ),
532 )
533 .build()
534 .expect("build pack");
535
536 let envelope = constraint_envelope_for_pack(&pack);
537
538 assert_eq!(
539 envelope.redaction_state,
540 BoundaryRedactionState::RawOperatorOptIn
541 );
542 assert_eq!(envelope.provenance_class, ProvenanceClass::OperatorAttested);
543 assert_eq!(
544 envelope.semantic_trust,
545 SemanticTrustClass::FalsificationTested
546 );
547 }
548
549 #[test]
550 fn rejected_pack_allows_only_diagnostic_claim_language() {
551 let mut pack = ContextPackBuilder::new("prepare rejected AXIOM work", 512)
552 .select_ref(
553 ContextRefCandidate::new(event_ref(), "raw external context")
554 .with_raw_event_payload(serde_json::json!({"payload": "must not leak"})),
555 )
556 .build()
557 .expect("build pack");
558 pack.redaction_policy.raw_event_payloads = RawEventPayloadPolicy::OperatorOptIn;
559
560 let envelope = constraint_envelope_for_pack(&pack);
561
562 assert_eq!(
563 envelope.quarantine_state,
564 BoundaryQuarantineState::Contaminated
565 );
566 assert!(!envelope
567 .allowed_claim_language
568 .contains(&AllowedClaimLanguage::CandidateClaim));
569 assert!(!envelope
570 .allowed_claim_language
571 .contains(&AllowedClaimLanguage::EvidenceReference));
572 assert!(envelope
573 .allowed_claim_language
574 .contains(&AllowedClaimLanguage::Constraint));
575 assert!(envelope
576 .allowed_claim_language
577 .contains(&AllowedClaimLanguage::ResidualRisk));
578 assert!(envelope
579 .allowed_claim_language
580 .contains(&AllowedClaimLanguage::VerificationRequest));
581 assert!(envelope
582 .allowed_claim_language
583 .contains(&AllowedClaimLanguage::Refusal));
584 }
585}