1use crate::jtbd::JTBDMetadata;
14use crate::{CompileError, compile_intent, parse_truth_document};
15use chrono::{DateTime, Duration, Utc};
16use converge_pack::{ContextFact, FactEvidenceRef, FactTraceLink};
17use organism_pack::{ForbiddenAction, IntentPacket};
18use serde::{Deserialize, Serialize};
19use serde_json::json;
20use sha2::{Digest, Sha256};
21use std::collections::{BTreeMap, BTreeSet};
22use uuid::Uuid;
23
24const DECODER_VERSION: &str = "0.10.0";
25const TRUTH_VERSION: &str = "v1";
26const DETERMINISTIC_EXPIRES_EPOCH: &str = "2099-01-01T00:00:00Z";
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct TimeBudget(u64);
42
43impl TimeBudget {
44 pub const fn from_seconds(seconds: u64) -> Self {
45 Self(seconds)
46 }
47
48 pub const fn from_minutes(minutes: u64) -> Self {
49 Self(minutes * 60)
50 }
51
52 pub const fn from_hours(hours: u64) -> Self {
53 Self(hours * 3600)
54 }
55
56 pub const fn as_seconds(self) -> u64 {
57 self.0
58 }
59}
60
61fn deterministic_expires_line(budget: Option<TimeBudget>) -> String {
62 let epoch: DateTime<Utc> = DateTime::parse_from_rfc3339(DETERMINISTIC_EXPIRES_EPOCH)
63 .expect("DETERMINISTIC_EXPIRES_EPOCH is a valid RFC-3339 timestamp")
64 .with_timezone(&Utc);
65 let expires = match budget {
66 Some(b) => {
67 let seconds = i64::try_from(b.as_seconds()).unwrap_or(i64::MAX);
68 epoch + Duration::seconds(seconds)
69 }
70 None => epoch,
71 };
72 expires.format("%Y-%m-%dT%H:%M:%SZ").to_string()
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct JtbdInput {
78 pub key: String,
80 pub actor: String,
82 pub functional_job: String,
84 pub so_that: String,
86 #[serde(default)]
88 pub evidence_required: Vec<ClauseInput>,
89 #[serde(default)]
91 pub failure_modes: Vec<ClauseInput>,
92 #[serde(default)]
98 pub time_budget: Option<TimeBudget>,
99}
100
101impl JtbdInput {
102 pub fn new(
103 key: impl Into<String>,
104 actor: impl Into<String>,
105 functional_job: impl Into<String>,
106 so_that: impl Into<String>,
107 ) -> Self {
108 Self {
109 key: key.into(),
110 actor: actor.into(),
111 functional_job: functional_job.into(),
112 so_that: so_that.into(),
113 evidence_required: Vec::new(),
114 failure_modes: Vec::new(),
115 time_budget: None,
116 }
117 }
118
119 #[must_use]
121 pub fn with_time_budget(mut self, budget: TimeBudget) -> Self {
122 self.time_budget = Some(budget);
123 self
124 }
125
126 pub fn from_metadata(key: impl Into<String>, metadata: &JTBDMetadata) -> Self {
129 Self {
130 key: key.into(),
131 actor: metadata.actor.clone(),
132 functional_job: metadata.job_functional.clone(),
133 so_that: metadata.so_that.clone(),
134 evidence_required: metadata
135 .evidence_required
136 .iter()
137 .cloned()
138 .map(ClauseInput::new)
139 .collect(),
140 failure_modes: metadata
141 .failure_modes
142 .iter()
143 .cloned()
144 .map(ClauseInput::new)
145 .collect(),
146 time_budget: None,
147 }
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct ClauseInput {
156 pub key: Option<String>,
157 pub text: String,
158}
159
160impl ClauseInput {
161 pub fn new(text: impl Into<String>) -> Self {
162 Self {
163 key: None,
164 text: text.into(),
165 }
166 }
167
168 pub fn with_key(key: impl Into<String>, text: impl Into<String>) -> Self {
169 Self {
170 key: Some(key.into()),
171 text: text.into(),
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct JtbdDocument {
179 pub key: String,
180 pub clauses: Vec<JtbdClause>,
181 #[serde(default)]
186 pub time_budget: Option<TimeBudget>,
187}
188
189impl JtbdDocument {
190 pub fn from_input(input: JtbdInput) -> Result<Self, TruthPackageError> {
191 let key = normalized_key(&input.key, "job");
192 let mut clauses = vec![
193 scalar_clause(&key, JtbdClauseKind::Actor, "actor", input.actor)?,
194 scalar_clause(
195 &key,
196 JtbdClauseKind::FunctionalJob,
197 "functional_job",
198 input.functional_job,
199 )?,
200 scalar_clause(&key, JtbdClauseKind::SoThat, "so_that", input.so_that)?,
201 ];
202
203 clauses.extend(collection_clauses(
204 &key,
205 JtbdClauseKind::EvidenceRequired,
206 "evidence",
207 input.evidence_required,
208 )?);
209 clauses.extend(collection_clauses(
210 &key,
211 JtbdClauseKind::FailureMode,
212 "failure",
213 input.failure_modes,
214 )?);
215
216 ensure_unique_clause_ids(&clauses)?;
217
218 Ok(Self {
219 key,
220 clauses,
221 time_budget: input.time_budget,
222 })
223 }
224
225 pub fn clause_ids(&self) -> impl Iterator<Item = &ClauseId> {
226 self.clauses.iter().map(|clause| &clause.id)
227 }
228
229 pub fn clause(&self, id: &ClauseId) -> Option<&JtbdClause> {
230 self.clauses.iter().find(|clause| &clause.id == id)
231 }
232
233 pub fn clauses_by_kind(&self, kind: JtbdClauseKind) -> impl Iterator<Item = &JtbdClause> {
234 self.clauses
235 .iter()
236 .filter(move |clause| clause.kind == kind)
237 }
238
239 fn known_clause_ids(&self) -> BTreeSet<ClauseId> {
240 self.clause_ids().cloned().collect()
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct JtbdClause {
247 pub id: ClauseId,
248 pub kind: JtbdClauseKind,
249 pub key: String,
250 pub text: String,
251 pub canonical_text: String,
252 pub fingerprint: ClauseFingerprint,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258pub enum JtbdClauseKind {
259 Actor,
260 FunctionalJob,
261 SoThat,
262 EvidenceRequired,
263 FailureMode,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
268#[serde(transparent)]
269pub struct ClauseId(String);
270
271impl ClauseId {
272 pub fn new(root_key: &str, path: &str) -> Self {
273 Self(format!("jtbd.{root_key}.{path}"))
274 }
275
276 pub fn as_str(&self) -> &str {
277 &self.0
278 }
279}
280
281impl std::fmt::Display for ClauseId {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 f.write_str(&self.0)
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
289#[serde(transparent)]
290pub struct ClauseFingerprint(String);
291
292impl ClauseFingerprint {
293 pub fn from_text(text: &str) -> Self {
294 let canonical = canonicalize_clause_text(text);
295 let mut hasher = Sha256::new();
296 hasher.update(canonical.as_bytes());
297 Self(hex_lower(&hasher.finalize()))
298 }
299
300 pub fn as_str(&self) -> &str {
301 &self.0
302 }
303
304 pub fn short(&self) -> &str {
305 &self.0[..12]
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
311#[serde(transparent)]
312pub struct ArtifactId(String);
313
314impl ArtifactId {
315 pub fn new(value: impl Into<String>) -> Self {
316 Self(value.into())
317 }
318
319 pub fn as_str(&self) -> &str {
320 &self.0
321 }
322}
323
324impl std::fmt::Display for ArtifactId {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 f.write_str(&self.0)
327 }
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
332#[serde(transparent)]
333pub struct TruthPackageId(String);
334
335impl TruthPackageId {
336 pub fn new(value: impl Into<String>) -> Self {
337 Self(value.into())
338 }
339
340 pub fn as_str(&self) -> &str {
341 &self.0
342 }
343}
344
345impl std::fmt::Display for TruthPackageId {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 f.write_str(&self.0)
348 }
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum ArtifactKind {
355 TruthPackageManifest,
356 TruthProjection,
357 Scenario,
358 Predicate,
359 PolicyRequirement,
360 InvariantArtifact,
361 SimulationCase,
362 CalibrationSuggestion,
363 CalibrationConcern,
364 ReplayProfile,
365 IntentField,
366 ProofObligation,
367 VerifierExpectation,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct TruthPackage {
373 pub package_id: TruthPackageId,
374 pub truth_version: String,
375 pub source_jtbd: JtbdDocument,
376 pub generated_truths: String,
377 pub artifacts: TruthPackageArtifacts,
378 pub intent_packet: IntentPacket,
379 pub proof_obligations: Vec<ProofObligation>,
380 pub verifier_spec: VerifierSpec,
381 pub replay_profile: ReplayProfile,
382 pub lineage: LineageMap,
383}
384
385impl TruthPackage {
386 pub fn base_projection(&self) -> TruthProjectionVersion {
388 let source_clause_ids: Vec<ClauseId> = self.source_jtbd.clause_ids().cloned().collect();
389 TruthProjectionVersion {
390 package_id: self.package_id.clone(),
391 base_truth_version: self.truth_version.clone(),
392 projection_version: self.truth_version.clone(),
393 truths: self.generated_truths.clone(),
394 source: TruthProjectionSource::BaseGenerated,
395 lineage: ArtifactLineage::new(
396 ArtifactId::new(format!("truth_projection.{}", self.source_jtbd.key)),
397 ArtifactKind::TruthProjection,
398 source_clause_ids,
399 "truth_projection.v0",
400 DECODER_VERSION,
401 &self.source_jtbd,
402 ),
403 }
404 }
405
406 pub fn apply_projection_overlay(
410 &self,
411 overlay: TruthProjectionOverlay,
412 ) -> Result<TruthProjectionVersion, TruthOverlayError> {
413 apply_truth_projection_overlay(self, overlay)
414 }
415
416 pub fn verify(&self, observation: &AxiomRunObservation) -> AxiomRunVerdict {
439 compute_verdict(self, observation)
440 }
441}
442
443#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
450pub struct TruthPackageArtifacts {
451 pub scenarios: Vec<GeneratedArtifact>,
452 pub predicates: Vec<GeneratedArtifact>,
453 pub policy_requirements: Vec<GeneratedArtifact>,
454 pub evidence_expectations: Vec<GeneratedArtifact>,
455 pub simulation_cases: Vec<GeneratedArtifact>,
456 pub invariant_expectations: Vec<GeneratedArtifact>,
457 #[serde(default)]
458 pub calibration_suggestions: Vec<GeneratedArtifact>,
459 #[serde(default)]
465 pub calibration_concerns: Vec<GeneratedArtifact>,
466}
467
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470pub struct GeneratedArtifact {
471 pub artifact_id: ArtifactId,
472 pub artifact_kind: ArtifactKind,
473 pub summary: String,
474 pub source_clause_ids: Vec<ClauseId>,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483pub struct TruthProjectionOverlay {
484 pub overlay_id: ArtifactId,
485 pub target_package_id: TruthPackageId,
486 pub target_truth_version: String,
487 pub projection_version: String,
488 pub edited_truths: String,
489 pub reason: String,
490 pub source_clause_ids: Vec<ClauseId>,
491}
492
493impl TruthProjectionOverlay {
494 pub fn new(
495 target_package_id: TruthPackageId,
496 target_truth_version: impl Into<String>,
497 projection_version: impl Into<String>,
498 edited_truths: impl Into<String>,
499 reason: impl Into<String>,
500 source_clause_ids: Vec<ClauseId>,
501 ) -> Self {
502 let target_truth_version = target_truth_version.into();
503 let projection_version = projection_version.into();
504 let edited_truths = edited_truths.into();
505 let overlay_id = overlay_id_for(
506 &target_package_id,
507 &target_truth_version,
508 &projection_version,
509 &edited_truths,
510 );
511 Self {
512 overlay_id,
513 target_package_id,
514 target_truth_version,
515 projection_version,
516 edited_truths,
517 reason: reason.into(),
518 source_clause_ids,
519 }
520 }
521}
522
523#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526pub struct TruthProjectionVersion {
527 pub package_id: TruthPackageId,
528 pub base_truth_version: String,
529 pub projection_version: String,
530 pub truths: String,
531 pub source: TruthProjectionSource,
532 pub lineage: ArtifactLineage,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "snake_case")]
537pub enum TruthProjectionSource {
538 BaseGenerated,
539 OverlayApplied {
540 overlay_id: ArtifactId,
541 reason: String,
542 },
543}
544
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
548pub struct ProofObligation {
549 pub artifact_id: ArtifactId,
550 pub kind: ProofObligationKind,
551 pub description: String,
552 pub source_clause_ids: Vec<ClauseId>,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
556#[serde(rename_all = "snake_case")]
557pub enum ProofObligationKind {
558 EvidenceRequired,
559 FailureGuard,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct VerifierSpec {
565 pub expected_stop_reasons: BTreeSet<ExpectedStopReason>,
566 pub required_evidence: Vec<String>,
567 pub forbidden_actions: Vec<ForbiddenAction>,
568 pub satisfaction_conditions: Vec<String>,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
572#[serde(rename_all = "snake_case")]
573pub enum ExpectedStopReason {
574 Converged,
575 CriteriaMet,
576 UserCancelled,
577 HumanInterventionRequired,
578 CycleBudgetExhausted,
579 FactBudgetExhausted,
580 TokenBudgetExhausted,
581 TimeBudgetExhausted,
582 InvariantViolated,
583 PromotionRejected,
584 RuntimeError,
585 AgentRefused,
586 HitlGatePending,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
591pub struct ReplayProfile {
592 pub profile_id: ArtifactId,
593 pub deterministic: bool,
594 pub input_clause_ids: Vec<ClauseId>,
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
600#[serde(rename_all = "snake_case")]
601pub enum AxiomRunVerdict {
602 Satisfied,
603 Blocked,
604 Exhausted,
605 Invalid,
606}
607
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
610#[serde(tag = "kind", rename_all = "snake_case")]
611pub enum ObservedStopReason {
612 Converged,
613 CriteriaMet {
614 criteria: Vec<String>,
615 },
616 UserCancelled,
617 HumanInterventionRequired {
618 criteria: Vec<String>,
619 approval_refs: Vec<String>,
620 },
621 CycleBudgetExhausted {
622 cycles_executed: u32,
623 limit: u32,
624 },
625 FactBudgetExhausted {
626 facts_count: u32,
627 limit: u32,
628 },
629 TokenBudgetExhausted {
630 tokens_consumed: u64,
631 limit: u64,
632 },
633 TimeBudgetExhausted {
634 duration_ms: u64,
635 limit_ms: u64,
636 },
637 InvariantViolated {
638 class: String,
639 name: String,
640 reason: String,
641 },
642 PromotionRejected {
643 proposal_id: String,
644 reason: String,
645 },
646 RuntimeError {
647 message: String,
648 category: String,
649 },
650 AgentRefused {
651 agent_id: String,
652 reason: String,
653 },
654 HitlGatePending {
655 gate_id: String,
656 proposal_id: String,
657 summary: String,
658 agent_id: String,
659 cycle: u32,
660 },
661}
662
663impl ObservedStopReason {
664 pub fn expectation_kind(&self) -> ExpectedStopReason {
665 match self {
666 Self::Converged => ExpectedStopReason::Converged,
667 Self::CriteriaMet { .. } => ExpectedStopReason::CriteriaMet,
668 Self::UserCancelled => ExpectedStopReason::UserCancelled,
669 Self::HumanInterventionRequired { .. } => ExpectedStopReason::HumanInterventionRequired,
670 Self::CycleBudgetExhausted { .. } => ExpectedStopReason::CycleBudgetExhausted,
671 Self::FactBudgetExhausted { .. } => ExpectedStopReason::FactBudgetExhausted,
672 Self::TokenBudgetExhausted { .. } => ExpectedStopReason::TokenBudgetExhausted,
673 Self::TimeBudgetExhausted { .. } => ExpectedStopReason::TimeBudgetExhausted,
674 Self::InvariantViolated { .. } => ExpectedStopReason::InvariantViolated,
675 Self::PromotionRejected { .. } => ExpectedStopReason::PromotionRejected,
676 Self::RuntimeError { .. } => ExpectedStopReason::RuntimeError,
677 Self::AgentRefused { .. } => ExpectedStopReason::AgentRefused,
678 Self::HitlGatePending { .. } => ExpectedStopReason::HitlGatePending,
679 }
680 }
681
682 pub fn matches_expected(&self, expected: &BTreeSet<ExpectedStopReason>) -> bool {
683 expected.contains(&self.expectation_kind())
684 }
685
686 pub fn is_budget_exhausted(&self) -> bool {
687 matches!(
688 self,
689 Self::CycleBudgetExhausted { .. }
690 | Self::FactBudgetExhausted { .. }
691 | Self::TokenBudgetExhausted { .. }
692 | Self::TimeBudgetExhausted { .. }
693 )
694 }
695
696 pub fn is_blocked(&self) -> bool {
697 matches!(
698 self,
699 Self::HumanInterventionRequired { .. } | Self::HitlGatePending { .. }
700 )
701 }
702
703 pub fn is_invalid(&self) -> bool {
704 matches!(
705 self,
706 Self::InvariantViolated { .. }
707 | Self::PromotionRejected { .. }
708 | Self::RuntimeError { .. }
709 | Self::AgentRefused { .. }
710 )
711 }
712}
713
714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716pub struct PromotedFactRecord {
717 pub context_key: String,
718 pub fact_id: String,
719 pub summary: String,
720 pub source_clause_ids: Vec<ClauseId>,
721 pub evidence_refs: Vec<EvidenceRefRecord>,
722 pub trace_link: Option<TraceLinkRecord>,
723 #[serde(default)]
724 pub promotion_authority: Option<PromotionAuthorityRecord>,
725}
726
727impl PromotedFactRecord {
728 pub fn from_context_fact(fact: &ContextFact, source_clause_ids: Vec<ClauseId>) -> Self {
729 let promotion = fact.promotion_record();
730 Self {
731 context_key: format!("{:?}", fact.key()),
732 fact_id: fact.id().as_str().to_string(),
733 summary: fact.text().map_or_else(
734 || format!("{} v{}", fact.payload_family(), fact.payload_version()),
735 ToString::to_string,
736 ),
737 source_clause_ids,
738 evidence_refs: promotion
739 .evidence_refs()
740 .iter()
741 .map(evidence_ref_record)
742 .collect(),
743 trace_link: Some(trace_link_record(promotion.trace_link())),
744 promotion_authority: Some(PromotionAuthorityRecord {
745 gate_id: promotion.gate_id().as_str().to_string(),
746 policy_version_hash: promotion.policy_version_hash().to_hex(),
747 approver_id: promotion.approver().id().as_str().to_string(),
748 approver_kind: format!("{:?}", promotion.approver().kind()),
749 }),
750 }
751 }
752}
753
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct EvidenceRefRecord {
757 pub evidence_id: String,
758 pub source: String,
759}
760
761#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
763pub struct TraceLinkRecord {
764 pub trace_id: String,
765 pub location: Option<String>,
766 pub replayable: bool,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
775pub struct PromotionAuthorityRecord {
776 pub gate_id: String,
777 pub policy_version_hash: String,
778 pub approver_id: String,
779 pub approver_kind: String,
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784pub struct RunIntegrityProof {
785 pub merkle_root: String,
786 pub clock_time: u64,
787 pub fact_count: usize,
788 pub algorithm: String,
789}
790
791impl RunIntegrityProof {
792 pub fn sha256_merkle(
793 merkle_root: impl Into<String>,
794 clock_time: u64,
795 fact_count: usize,
796 ) -> Self {
797 Self {
798 merkle_root: merkle_root.into(),
799 clock_time,
800 fact_count,
801 algorithm: "sha256-merkle".to_string(),
802 }
803 }
804}
805
806#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
808pub struct AxiomRunObservation {
809 pub stop_reason: ObservedStopReason,
810 pub promoted_facts: Vec<PromotedFactRecord>,
811 pub integrity: RunIntegrityProof,
812 pub replay_notes: Vec<String>,
813 #[serde(default)]
814 pub run_stages: Vec<AxiomRunStageRecord>,
815}
816
817impl AxiomRunObservation {
818 pub fn from_stages(
819 stop_reason: ObservedStopReason,
820 integrity: RunIntegrityProof,
821 replay_notes: Vec<String>,
822 run_stages: Vec<AxiomRunStageRecord>,
823 ) -> Self {
824 let promoted_facts = run_stages
825 .iter()
826 .flat_map(|stage| stage.promoted_facts.iter().cloned())
827 .collect();
828
829 Self {
830 stop_reason,
831 promoted_facts,
832 integrity,
833 replay_notes,
834 run_stages,
835 }
836 }
837}
838
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847pub struct AxiomRunStageRecord {
848 pub stage_id: String,
849 pub formation_id: Option<String>,
850 pub observed_stop_reason: ObservedStopReason,
851 pub promoted_facts: Vec<PromotedFactRecord>,
852 pub integrity: RunIntegrityProof,
853 pub replay_notes: Vec<String>,
854}
855
856#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
859#[serde(rename_all = "snake_case")]
860pub enum ObservationAdapterStatus {
861 Succeeded,
862 Rejected,
863}
864
865impl ObservationAdapterStatus {
866 pub const fn as_str(self) -> &'static str {
867 match self {
868 Self::Succeeded => "succeeded",
869 Self::Rejected => "rejected",
870 }
871 }
872}
873
874#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880pub struct ObservationAdapterReceiptInput {
881 pub adapter_id: String,
882 pub adapter_version: String,
883 pub status: ObservationAdapterStatus,
884 pub source_app: String,
885 pub source_run_id: String,
886 pub source_transcript_ref: String,
887 pub source_transcript_hash: String,
888 pub package_id: TruthPackageId,
889 pub truth_version: String,
890 pub domain_hint: String,
891 pub observation_hash: Option<String>,
892 pub mapped_fact_ids: Vec<String>,
893 pub mapped_clause_ids: Vec<ClauseId>,
894 pub dropped_source_fields: Vec<String>,
895 pub warnings: Vec<String>,
896 pub errors: Vec<String>,
897 pub replay_notes: Vec<String>,
898}
899
900#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908pub struct ObservationAdapterReceipt {
909 pub receipt_id: ArtifactId,
910 pub adapter_id: String,
911 pub adapter_version: String,
912 pub status: ObservationAdapterStatus,
913 pub source_app: String,
914 pub source_run_id: String,
915 pub source_transcript_ref: String,
916 pub source_transcript_hash: String,
917 pub package_id: TruthPackageId,
918 pub truth_version: String,
919 pub domain_hint: String,
920 pub observation_hash: Option<String>,
921 pub mapped_fact_ids: Vec<String>,
922 pub mapped_clause_ids: Vec<ClauseId>,
923 pub dropped_source_fields: Vec<String>,
924 pub warnings: Vec<String>,
925 pub errors: Vec<String>,
926 pub replay_notes: Vec<String>,
927}
928
929impl ObservationAdapterReceipt {
930 pub fn new(input: ObservationAdapterReceiptInput) -> Self {
931 let receipt_id = observation_adapter_receipt_id(&input);
932 Self {
933 receipt_id,
934 adapter_id: input.adapter_id,
935 adapter_version: input.adapter_version,
936 status: input.status,
937 source_app: input.source_app,
938 source_run_id: input.source_run_id,
939 source_transcript_ref: input.source_transcript_ref,
940 source_transcript_hash: input.source_transcript_hash,
941 package_id: input.package_id,
942 truth_version: input.truth_version,
943 domain_hint: input.domain_hint,
944 observation_hash: input.observation_hash,
945 mapped_fact_ids: input.mapped_fact_ids,
946 mapped_clause_ids: input.mapped_clause_ids,
947 dropped_source_fields: input.dropped_source_fields,
948 warnings: input.warnings,
949 errors: input.errors,
950 replay_notes: input.replay_notes,
951 }
952 }
953}
954
955fn evidence_ref_record(evidence_ref: &FactEvidenceRef) -> EvidenceRefRecord {
956 match evidence_ref {
957 FactEvidenceRef::Observation(id) => EvidenceRefRecord {
958 evidence_id: id.as_str().to_string(),
959 source: "observation".to_string(),
960 },
961 FactEvidenceRef::HumanApproval(id) => EvidenceRefRecord {
962 evidence_id: id.as_str().to_string(),
963 source: "human_approval".to_string(),
964 },
965 FactEvidenceRef::Derived(id) => EvidenceRefRecord {
966 evidence_id: id.as_str().to_string(),
967 source: "derived".to_string(),
968 },
969 }
970}
971
972fn trace_link_record(trace_link: &FactTraceLink) -> TraceLinkRecord {
973 match trace_link {
974 FactTraceLink::Local(local) => TraceLinkRecord {
975 trace_id: local.trace_id().as_str().to_string(),
976 location: Some(format!("span:{}", local.span_id().as_str())),
977 replayable: true,
978 },
979 FactTraceLink::Remote(remote) => TraceLinkRecord {
980 trace_id: remote.reference().as_str().to_string(),
981 location: Some(remote.system().as_str().to_string()),
982 replayable: false,
983 },
984 }
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct AxiomRunReport {
995 pub package_id: TruthPackageId,
996 pub truth_version: String,
997 pub intent_packet_id: Uuid,
998 pub source_clause_ids: Vec<ClauseId>,
999 pub verifier_spec: VerifierSpec,
1000 pub verdict: AxiomRunVerdict,
1001 pub observed_stop_reason: ObservedStopReason,
1002 pub promoted_facts: Vec<PromotedFactRecord>,
1003 pub integrity: RunIntegrityProof,
1004 pub replay_profile_id: ArtifactId,
1005 pub replay_notes: Vec<String>,
1006 #[serde(default)]
1007 pub run_stages: Vec<AxiomRunStageRecord>,
1008}
1009
1010impl AxiomRunReport {
1011 pub fn from_observation(
1012 package: &TruthPackage,
1013 verdict: AxiomRunVerdict,
1014 observation: AxiomRunObservation,
1015 ) -> Self {
1016 Self {
1017 package_id: package.package_id.clone(),
1018 truth_version: package.truth_version.clone(),
1019 intent_packet_id: package.intent_packet.id,
1020 source_clause_ids: package.source_jtbd.clause_ids().cloned().collect(),
1021 verifier_spec: package.verifier_spec.clone(),
1022 verdict,
1023 observed_stop_reason: observation.stop_reason,
1024 promoted_facts: observation.promoted_facts,
1025 integrity: observation.integrity,
1026 replay_profile_id: package.replay_profile.profile_id.clone(),
1027 replay_notes: observation.replay_notes,
1028 run_stages: observation.run_stages,
1029 }
1030 }
1031
1032 pub fn verify(package: &TruthPackage, observation: AxiomRunObservation) -> Self {
1041 let verdict = package.verify(&observation);
1042 Self::from_observation(package, verdict, observation)
1043 }
1044
1045 pub fn expected_stop_reason_matched(&self) -> bool {
1046 self.observed_stop_reason
1047 .matches_expected(&self.verifier_spec.expected_stop_reasons)
1048 }
1049
1050 pub fn stage(&self, stage_id: &str) -> Option<&AxiomRunStageRecord> {
1051 self.run_stages
1052 .iter()
1053 .find(|stage| stage.stage_id == stage_id)
1054 }
1055
1056 pub fn audit_fact_lineage(
1072 &self,
1073 package: &TruthPackage,
1074 ) -> Result<FactLineageAudit, FactLineageAuditError> {
1075 if self.package_id != package.package_id {
1076 return Err(FactLineageAuditError::PackageMismatch {
1077 report: self.package_id.clone(),
1078 package: package.package_id.clone(),
1079 });
1080 }
1081 if self.truth_version != package.truth_version {
1082 return Err(FactLineageAuditError::TruthVersionMismatch {
1083 report: self.truth_version.clone(),
1084 package: package.truth_version.clone(),
1085 });
1086 }
1087
1088 let known_ids: BTreeSet<&ClauseId> = package
1089 .source_jtbd
1090 .clauses
1091 .iter()
1092 .map(|clause| &clause.id)
1093 .collect();
1094 let evidence_ids: BTreeSet<&ClauseId> = package
1095 .source_jtbd
1096 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
1097 .map(|clause| &clause.id)
1098 .collect();
1099 let failure_ids: BTreeSet<&ClauseId> = package
1100 .source_jtbd
1101 .clauses_by_kind(JtbdClauseKind::FailureMode)
1102 .map(|clause| &clause.id)
1103 .collect();
1104
1105 let mut evidence_coverage: BTreeSet<ClauseId> = BTreeSet::new();
1106 let mut failure_coverage: BTreeSet<ClauseId> = BTreeSet::new();
1107
1108 for fact in &self.promoted_facts {
1109 let mut serves_contract = false;
1110 for clause_id in &fact.source_clause_ids {
1111 if !known_ids.contains(clause_id) {
1112 return Err(FactLineageAuditError::UnknownClause {
1113 fact_id: fact.fact_id.clone(),
1114 clause_id: clause_id.clone(),
1115 });
1116 }
1117 if evidence_ids.contains(clause_id) {
1118 evidence_coverage.insert(clause_id.clone());
1119 serves_contract = true;
1120 }
1121 if failure_ids.contains(clause_id) {
1122 failure_coverage.insert(clause_id.clone());
1123 serves_contract = true;
1124 }
1125 }
1126 if !serves_contract {
1127 return Err(FactLineageAuditError::ScopeOnlyFact {
1128 fact_id: fact.fact_id.clone(),
1129 });
1130 }
1131 }
1132
1133 Ok(FactLineageAudit {
1134 package_id: package.package_id.clone(),
1135 truth_version: package.truth_version.clone(),
1136 facts_audited: self.promoted_facts.len(),
1137 evidence_coverage,
1138 failure_coverage,
1139 })
1140 }
1141}
1142
1143#[derive(Debug, Clone, PartialEq, Eq)]
1146pub struct FactLineageAudit {
1147 pub package_id: TruthPackageId,
1148 pub truth_version: String,
1149 pub facts_audited: usize,
1150 pub evidence_coverage: BTreeSet<ClauseId>,
1151 pub failure_coverage: BTreeSet<ClauseId>,
1152}
1153
1154#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1155pub enum FactLineageAuditError {
1156 #[error("report package_id {report:?} does not match audit package {package:?}")]
1157 PackageMismatch {
1158 report: TruthPackageId,
1159 package: TruthPackageId,
1160 },
1161 #[error("report truth_version {report} does not match audit package {package}")]
1162 TruthVersionMismatch { report: String, package: String },
1163 #[error("promoted fact {fact_id} cites clause {clause_id} not present in the package")]
1164 UnknownClause {
1165 fact_id: String,
1166 clause_id: ClauseId,
1167 },
1168 #[error(
1169 "promoted fact {fact_id} does not cite any evidence_required or failure_mode clause it serves"
1170 )]
1171 ScopeOnlyFact { fact_id: String },
1172}
1173
1174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1180pub struct LearningEpisode {
1181 pub episode_id: ArtifactId,
1182 pub source_run_id: String,
1183 pub domain_hint: String,
1184 pub package_id: TruthPackageId,
1185 pub truth_version: String,
1186 pub verdict: AxiomRunVerdict,
1187 pub observed_stop_reason: String,
1188 pub source_clause_signals: Vec<LearningClauseSignal>,
1189 pub promoted_fact_ids: Vec<String>,
1196 pub promotion_policy_hashes: Vec<String>,
1197 pub verifier_required_evidence: Vec<String>,
1198 pub verifier_forbidden_actions: Vec<String>,
1199}
1200
1201impl LearningEpisode {
1202 pub fn from_report(
1203 source_run_id: impl Into<String>,
1204 domain_hint: impl Into<String>,
1205 package: &TruthPackage,
1206 report: &AxiomRunReport,
1207 audit: &FactLineageAudit,
1208 ) -> Self {
1209 let source_run_id = source_run_id.into();
1210 let domain_hint = domain_hint.into();
1211 let mut promotion_policy_hashes: Vec<String> = report
1212 .promoted_facts
1213 .iter()
1214 .filter_map(|fact| fact.promotion_authority.as_ref())
1215 .map(|authority| authority.policy_version_hash.clone())
1216 .collect();
1217 promotion_policy_hashes.sort();
1218 promotion_policy_hashes.dedup();
1219
1220 let source_clause_signals = package
1221 .source_jtbd
1222 .clauses
1223 .iter()
1224 .map(|clause| {
1225 let evidence = audit.evidence_coverage.contains(&clause.id);
1226 let failure_guard = audit.failure_coverage.contains(&clause.id);
1227 let coverage_status = match (evidence, failure_guard) {
1228 (true, true) => ClauseCoverageStatus::CoveredAsEvidenceAndFailureGuard,
1229 (true, false) => ClauseCoverageStatus::CoveredAsEvidence,
1230 (false, true) => ClauseCoverageStatus::CoveredAsFailureGuard,
1231 (false, false) => ClauseCoverageStatus::Uncovered,
1232 };
1233 LearningClauseSignal {
1234 clause_id: clause.id.clone(),
1235 clause_kind: clause.kind,
1236 fingerprint: clause.fingerprint.clone(),
1237 coverage_status,
1238 }
1239 })
1240 .collect();
1241
1242 let episode_id = learning_episode_id(
1243 &source_run_id,
1244 &domain_hint,
1245 &package.package_id,
1246 &package.truth_version,
1247 report.verdict,
1248 );
1249
1250 Self {
1251 episode_id,
1252 source_run_id,
1253 domain_hint,
1254 package_id: package.package_id.clone(),
1255 truth_version: package.truth_version.clone(),
1256 verdict: report.verdict,
1257 observed_stop_reason: format!("{:?}", report.observed_stop_reason),
1258 source_clause_signals,
1259 promoted_fact_ids: report
1260 .promoted_facts
1261 .iter()
1262 .map(|fact| fact.fact_id.clone())
1263 .collect(),
1264 promotion_policy_hashes,
1265 verifier_required_evidence: report.verifier_spec.required_evidence.clone(),
1266 verifier_forbidden_actions: report
1267 .verifier_spec
1268 .forbidden_actions
1269 .iter()
1270 .map(|forbidden| forbidden.action.clone())
1271 .collect(),
1272 }
1273 }
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1277pub struct LearningClauseSignal {
1278 pub clause_id: ClauseId,
1279 pub clause_kind: JtbdClauseKind,
1280 pub fingerprint: ClauseFingerprint,
1281 pub coverage_status: ClauseCoverageStatus,
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1292#[serde(rename_all = "snake_case")]
1293pub enum ClauseCoverageStatus {
1294 #[default]
1299 Uncovered,
1300 CoveredAsEvidence,
1303 CoveredAsFailureGuard,
1306 CoveredAsEvidenceAndFailureGuard,
1308}
1309
1310impl ClauseCoverageStatus {
1311 pub fn was_covered_as_evidence(self) -> bool {
1313 matches!(
1314 self,
1315 Self::CoveredAsEvidence | Self::CoveredAsEvidenceAndFailureGuard
1316 )
1317 }
1318
1319 pub fn was_covered_as_failure_guard(self) -> bool {
1321 matches!(
1322 self,
1323 Self::CoveredAsFailureGuard | Self::CoveredAsEvidenceAndFailureGuard
1324 )
1325 }
1326
1327 pub fn is_uncovered(self) -> bool {
1329 matches!(self, Self::Uncovered)
1330 }
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1334pub struct CalibrationKey {
1335 pub clause_kind: JtbdClauseKind,
1336 pub normalized_clause_shape: String,
1337 pub domain_hint: String,
1338 pub decoder_rule_id: String,
1339 pub fingerprint_class: String,
1340}
1341
1342impl CalibrationKey {
1343 pub fn for_clause(
1344 clause: &JtbdClause,
1345 domain_hint: impl Into<String>,
1346 decoder_rule_id: impl Into<String>,
1347 ) -> Self {
1348 Self {
1349 clause_kind: clause.kind,
1350 normalized_clause_shape: canonicalize_clause_text(&clause.canonical_text),
1351 domain_hint: domain_hint.into(),
1352 decoder_rule_id: decoder_rule_id.into(),
1353 fingerprint_class: clause.fingerprint.short().to_string(),
1354 }
1355 }
1356}
1357
1358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1359pub struct CalibrationValue {
1360 pub suggested_evidence_templates: Vec<String>,
1361 pub suggested_failure_scenarios: Vec<String>,
1362 pub suggested_policy_requirements: Vec<String>,
1363 pub suggested_verifier_expectations: Vec<String>,
1364 pub confidence_basis_points: u16,
1373 pub rationale: String,
1374 pub source_episode_ids: Vec<ArtifactId>,
1375 #[serde(default)]
1386 pub signal_kind: CalibrationSignalKind,
1387}
1388
1389#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1392#[serde(rename_all = "snake_case")]
1393pub enum CalibrationSignalKind {
1394 #[default]
1399 Reinforcement,
1400 Concern,
1406}
1407
1408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1409#[serde(rename_all = "snake_case")]
1410pub enum CalibrationStatus {
1411 Proposed,
1412 Accepted,
1413 Rejected,
1414 Reset,
1415}
1416
1417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1418pub struct CalibrationRecord {
1419 pub record_id: ArtifactId,
1420 pub key: CalibrationKey,
1421 pub value: CalibrationValue,
1422 pub status: CalibrationStatus,
1423 pub review_note: Option<String>,
1424}
1425
1426impl CalibrationRecord {
1427 pub fn with_status(
1428 mut self,
1429 status: CalibrationStatus,
1430 review_note: impl Into<String>,
1431 ) -> Self {
1432 self.status = status;
1433 self.review_note = Some(review_note.into());
1434 self
1435 }
1436
1437 pub fn accepted(mut self, review_note: impl Into<String>) -> Self {
1438 self.status = CalibrationStatus::Accepted;
1439 self.review_note = Some(review_note.into());
1440 self
1441 }
1442}
1443
1444#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1445pub struct CalibrationTable {
1446 pub records: Vec<CalibrationRecord>,
1447}
1448
1449impl CalibrationTable {
1450 pub fn new(mut records: Vec<CalibrationRecord>) -> Self {
1451 records.sort_by(|left, right| left.record_id.as_str().cmp(right.record_id.as_str()));
1452 Self { records }
1453 }
1454
1455 pub fn accepted_for_clause<'a>(
1456 &'a self,
1457 clause: &JtbdClause,
1458 domain_hint: &str,
1459 decoder_rule_id: &str,
1460 ) -> impl Iterator<Item = &'a CalibrationRecord> {
1461 let key = CalibrationKey::for_clause(clause, domain_hint, decoder_rule_id);
1462 self.records
1463 .iter()
1464 .filter(move |record| record.status == CalibrationStatus::Accepted && record.key == key)
1465 }
1466
1467 pub fn to_jsonl(&self) -> String {
1476 let mut sorted: Vec<&CalibrationRecord> = self.records.iter().collect();
1477 sorted.sort_by(|left, right| left.record_id.as_str().cmp(right.record_id.as_str()));
1478 let mut out = String::new();
1479 for record in sorted {
1480 let line = serde_json::to_string(record)
1481 .expect("calibration record serializes (every field is serde-derived)");
1482 out.push_str(&line);
1483 out.push('\n');
1484 }
1485 out
1486 }
1487
1488 pub fn accept(
1495 &mut self,
1496 record_id: &ArtifactId,
1497 note: impl Into<String>,
1498 ) -> Result<(), CalibrationReviewError> {
1499 self.review(record_id, CalibrationStatus::Accepted, note.into())
1500 }
1501
1502 pub fn reject(
1505 &mut self,
1506 record_id: &ArtifactId,
1507 note: impl Into<String>,
1508 ) -> Result<(), CalibrationReviewError> {
1509 self.review(record_id, CalibrationStatus::Rejected, note.into())
1510 }
1511
1512 pub fn reset(
1517 &mut self,
1518 record_id: &ArtifactId,
1519 note: impl Into<String>,
1520 ) -> Result<(), CalibrationReviewError> {
1521 self.review(record_id, CalibrationStatus::Reset, note.into())
1522 }
1523
1524 fn review(
1525 &mut self,
1526 record_id: &ArtifactId,
1527 status: CalibrationStatus,
1528 note: String,
1529 ) -> Result<(), CalibrationReviewError> {
1530 if note.trim().is_empty() {
1531 return Err(CalibrationReviewError::EmptyNote {
1532 record_id: record_id.clone(),
1533 status,
1534 });
1535 }
1536 let record = self
1537 .records
1538 .iter_mut()
1539 .find(|record| &record.record_id == record_id)
1540 .ok_or_else(|| CalibrationReviewError::RecordNotFound {
1541 record_id: record_id.clone(),
1542 })?;
1543 record.status = status;
1544 record.review_note = Some(note);
1545 Ok(())
1546 }
1547
1548 pub fn from_jsonl(input: &str) -> Result<Self, CalibrationPersistenceError> {
1554 let mut records = Vec::new();
1555 let mut seen_ids: BTreeSet<ArtifactId> = BTreeSet::new();
1556 for (index, raw_line) in input.lines().enumerate() {
1557 let line = raw_line.trim();
1558 if line.is_empty() {
1559 continue;
1560 }
1561 let record: CalibrationRecord = serde_json::from_str(line).map_err(|err| {
1562 CalibrationPersistenceError::InvalidLine {
1563 line_number: index + 1,
1564 message: err.to_string(),
1565 }
1566 })?;
1567 if !seen_ids.insert(record.record_id.clone()) {
1568 return Err(CalibrationPersistenceError::DuplicateRecord {
1569 record_id: record.record_id.clone(),
1570 });
1571 }
1572 records.push(record);
1573 }
1574 Ok(Self::new(records))
1575 }
1576}
1577
1578const CALIBRATION_DECODER_RULE_ID: &str = "decoder_calibration.v0.13";
1579
1580pub fn calibration_records_from_learning_episode(
1581 package: &TruthPackage,
1582 episode: &LearningEpisode,
1583) -> Result<Vec<CalibrationRecord>, CalibrationError> {
1584 if package.package_id != episode.package_id {
1585 return Err(CalibrationError::PackageMismatch {
1586 package: package.package_id.clone(),
1587 episode: episode.package_id.clone(),
1588 });
1589 }
1590 if package.truth_version != episode.truth_version {
1591 return Err(CalibrationError::TruthVersionMismatch {
1592 package: package.truth_version.clone(),
1593 episode: episode.truth_version.clone(),
1594 });
1595 }
1596
1597 let mut records = Vec::new();
1598 for signal in &episode.source_clause_signals {
1599 let clause = package
1600 .source_jtbd
1601 .clause(&signal.clause_id)
1602 .ok_or_else(|| CalibrationError::UnknownClause {
1603 clause_id: signal.clause_id.clone(),
1604 })?;
1605
1606 if !signal.coverage_status.is_uncovered() {
1607 records.push(reinforcement_record_for_clause(
1608 package, episode, clause, signal,
1609 ));
1610 } else if should_emit_concern(clause.kind, episode.verdict) {
1611 records.push(concern_record_for_clause(package, episode, clause));
1612 }
1613 }
1614 Ok(records)
1615}
1616
1617fn should_emit_concern(clause_kind: JtbdClauseKind, verdict: AxiomRunVerdict) -> bool {
1635 clause_kind == JtbdClauseKind::EvidenceRequired
1636 && matches!(verdict, AxiomRunVerdict::Invalid | AxiomRunVerdict::Blocked)
1637}
1638
1639fn reinforcement_record_for_clause(
1640 package: &TruthPackage,
1641 episode: &LearningEpisode,
1642 clause: &JtbdClause,
1643 signal: &LearningClauseSignal,
1644) -> CalibrationRecord {
1645 let key = CalibrationKey::for_clause(clause, &episode.domain_hint, CALIBRATION_DECODER_RULE_ID);
1646 let mut value = CalibrationValue {
1647 suggested_evidence_templates: Vec::new(),
1648 suggested_failure_scenarios: Vec::new(),
1649 suggested_policy_requirements: Vec::new(),
1650 suggested_verifier_expectations: Vec::new(),
1651 confidence_basis_points: confidence_for_verdict(episode.verdict),
1652 rationale: format!(
1653 "learned from {} verdict {:?} for package {}",
1654 episode.source_run_id, episode.verdict, package.package_id
1655 ),
1656 source_episode_ids: vec![episode.episode_id.clone()],
1657 signal_kind: CalibrationSignalKind::Reinforcement,
1658 };
1659
1660 if signal.coverage_status.was_covered_as_evidence() {
1661 value
1662 .suggested_evidence_templates
1663 .push(clause.canonical_text.clone());
1664 value.suggested_policy_requirements.push(format!(
1665 "require before promotion: {}",
1666 clause.canonical_text
1667 ));
1668 value
1669 .suggested_verifier_expectations
1670 .push(clause.canonical_text.clone());
1671 }
1672 if signal.coverage_status.was_covered_as_failure_guard() {
1673 value
1674 .suggested_failure_scenarios
1675 .push(clause.canonical_text.clone());
1676 value.suggested_policy_requirements.push(format!(
1677 "forbid promotion when observed: {}",
1678 clause.canonical_text
1679 ));
1680 value
1681 .suggested_verifier_expectations
1682 .push(format!("forbidden action: {}", clause.canonical_text));
1683 }
1684
1685 let record_id = calibration_record_id(&key, &episode.episode_id);
1686 CalibrationRecord {
1687 record_id,
1688 key,
1689 value,
1690 status: CalibrationStatus::Proposed,
1691 review_note: None,
1692 }
1693}
1694
1695fn concern_record_for_clause(
1696 package: &TruthPackage,
1697 episode: &LearningEpisode,
1698 clause: &JtbdClause,
1699) -> CalibrationRecord {
1700 let key = CalibrationKey::for_clause(clause, &episode.domain_hint, CALIBRATION_DECODER_RULE_ID);
1701 let value = CalibrationValue {
1702 suggested_evidence_templates: Vec::new(),
1707 suggested_failure_scenarios: Vec::new(),
1708 suggested_policy_requirements: Vec::new(),
1709 suggested_verifier_expectations: Vec::new(),
1710 confidence_basis_points: confidence_for_concern(episode.verdict),
1711 rationale: format!(
1712 "concern from {} verdict {:?} for package {}: evidence requirement \"{}\" was not cited by any promoted fact",
1713 episode.source_run_id, episode.verdict, package.package_id, clause.canonical_text
1714 ),
1715 source_episode_ids: vec![episode.episode_id.clone()],
1716 signal_kind: CalibrationSignalKind::Concern,
1717 };
1718
1719 let record_id = calibration_record_id(&key, &episode.episode_id);
1720 CalibrationRecord {
1721 record_id,
1722 key,
1723 value,
1724 status: CalibrationStatus::Proposed,
1725 review_note: None,
1726 }
1727}
1728
1729fn confidence_for_concern(verdict: AxiomRunVerdict) -> u16 {
1730 match verdict {
1731 AxiomRunVerdict::Invalid => 8_000,
1734 AxiomRunVerdict::Blocked => 5_500,
1737 AxiomRunVerdict::Satisfied | AxiomRunVerdict::Exhausted => 3_000,
1740 }
1741}
1742
1743fn confidence_for_verdict(verdict: AxiomRunVerdict) -> u16 {
1744 match verdict {
1745 AxiomRunVerdict::Satisfied => 9_000,
1746 AxiomRunVerdict::Blocked | AxiomRunVerdict::Invalid => 7_000,
1747 AxiomRunVerdict::Exhausted => 4_000,
1748 }
1749}
1750
1751fn learning_episode_id(
1752 source_run_id: &str,
1753 domain_hint: &str,
1754 package_id: &TruthPackageId,
1755 truth_version: &str,
1756 verdict: AxiomRunVerdict,
1757) -> ArtifactId {
1758 let mut hasher = Sha256::new();
1759 hasher.update(source_run_id.as_bytes());
1760 hasher.update(b"\0");
1761 hasher.update(domain_hint.as_bytes());
1762 hasher.update(b"\0");
1763 hasher.update(package_id.as_str().as_bytes());
1764 hasher.update(b"\0");
1765 hasher.update(truth_version.as_bytes());
1766 hasher.update(b"\0");
1767 hasher.update(format!("{verdict:?}").as_bytes());
1768 ArtifactId::new(format!(
1769 "learning_episode.{}",
1770 hex_lower(&hasher.finalize())
1771 ))
1772}
1773
1774fn observation_adapter_receipt_id(input: &ObservationAdapterReceiptInput) -> ArtifactId {
1775 let serialized = serde_json::to_vec(input)
1776 .expect("ObservationAdapterReceiptInput has no fallible serialization fields");
1777 let mut hasher = Sha256::new();
1778 hasher.update(serialized);
1779 ArtifactId::new(format!(
1780 "observation_adapter_receipt.{}",
1781 hex_lower(&hasher.finalize())
1782 ))
1783}
1784
1785fn calibration_record_id(key: &CalibrationKey, episode_id: &ArtifactId) -> ArtifactId {
1786 let mut hasher = Sha256::new();
1787 hasher.update(format!("{:?}", key.clause_kind).as_bytes());
1788 hasher.update(b"\0");
1789 hasher.update(key.normalized_clause_shape.as_bytes());
1790 hasher.update(b"\0");
1791 hasher.update(key.domain_hint.as_bytes());
1792 hasher.update(b"\0");
1793 hasher.update(key.decoder_rule_id.as_bytes());
1794 hasher.update(b"\0");
1795 hasher.update(key.fingerprint_class.as_bytes());
1796 hasher.update(b"\0");
1797 hasher.update(episode_id.as_str().as_bytes());
1798 ArtifactId::new(format!(
1799 "calibration_record.{}",
1800 hex_lower(&hasher.finalize())
1801 ))
1802}
1803
1804fn calibration_summary(value: &CalibrationValue) -> String {
1805 match value.signal_kind {
1806 CalibrationSignalKind::Reinforcement => {
1807 if let Some(first) = value.suggested_evidence_templates.first() {
1808 return format!("suggests evidence template: {first}");
1809 }
1810 if let Some(first) = value.suggested_failure_scenarios.first() {
1811 return format!("suggests failure scenario: {first}");
1812 }
1813 if let Some(first) = value.suggested_policy_requirements.first() {
1814 return format!("suggests policy requirement: {first}");
1815 }
1816 "reinforces reviewed decoder prior".to_string()
1817 }
1818 CalibrationSignalKind::Concern => {
1819 format!("raises decoder concern: {}", value.rationale)
1820 }
1821 }
1822}
1823
1824pub fn apply_decoder_calibration(
1825 mut package: TruthPackage,
1826 table: &CalibrationTable,
1827 domain_hint: &str,
1828) -> Result<TruthPackage, CalibrationError> {
1829 let baseline_required_evidence = package.verifier_spec.required_evidence.clone();
1830 let baseline_forbidden_actions = serde_json::to_value(&package.verifier_spec.forbidden_actions)
1833 .expect("forbidden actions serialize for invariant baseline");
1834
1835 let mut suggestions = Vec::new();
1836 let mut concerns = Vec::new();
1837 for clause in &package.source_jtbd.clauses {
1838 for record in table.accepted_for_clause(clause, domain_hint, CALIBRATION_DECODER_RULE_ID) {
1839 let (artifact_kind, id_prefix) = match record.value.signal_kind {
1840 CalibrationSignalKind::Reinforcement => (
1841 ArtifactKind::CalibrationSuggestion,
1842 "calibration_suggestion",
1843 ),
1844 CalibrationSignalKind::Concern => {
1845 (ArtifactKind::CalibrationConcern, "calibration_concern")
1846 }
1847 };
1848 let artifact = GeneratedArtifact {
1849 artifact_id: ArtifactId::new(format!(
1850 "{id_prefix}.{}.{}",
1851 clause.key,
1852 record.record_id.as_str()
1853 )),
1854 artifact_kind,
1855 summary: format!(
1856 "calibration {} {}",
1857 record.record_id.as_str(),
1858 calibration_summary(&record.value)
1859 ),
1860 source_clause_ids: vec![clause.id.clone()],
1861 };
1862 package.lineage.artifacts.push(ArtifactLineage::new(
1863 artifact.artifact_id.clone(),
1864 artifact_kind,
1865 artifact.source_clause_ids.clone(),
1866 format!("decoder_calibration.{}", record.record_id.as_str()),
1867 DECODER_VERSION,
1868 &package.source_jtbd,
1869 ));
1870 match record.value.signal_kind {
1871 CalibrationSignalKind::Reinforcement => suggestions.push(artifact),
1872 CalibrationSignalKind::Concern => concerns.push(artifact),
1873 }
1874 }
1875 }
1876
1877 package
1878 .artifacts
1879 .calibration_suggestions
1880 .extend(suggestions);
1881 package.artifacts.calibration_concerns.extend(concerns);
1882 package.lineage.validate_closure(&package.source_jtbd)?;
1883
1884 debug_assert_eq!(
1889 package.verifier_spec.required_evidence, baseline_required_evidence,
1890 "calibration must not modify verifier_spec.required_evidence",
1891 );
1892 debug_assert_eq!(
1893 serde_json::to_value(&package.verifier_spec.forbidden_actions)
1894 .expect("forbidden actions serialize for invariant check"),
1895 baseline_forbidden_actions,
1896 "calibration must not modify verifier_spec.forbidden_actions",
1897 );
1898
1899 Ok(package)
1900}
1901
1902#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1903pub enum CalibrationError {
1904 #[error("calibration package_id {package:?} does not match episode package_id {episode:?}")]
1905 PackageMismatch {
1906 package: TruthPackageId,
1907 episode: TruthPackageId,
1908 },
1909 #[error("calibration truth version {package} does not match episode truth version {episode}")]
1910 TruthVersionMismatch { package: String, episode: String },
1911 #[error("calibration episode references unknown clause {clause_id}")]
1912 UnknownClause { clause_id: ClauseId },
1913 #[error(transparent)]
1914 Lineage(#[from] LineageError),
1915}
1916
1917#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1919pub enum CalibrationPersistenceError {
1920 #[error("calibration line {line_number} did not parse: {message}")]
1921 InvalidLine { line_number: usize, message: String },
1922 #[error("duplicate calibration record id {record_id}")]
1923 DuplicateRecord { record_id: ArtifactId },
1924}
1925
1926#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1929pub enum CalibrationReviewError {
1930 #[error("calibration record {record_id} not found in table")]
1931 RecordNotFound { record_id: ArtifactId },
1932 #[error("review action {status:?} on {record_id} requires a non-empty note")]
1933 EmptyNote {
1934 record_id: ArtifactId,
1935 status: CalibrationStatus,
1936 },
1937}
1938
1939fn compute_verdict(package: &TruthPackage, observation: &AxiomRunObservation) -> AxiomRunVerdict {
1940 let spec = &package.verifier_spec;
1941
1942 let known_clause_ids: BTreeSet<&ClauseId> = package
1944 .source_jtbd
1945 .clauses
1946 .iter()
1947 .map(|clause| &clause.id)
1948 .collect();
1949 let lineage_violation = observation
1950 .promoted_facts
1951 .iter()
1952 .flat_map(|fact| fact.source_clause_ids.iter())
1953 .any(|id| !known_clause_ids.contains(id));
1954 if lineage_violation {
1955 return AxiomRunVerdict::Invalid;
1956 }
1957
1958 let forbidden_observed = spec.forbidden_actions.iter().any(|forbidden| {
1963 let needle = forbidden.action.as_str();
1964 observation
1965 .promoted_facts
1966 .iter()
1967 .any(|fact| fact.summary.contains(needle))
1968 || observation
1969 .replay_notes
1970 .iter()
1971 .any(|note| note.contains(needle))
1972 || observation
1973 .run_stages
1974 .iter()
1975 .any(|stage| stage.replay_notes.iter().any(|note| note.contains(needle)))
1976 });
1977 if forbidden_observed {
1978 return AxiomRunVerdict::Invalid;
1979 }
1980
1981 if !observation
1983 .stop_reason
1984 .matches_expected(&spec.expected_stop_reasons)
1985 {
1986 if observation.stop_reason.is_invalid() {
1987 return AxiomRunVerdict::Invalid;
1988 }
1989 if observation.stop_reason.is_budget_exhausted() {
1990 return AxiomRunVerdict::Exhausted;
1991 }
1992 if observation.stop_reason.is_blocked() {
1993 return AxiomRunVerdict::Blocked;
1994 }
1995 return AxiomRunVerdict::Invalid;
1996 }
1997
1998 let required_evidence_ids: BTreeSet<&ClauseId> = package
2001 .source_jtbd
2002 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2003 .map(|clause| &clause.id)
2004 .collect();
2005 let cited_clause_ids: BTreeSet<&ClauseId> = observation
2006 .promoted_facts
2007 .iter()
2008 .flat_map(|fact| fact.source_clause_ids.iter())
2009 .collect();
2010 if !required_evidence_ids.is_subset(&cited_clause_ids) {
2011 return AxiomRunVerdict::Invalid;
2012 }
2013
2014 AxiomRunVerdict::Satisfied
2015}
2016
2017#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2019pub struct ArtifactLineage {
2020 pub artifact_id: ArtifactId,
2021 pub artifact_kind: ArtifactKind,
2022 pub source_clause_ids: Vec<ClauseId>,
2023 pub decoder_rule_id: String,
2024 pub decoder_version: String,
2025 pub input_fingerprints: Vec<ClauseFingerprint>,
2026}
2027
2028impl ArtifactLineage {
2029 pub fn new(
2030 artifact_id: ArtifactId,
2031 artifact_kind: ArtifactKind,
2032 source_clause_ids: Vec<ClauseId>,
2033 decoder_rule_id: impl Into<String>,
2034 decoder_version: impl Into<String>,
2035 document: &JtbdDocument,
2036 ) -> Self {
2037 let input_fingerprints = source_clause_ids
2038 .iter()
2039 .filter_map(|id| document.clause(id))
2040 .map(|clause| clause.fingerprint.clone())
2041 .collect();
2042
2043 Self {
2044 artifact_id,
2045 artifact_kind,
2046 source_clause_ids,
2047 decoder_rule_id: decoder_rule_id.into(),
2048 decoder_version: decoder_version.into(),
2049 input_fingerprints,
2050 }
2051 }
2052}
2053
2054#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2056#[serde(rename_all = "snake_case")]
2057pub enum ClauseDisposition {
2058 Deferred { reason: String },
2059 Rejected { reason: String },
2060}
2061
2062#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2064pub struct LineageMap {
2065 pub artifacts: Vec<ArtifactLineage>,
2066 pub clause_dispositions: BTreeMap<ClauseId, ClauseDisposition>,
2067}
2068
2069impl LineageMap {
2070 pub fn single_artifact_from_document(
2071 document: &JtbdDocument,
2072 artifact_id: ArtifactId,
2073 artifact_kind: ArtifactKind,
2074 decoder_rule_id: impl Into<String>,
2075 decoder_version: impl Into<String>,
2076 ) -> Self {
2077 let source_clause_ids: Vec<ClauseId> = document.clause_ids().cloned().collect();
2078 Self {
2079 artifacts: vec![ArtifactLineage::new(
2080 artifact_id,
2081 artifact_kind,
2082 source_clause_ids,
2083 decoder_rule_id,
2084 decoder_version,
2085 document,
2086 )],
2087 clause_dispositions: BTreeMap::new(),
2088 }
2089 }
2090
2091 pub fn validate_closure(&self, document: &JtbdDocument) -> Result<(), LineageError> {
2092 let known = document.known_clause_ids();
2093 let mut accounted = BTreeSet::new();
2094
2095 for artifact in &self.artifacts {
2096 if artifact.source_clause_ids.is_empty() {
2097 return Err(LineageError::ArtifactWithoutSource {
2098 artifact_id: artifact.artifact_id.clone(),
2099 });
2100 }
2101
2102 for clause_id in &artifact.source_clause_ids {
2103 if !known.contains(clause_id) {
2104 return Err(LineageError::UnknownArtifactClause {
2105 artifact_id: artifact.artifact_id.clone(),
2106 clause_id: clause_id.clone(),
2107 });
2108 }
2109 accounted.insert(clause_id.clone());
2110 }
2111 }
2112
2113 for clause_id in self.clause_dispositions.keys() {
2114 if !known.contains(clause_id) {
2115 return Err(LineageError::UnknownDispositionClause {
2116 clause_id: clause_id.clone(),
2117 });
2118 }
2119 accounted.insert(clause_id.clone());
2120 }
2121
2122 for clause_id in &known {
2123 if !accounted.contains(clause_id) {
2124 return Err(LineageError::UnaccountedClause {
2125 clause_id: clause_id.clone(),
2126 });
2127 }
2128 }
2129
2130 Ok(())
2131 }
2132}
2133
2134#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2136pub enum TruthPackageError {
2137 #[error("empty JTBD clause: {field}")]
2138 EmptyClause { field: String },
2139 #[error("duplicate clause id: {id}")]
2140 DuplicateClauseId { id: ClauseId },
2141}
2142
2143#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2145pub enum LineageError {
2146 #[error("artifact {artifact_id} has no source clauses")]
2147 ArtifactWithoutSource { artifact_id: ArtifactId },
2148 #[error("artifact {artifact_id} references unknown clause {clause_id}")]
2149 UnknownArtifactClause {
2150 artifact_id: ArtifactId,
2151 clause_id: ClauseId,
2152 },
2153 #[error("clause disposition references unknown clause {clause_id}")]
2154 UnknownDispositionClause { clause_id: ClauseId },
2155 #[error("clause {clause_id} is not used, deferred, or rejected")]
2156 UnaccountedClause { clause_id: ClauseId },
2157}
2158
2159#[derive(Debug, thiserror::Error)]
2160pub enum TruthOverlayError {
2161 #[error("overlay targets package {target}, but package is {actual}")]
2162 PackageMismatch {
2163 target: TruthPackageId,
2164 actual: TruthPackageId,
2165 },
2166 #[error("overlay targets truth version {target}, but package version is {actual}")]
2167 TruthVersionMismatch { target: String, actual: String },
2168 #[error("overlay projection version must not be empty")]
2169 EmptyProjectionVersion,
2170 #[error("overlay reason must not be empty")]
2171 EmptyReason,
2172 #[error("overlay must name at least one source clause")]
2173 MissingSourceClauses,
2174 #[error("overlay references unknown clause {clause_id}")]
2175 UnknownSourceClause { clause_id: ClauseId },
2176 #[error("overlay .truths projection did not parse: {message}")]
2177 TruthProjectionParse { message: String },
2178}
2179
2180pub fn decode_jtbd(input: JtbdInput) -> Result<TruthPackage, DecodeJtbdError> {
2185 let document = JtbdDocument::from_input(input)?;
2186 let package_id = package_id_for_document(&document);
2187 let generated_truths = generate_truth_projection(&document);
2188 let parsed_truth = parse_truth_document(&generated_truths).map_err(|err| {
2189 DecodeJtbdError::TruthProjectionParse {
2190 message: err.to_string(),
2191 }
2192 })?;
2193 let mut intent_packet = compile_intent(&parsed_truth)?;
2194 intent_packet.id = deterministic_uuid(package_id.as_str());
2195 intent_packet.context = json!({
2196 "truth_package_id": package_id.as_str(),
2197 "truth_version": TRUTH_VERSION,
2198 "source_clause_ids": document
2199 .clause_ids()
2200 .map(ClauseId::as_str)
2201 .collect::<Vec<_>>(),
2202 });
2203 if let Some(budget) = document.time_budget
2204 && let Some(ctx) = intent_packet.context.as_object_mut()
2205 {
2206 ctx.insert(
2207 "time_budget_seconds".to_string(),
2208 json!(budget.as_seconds()),
2209 );
2210 }
2211
2212 let artifacts = build_artifacts(&document);
2213 let proof_obligations = build_proof_obligations(&document);
2214 let verifier_spec = build_verifier_spec(&document);
2215 let replay_profile = ReplayProfile {
2216 profile_id: ArtifactId::new(format!("replay_profile.{}", document.key)),
2217 deterministic: true,
2218 input_clause_ids: document.clause_ids().cloned().collect(),
2219 };
2220 let lineage = build_lineage_map(
2221 &document,
2222 &artifacts,
2223 &proof_obligations,
2224 &verifier_spec,
2225 &replay_profile,
2226 );
2227 lineage.validate_closure(&document)?;
2228
2229 Ok(TruthPackage {
2230 package_id,
2231 truth_version: TRUTH_VERSION.to_string(),
2232 source_jtbd: document,
2233 generated_truths,
2234 artifacts,
2235 intent_packet,
2236 proof_obligations,
2237 verifier_spec,
2238 replay_profile,
2239 lineage,
2240 })
2241}
2242
2243pub fn apply_truth_projection_overlay(
2244 package: &TruthPackage,
2245 overlay: TruthProjectionOverlay,
2246) -> Result<TruthProjectionVersion, TruthOverlayError> {
2247 if overlay.target_package_id != package.package_id {
2248 return Err(TruthOverlayError::PackageMismatch {
2249 target: overlay.target_package_id,
2250 actual: package.package_id.clone(),
2251 });
2252 }
2253 if overlay.target_truth_version != package.truth_version {
2254 return Err(TruthOverlayError::TruthVersionMismatch {
2255 target: overlay.target_truth_version,
2256 actual: package.truth_version.clone(),
2257 });
2258 }
2259 if canonicalize_clause_text(&overlay.projection_version).is_empty() {
2260 return Err(TruthOverlayError::EmptyProjectionVersion);
2261 }
2262 if canonicalize_clause_text(&overlay.reason).is_empty() {
2263 return Err(TruthOverlayError::EmptyReason);
2264 }
2265 if overlay.source_clause_ids.is_empty() {
2266 return Err(TruthOverlayError::MissingSourceClauses);
2267 }
2268
2269 let known = package.source_jtbd.known_clause_ids();
2270 for clause_id in &overlay.source_clause_ids {
2271 if !known.contains(clause_id) {
2272 return Err(TruthOverlayError::UnknownSourceClause {
2273 clause_id: clause_id.clone(),
2274 });
2275 }
2276 }
2277
2278 parse_truth_document(&overlay.edited_truths).map_err(|err| {
2279 TruthOverlayError::TruthProjectionParse {
2280 message: err.to_string(),
2281 }
2282 })?;
2283
2284 let lineage = ArtifactLineage::new(
2285 overlay.overlay_id.clone(),
2286 ArtifactKind::TruthProjection,
2287 overlay.source_clause_ids.clone(),
2288 "truth_projection_overlay.v0",
2289 DECODER_VERSION,
2290 &package.source_jtbd,
2291 );
2292
2293 Ok(TruthProjectionVersion {
2294 package_id: package.package_id.clone(),
2295 base_truth_version: package.truth_version.clone(),
2296 projection_version: overlay.projection_version,
2297 truths: overlay.edited_truths,
2298 source: TruthProjectionSource::OverlayApplied {
2299 overlay_id: overlay.overlay_id,
2300 reason: overlay.reason,
2301 },
2302 lineage,
2303 })
2304}
2305
2306#[derive(Debug, thiserror::Error)]
2307pub enum DecodeJtbdError {
2308 #[error(transparent)]
2309 TruthPackage(#[from] TruthPackageError),
2310 #[error("generated truth projection did not parse: {message}")]
2311 TruthProjectionParse { message: String },
2312 #[error(transparent)]
2313 Intent(#[from] CompileError),
2314 #[error(transparent)]
2315 Lineage(#[from] LineageError),
2316}
2317
2318pub fn canonicalize_clause_text(text: &str) -> String {
2319 text.split_whitespace().collect::<Vec<_>>().join(" ")
2320}
2321
2322fn package_id_for_document(document: &JtbdDocument) -> TruthPackageId {
2323 let mut hasher = Sha256::new();
2324 hasher.update(document.key.as_bytes());
2325 for clause in &document.clauses {
2326 hasher.update(clause.id.as_str().as_bytes());
2327 hasher.update(clause.fingerprint.as_str().as_bytes());
2328 }
2329 let digest = hex_lower(&hasher.finalize());
2330 TruthPackageId::new(format!("truth_package.{}.{}", document.key, &digest[..12]))
2331}
2332
2333fn overlay_id_for(
2334 package_id: &TruthPackageId,
2335 target_truth_version: &str,
2336 projection_version: &str,
2337 edited_truths: &str,
2338) -> ArtifactId {
2339 let mut hasher = Sha256::new();
2340 hasher.update(package_id.as_str().as_bytes());
2341 hasher.update(target_truth_version.as_bytes());
2342 hasher.update(projection_version.as_bytes());
2343 hasher.update(canonicalize_clause_text(edited_truths).as_bytes());
2344 let digest = hex_lower(&hasher.finalize());
2345 ArtifactId::new(format!("truth_projection_overlay.{}", &digest[..12]))
2346}
2347
2348fn generate_truth_projection(document: &JtbdDocument) -> String {
2349 let actor = required_clause(document, JtbdClauseKind::Actor);
2350 let functional_job = required_clause(document, JtbdClauseKind::FunctionalJob);
2351 let so_that = required_clause(document, JtbdClauseKind::SoThat);
2352 let evidence: Vec<&JtbdClause> = document
2353 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2354 .collect();
2355 let failures: Vec<&JtbdClause> = document
2356 .clauses_by_kind(JtbdClauseKind::FailureMode)
2357 .collect();
2358
2359 let mut out = String::new();
2360 push_line(
2361 &mut out,
2362 &format!("Truth: {}", sentence_title(&functional_job.canonical_text)),
2363 );
2364 push_line(&mut out, "");
2365 push_line(&mut out, " Intent:");
2366 push_line(
2367 &mut out,
2368 &format!(" Outcome: {}", so_that.canonical_text),
2369 );
2370 push_line(&mut out, "");
2371 push_line(&mut out, " Authority:");
2372 push_line(&mut out, &format!(" Actor: {}", actor.canonical_text));
2373 push_line(
2374 &mut out,
2375 &format!(" May: attempt {}", functional_job.canonical_text),
2376 );
2377 push_line(
2378 &mut out,
2379 &format!(
2380 " Expires: {}",
2381 deterministic_expires_line(document.time_budget)
2382 ),
2383 );
2384 if !failures.is_empty() {
2385 push_line(&mut out, "");
2386 push_line(&mut out, " Constraint:");
2387 for failure in &failures {
2388 push_line(
2389 &mut out,
2390 &format!(" Must Not: {}", failure.canonical_text),
2391 );
2392 }
2393 }
2394 push_line(&mut out, "");
2395 push_line(&mut out, " Evidence:");
2396 for item in &evidence {
2397 push_line(&mut out, &format!(" Requires: {}", item.canonical_text));
2398 }
2399 push_line(&mut out, " Provenance: axiom_truth_package");
2400 push_line(&mut out, " Audit: truth_package_lineage");
2401 push_line(&mut out, "");
2402 push_line(&mut out, " @acceptance @invariant");
2403 push_line(
2404 &mut out,
2405 " Scenario: Job is satisfied by required evidence",
2406 );
2407 push_line(&mut out, " Given the actor has the declared job");
2408 push_line(&mut out, " When the system evaluates the truth package");
2409 push_line(
2410 &mut out,
2411 " Then the declared outcome is supported by required evidence",
2412 );
2413 out
2414}
2415
2416fn build_artifacts(document: &JtbdDocument) -> TruthPackageArtifacts {
2417 let all_clause_ids: Vec<ClauseId> = document.clause_ids().cloned().collect();
2418 let actor = required_clause(document, JtbdClauseKind::Actor);
2419 let functional_job = required_clause(document, JtbdClauseKind::FunctionalJob);
2420 let scenarios = vec![GeneratedArtifact {
2421 artifact_id: ArtifactId::new(format!("scenario.{}.satisfaction", document.key)),
2422 artifact_kind: ArtifactKind::Scenario,
2423 summary: "Job is satisfied by required evidence".to_string(),
2424 source_clause_ids: all_clause_ids.clone(),
2425 }];
2426 let policy_requirements = build_policy_requirement_artifacts(document, actor, functional_job);
2427 let evidence_expectations = document
2428 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2429 .map(|clause| GeneratedArtifact {
2430 artifact_id: ArtifactId::new(format!("evidence_expectation.{}", clause.key)),
2431 artifact_kind: ArtifactKind::VerifierExpectation,
2432 summary: clause.canonical_text.clone(),
2433 source_clause_ids: vec![clause.id.clone()],
2434 })
2435 .collect();
2436 let invariant_expectations = document
2437 .clauses_by_kind(JtbdClauseKind::FailureMode)
2438 .map(|clause| GeneratedArtifact {
2439 artifact_id: ArtifactId::new(format!("invariant_expectation.{}", clause.key)),
2440 artifact_kind: ArtifactKind::InvariantArtifact,
2441 summary: format!("forbid {}", clause.canonical_text),
2442 source_clause_ids: vec![clause.id.clone()],
2443 })
2444 .collect();
2445 let simulation_cases = vec![GeneratedArtifact {
2446 artifact_id: ArtifactId::new(format!("simulation_case.{}.baseline", document.key)),
2447 artifact_kind: ArtifactKind::SimulationCase,
2448 summary: "baseline deterministic package readiness case".to_string(),
2449 source_clause_ids: all_clause_ids,
2450 }];
2451
2452 TruthPackageArtifacts {
2453 scenarios,
2454 predicates: Vec::new(),
2455 policy_requirements,
2456 evidence_expectations,
2457 simulation_cases,
2458 invariant_expectations,
2459 calibration_suggestions: Vec::new(),
2460 calibration_concerns: Vec::new(),
2461 }
2462}
2463
2464fn build_policy_requirement_artifacts(
2465 document: &JtbdDocument,
2466 actor: &JtbdClause,
2467 functional_job: &JtbdClause,
2468) -> Vec<GeneratedArtifact> {
2469 let mut artifacts = vec![GeneratedArtifact {
2470 artifact_id: ArtifactId::new(format!("policy_requirement.{}.authority", document.key)),
2471 artifact_kind: ArtifactKind::PolicyRequirement,
2472 summary: format!(
2473 "authority envelope: {} may attempt {} only through current Converge promotion policy",
2474 actor.canonical_text, functional_job.canonical_text
2475 ),
2476 source_clause_ids: vec![actor.id.clone(), functional_job.id.clone()],
2477 }];
2478
2479 artifacts.extend(
2480 document
2481 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2482 .map(|clause| GeneratedArtifact {
2483 artifact_id: ArtifactId::new(format!(
2484 "policy_requirement.{}.evidence.{}",
2485 document.key, clause.key
2486 )),
2487 artifact_kind: ArtifactKind::PolicyRequirement,
2488 summary: format!("require before promotion: {}", clause.canonical_text),
2489 source_clause_ids: vec![clause.id.clone()],
2490 }),
2491 );
2492
2493 artifacts.extend(
2494 document
2495 .clauses_by_kind(JtbdClauseKind::FailureMode)
2496 .map(|clause| GeneratedArtifact {
2497 artifact_id: ArtifactId::new(format!(
2498 "policy_requirement.{}.failure.{}",
2499 document.key, clause.key
2500 )),
2501 artifact_kind: ArtifactKind::PolicyRequirement,
2502 summary: format!("forbid promotion when observed: {}", clause.canonical_text),
2503 source_clause_ids: vec![clause.id.clone()],
2504 }),
2505 );
2506
2507 artifacts
2508}
2509
2510fn build_proof_obligations(document: &JtbdDocument) -> Vec<ProofObligation> {
2511 let mut obligations = Vec::new();
2512 obligations.extend(
2513 document
2514 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2515 .map(|clause| ProofObligation {
2516 artifact_id: ArtifactId::new(format!("proof_obligation.evidence.{}", clause.key)),
2517 kind: ProofObligationKind::EvidenceRequired,
2518 description: format!("evidence required: {}", clause.canonical_text),
2519 source_clause_ids: vec![clause.id.clone()],
2520 }),
2521 );
2522 obligations.extend(
2523 document
2524 .clauses_by_kind(JtbdClauseKind::FailureMode)
2525 .map(|clause| ProofObligation {
2526 artifact_id: ArtifactId::new(format!("proof_obligation.failure.{}", clause.key)),
2527 kind: ProofObligationKind::FailureGuard,
2528 description: format!("failure mode must be guarded: {}", clause.canonical_text),
2529 source_clause_ids: vec![clause.id.clone()],
2530 }),
2531 );
2532 obligations
2533}
2534
2535fn build_verifier_spec(document: &JtbdDocument) -> VerifierSpec {
2536 let required_evidence = document
2537 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2538 .map(|clause| clause.canonical_text.clone())
2539 .collect();
2540 let forbidden_actions = document
2541 .clauses_by_kind(JtbdClauseKind::FailureMode)
2542 .map(|clause| ForbiddenAction {
2543 action: clause.canonical_text.clone(),
2544 reason: "failure_mode".to_string(),
2545 })
2546 .collect();
2547 let satisfaction_conditions = vec![
2548 required_clause(document, JtbdClauseKind::SoThat)
2549 .canonical_text
2550 .clone(),
2551 ];
2552
2553 VerifierSpec {
2554 expected_stop_reasons: BTreeSet::from([
2555 ExpectedStopReason::Converged,
2556 ExpectedStopReason::CriteriaMet,
2557 ]),
2558 required_evidence,
2559 forbidden_actions,
2560 satisfaction_conditions,
2561 }
2562}
2563
2564fn build_lineage_map(
2565 document: &JtbdDocument,
2566 artifacts: &TruthPackageArtifacts,
2567 proof_obligations: &[ProofObligation],
2568 verifier_spec: &VerifierSpec,
2569 replay_profile: &ReplayProfile,
2570) -> LineageMap {
2571 let all_clause_ids: Vec<ClauseId> = document.clause_ids().cloned().collect();
2572 let mut lineages = vec![
2573 ArtifactLineage::new(
2574 ArtifactId::new(format!("manifest.{}", document.key)),
2575 ArtifactKind::TruthPackageManifest,
2576 all_clause_ids.clone(),
2577 "truth_package_manifest.v0",
2578 DECODER_VERSION,
2579 document,
2580 ),
2581 ArtifactLineage::new(
2582 ArtifactId::new(format!("truth_projection.{}", document.key)),
2583 ArtifactKind::TruthProjection,
2584 all_clause_ids.clone(),
2585 "truth_projection.v0",
2586 DECODER_VERSION,
2587 document,
2588 ),
2589 ArtifactLineage::new(
2590 ArtifactId::new(format!("intent_packet.{}", document.key)),
2591 ArtifactKind::IntentField,
2592 all_clause_ids.clone(),
2593 "intent_packet.v0",
2594 DECODER_VERSION,
2595 document,
2596 ),
2597 ArtifactLineage::new(
2598 ArtifactId::new(format!("verifier_spec.{}", document.key)),
2599 ArtifactKind::VerifierExpectation,
2600 verifier_source_clause_ids(document, verifier_spec),
2601 "verifier_spec.v0",
2602 DECODER_VERSION,
2603 document,
2604 ),
2605 ArtifactLineage::new(
2606 replay_profile.profile_id.clone(),
2607 ArtifactKind::ReplayProfile,
2608 replay_profile.input_clause_ids.clone(),
2609 "replay_profile.v0",
2610 DECODER_VERSION,
2611 document,
2612 ),
2613 ];
2614
2615 for artifact in generated_artifact_iter(artifacts) {
2616 lineages.push(ArtifactLineage::new(
2617 artifact.artifact_id.clone(),
2618 artifact.artifact_kind,
2619 artifact.source_clause_ids.clone(),
2620 "generated_artifact.v0",
2621 DECODER_VERSION,
2622 document,
2623 ));
2624 }
2625 for obligation in proof_obligations {
2626 lineages.push(ArtifactLineage::new(
2627 obligation.artifact_id.clone(),
2628 ArtifactKind::ProofObligation,
2629 obligation.source_clause_ids.clone(),
2630 "proof_obligation.v0",
2631 DECODER_VERSION,
2632 document,
2633 ));
2634 }
2635
2636 LineageMap {
2637 artifacts: lineages,
2638 clause_dispositions: BTreeMap::new(),
2639 }
2640}
2641
2642fn generated_artifact_iter(
2643 artifacts: &TruthPackageArtifacts,
2644) -> impl Iterator<Item = &GeneratedArtifact> {
2645 artifacts
2646 .scenarios
2647 .iter()
2648 .chain(artifacts.predicates.iter())
2649 .chain(artifacts.policy_requirements.iter())
2650 .chain(artifacts.evidence_expectations.iter())
2651 .chain(artifacts.simulation_cases.iter())
2652 .chain(artifacts.invariant_expectations.iter())
2653 .chain(artifacts.calibration_suggestions.iter())
2654 .chain(artifacts.calibration_concerns.iter())
2655}
2656
2657fn verifier_source_clause_ids(
2658 document: &JtbdDocument,
2659 verifier_spec: &VerifierSpec,
2660) -> Vec<ClauseId> {
2661 let mut ids = BTreeSet::new();
2662 ids.insert(required_clause(document, JtbdClauseKind::SoThat).id.clone());
2663 if !verifier_spec.required_evidence.is_empty() {
2664 ids.extend(
2665 document
2666 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
2667 .map(|clause| clause.id.clone()),
2668 );
2669 }
2670 if !verifier_spec.forbidden_actions.is_empty() {
2671 ids.extend(
2672 document
2673 .clauses_by_kind(JtbdClauseKind::FailureMode)
2674 .map(|clause| clause.id.clone()),
2675 );
2676 }
2677 ids.into_iter().collect()
2678}
2679
2680fn required_clause(document: &JtbdDocument, kind: JtbdClauseKind) -> &JtbdClause {
2681 document
2682 .clauses_by_kind(kind)
2683 .next()
2684 .expect("JtbdDocument always contains scalar clauses")
2685}
2686
2687fn deterministic_uuid(seed: &str) -> Uuid {
2688 let mut hasher = Sha256::new();
2689 hasher.update(seed.as_bytes());
2690 let digest = hasher.finalize();
2691 let mut bytes = [0_u8; 16];
2692 bytes.copy_from_slice(&digest[..16]);
2693 bytes[6] = (bytes[6] & 0x0f) | 0x50;
2694 bytes[8] = (bytes[8] & 0x3f) | 0x80;
2695 Uuid::from_bytes(bytes)
2696}
2697
2698fn sentence_title(value: &str) -> String {
2699 let mut chars = value.chars();
2700 let Some(first) = chars.next() else {
2701 return "Untitled job".to_string();
2702 };
2703 format!("{}{}", first.to_uppercase(), chars.as_str())
2704}
2705
2706fn push_line(buffer: &mut String, line: &str) {
2707 buffer.push_str(line);
2708 buffer.push('\n');
2709}
2710
2711fn scalar_clause(
2712 root_key: &str,
2713 kind: JtbdClauseKind,
2714 path: &str,
2715 text: String,
2716) -> Result<JtbdClause, TruthPackageError> {
2717 build_clause(root_key, kind, path, path, text)
2718}
2719
2720fn collection_clauses(
2721 root_key: &str,
2722 kind: JtbdClauseKind,
2723 path_prefix: &str,
2724 inputs: Vec<ClauseInput>,
2725) -> Result<Vec<JtbdClause>, TruthPackageError> {
2726 let mut implicit_counts = BTreeMap::<String, usize>::new();
2727 for input in &inputs {
2728 if input.key.is_none() {
2729 let canonical = canonicalize_clause_text(&input.text);
2730 let fingerprint = ClauseFingerprint::from_text(&canonical);
2731 let key = implicit_clause_key(&canonical, &fingerprint);
2732 *implicit_counts.entry(key).or_default() += 1;
2733 }
2734 }
2735
2736 let mut clauses = Vec::with_capacity(inputs.len());
2737 for input in inputs {
2738 let canonical = canonicalize_clause_text(&input.text);
2739 let fingerprint = ClauseFingerprint::from_text(&canonical);
2740 let explicit = input.key.is_some();
2741 let base_key = input.key.map_or_else(
2742 || implicit_clause_key(&canonical, &fingerprint),
2743 |key| normalized_key(&key, "clause"),
2744 );
2745 let key = if explicit || implicit_counts.get(&base_key).copied().unwrap_or(0) <= 1 {
2746 base_key
2747 } else {
2748 format!("{base_key}_{}", fingerprint.short())
2749 };
2750 let path = format!("{path_prefix}.{key}");
2751 clauses.push(build_clause(root_key, kind, &path, &key, input.text)?);
2752 }
2753
2754 clauses.sort_by(|left, right| left.id.cmp(&right.id));
2755 ensure_unique_clause_ids(&clauses)?;
2756 Ok(clauses)
2757}
2758
2759fn build_clause(
2760 root_key: &str,
2761 kind: JtbdClauseKind,
2762 path: &str,
2763 key: &str,
2764 text: String,
2765) -> Result<JtbdClause, TruthPackageError> {
2766 let canonical_text = canonicalize_clause_text(&text);
2767 if canonical_text.is_empty() {
2768 return Err(TruthPackageError::EmptyClause {
2769 field: path.to_string(),
2770 });
2771 }
2772
2773 Ok(JtbdClause {
2774 id: ClauseId::new(root_key, path),
2775 kind,
2776 key: key.to_string(),
2777 text,
2778 fingerprint: ClauseFingerprint::from_text(&canonical_text),
2779 canonical_text,
2780 })
2781}
2782
2783fn ensure_unique_clause_ids(clauses: &[JtbdClause]) -> Result<(), TruthPackageError> {
2784 let mut seen = BTreeSet::new();
2785 for clause in clauses {
2786 if !seen.insert(clause.id.clone()) {
2787 return Err(TruthPackageError::DuplicateClauseId {
2788 id: clause.id.clone(),
2789 });
2790 }
2791 }
2792 Ok(())
2793}
2794
2795fn implicit_clause_key(canonical_text: &str, fingerprint: &ClauseFingerprint) -> String {
2796 let slug = slugify(canonical_text);
2797 if slug.is_empty() {
2798 format!("clause_{}", fingerprint.short())
2799 } else {
2800 slug
2801 }
2802}
2803
2804fn normalized_key(value: &str, fallback_prefix: &str) -> String {
2805 let slug = slugify(value);
2806 if slug.is_empty() {
2807 let fingerprint = ClauseFingerprint::from_text(value);
2808 format!("{fallback_prefix}_{}", fingerprint.short())
2809 } else {
2810 slug
2811 }
2812}
2813
2814fn slugify(value: &str) -> String {
2815 let mut out = String::new();
2816 let mut last_was_separator = false;
2817
2818 for ch in value.chars() {
2819 if ch.is_ascii_alphanumeric() {
2820 out.push(ch.to_ascii_lowercase());
2821 last_was_separator = false;
2822 } else if !last_was_separator {
2823 out.push('_');
2824 last_was_separator = true;
2825 }
2826 }
2827
2828 out.trim_matches('_').to_string()
2829}
2830
2831fn hex_lower(bytes: &[u8]) -> String {
2832 use std::fmt::Write as _;
2833
2834 let mut out = String::with_capacity(bytes.len() * 2);
2835 for byte in bytes {
2836 write!(&mut out, "{byte:02x}").expect("writing to String cannot fail");
2837 }
2838 out
2839}
2840
2841#[cfg(test)]
2842mod tests {
2843 use super::*;
2844
2845 fn vendor_input() -> JtbdInput {
2846 JtbdInput {
2847 key: "Vendor Commitment".to_string(),
2848 actor: "finance controller".to_string(),
2849 functional_job: "approve a vendor commitment".to_string(),
2850 so_that: "spend is traceable and policy-compliant".to_string(),
2851 evidence_required: vec![
2852 ClauseInput::new("vendor assessment"),
2853 ClauseInput::with_key("po", "purchase order"),
2854 ],
2855 failure_modes: vec![
2856 ClauseInput::new("bypassed approval"),
2857 ClauseInput::new("missing audit trail"),
2858 ],
2859 time_budget: None,
2860 }
2861 }
2862
2863 #[test]
2864 fn deterministic_clause_ids_do_not_depend_on_collection_order() {
2865 let mut reordered = vendor_input();
2866 reordered.evidence_required.reverse();
2867 reordered.failure_modes.reverse();
2868
2869 let original = JtbdDocument::from_input(vendor_input()).unwrap();
2870 let reordered = JtbdDocument::from_input(reordered).unwrap();
2871
2872 assert_eq!(original, reordered);
2873
2874 let ids: Vec<&str> = original.clause_ids().map(ClauseId::as_str).collect();
2875 assert!(ids.contains(&"jtbd.vendor_commitment.actor"));
2876 assert!(ids.contains(&"jtbd.vendor_commitment.functional_job"));
2877 assert!(ids.contains(&"jtbd.vendor_commitment.so_that"));
2878 assert!(ids.contains(&"jtbd.vendor_commitment.evidence.vendor_assessment"));
2879 assert!(ids.contains(&"jtbd.vendor_commitment.evidence.po"));
2880 assert!(ids.contains(&"jtbd.vendor_commitment.failure.bypassed_approval"));
2881 assert!(!ids.iter().any(|id| id.contains("[0]") || id.contains(".0")));
2882 }
2883
2884 #[test]
2885 fn explicit_clause_key_preserves_id_while_fingerprint_changes() {
2886 let mut first = vendor_input();
2887 first.evidence_required = vec![ClauseInput::with_key("risk_review", "risk review")];
2888
2889 let mut second = first.clone();
2890 second.evidence_required = vec![ClauseInput::with_key(
2891 "risk_review",
2892 "fresh risk review with policy citations",
2893 )];
2894
2895 let first = JtbdDocument::from_input(first).unwrap();
2896 let second = JtbdDocument::from_input(second).unwrap();
2897
2898 let first_clause = first
2899 .clauses
2900 .iter()
2901 .find(|clause| clause.key == "risk_review")
2902 .unwrap();
2903 let second_clause = second
2904 .clauses
2905 .iter()
2906 .find(|clause| clause.key == "risk_review")
2907 .unwrap();
2908
2909 assert_eq!(first_clause.id, second_clause.id);
2910 assert_ne!(first_clause.fingerprint, second_clause.fingerprint);
2911 }
2912
2913 #[test]
2914 fn lineage_map_closure_requires_every_clause_to_be_accounted_for() {
2915 let document = JtbdDocument::from_input(vendor_input()).unwrap();
2916 let map = LineageMap::single_artifact_from_document(
2917 &document,
2918 ArtifactId::new("truth_projection.vendor_commitment.v1"),
2919 ArtifactKind::TruthProjection,
2920 "truth_projection.v0",
2921 "0.10.0",
2922 );
2923
2924 assert!(map.validate_closure(&document).is_ok());
2925
2926 let mut missing_one = map.clone();
2927 missing_one.artifacts[0].source_clause_ids.pop();
2928 assert!(matches!(
2929 missing_one.validate_closure(&document),
2930 Err(LineageError::UnaccountedClause { .. })
2931 ));
2932
2933 let mut unknown = map;
2934 unknown.artifacts[0]
2935 .source_clause_ids
2936 .push(ClauseId::new("vendor_commitment", "evidence.unknown"));
2937 assert!(matches!(
2938 unknown.validate_closure(&document),
2939 Err(LineageError::UnknownArtifactClause { .. })
2940 ));
2941 }
2942
2943 #[test]
2944 fn decode_jtbd_builds_parseable_package_spine() {
2945 let package = decode_jtbd(vendor_input()).unwrap();
2946
2947 assert!(
2948 package
2949 .generated_truths
2950 .contains("Truth: Approve a vendor commitment")
2951 );
2952 assert!(
2953 package
2954 .generated_truths
2955 .contains("Requires: purchase order")
2956 );
2957 assert!(
2958 package
2959 .generated_truths
2960 .contains("Must Not: bypassed approval")
2961 );
2962 assert_eq!(package.truth_version, TRUTH_VERSION);
2963 assert_eq!(
2964 package.intent_packet.outcome,
2965 "spend is traceable and policy-compliant"
2966 );
2967 assert_eq!(
2968 package
2969 .intent_packet
2970 .context
2971 .get("truth_package_id")
2972 .and_then(serde_json::Value::as_str),
2973 Some(package.package_id.as_str())
2974 );
2975 assert_eq!(package.verifier_spec.required_evidence.len(), 2);
2976 assert_eq!(package.verifier_spec.forbidden_actions.len(), 2);
2977 assert_eq!(package.proof_obligations.len(), 4);
2978 assert!(
2979 package
2980 .lineage
2981 .validate_closure(&package.source_jtbd)
2982 .is_ok()
2983 );
2984 }
2985
2986 #[test]
2987 fn decode_jtbd_is_deterministic_for_same_semantic_input() {
2988 let mut reordered = vendor_input();
2989 reordered.evidence_required.reverse();
2990 reordered.failure_modes.reverse();
2991
2992 let first = decode_jtbd(vendor_input()).unwrap();
2993 let second = decode_jtbd(reordered).unwrap();
2994
2995 assert_eq!(first.package_id, second.package_id);
2996 assert_eq!(first.intent_packet.id, second.intent_packet.id);
2997 assert_eq!(first.generated_truths, second.generated_truths);
2998 assert_eq!(
2999 serde_json::to_value(&first).unwrap(),
3000 serde_json::to_value(&second).unwrap()
3001 );
3002 }
3003
3004 #[test]
3005 fn truth_projection_overlay_versions_without_mutating_package() {
3006 let package = decode_jtbd(vendor_input()).unwrap();
3007 let original_truths = package.generated_truths.clone();
3008 let mut edited_truths = package.generated_truths.clone();
3009 edited_truths.push_str(" And a finance owner can review the evidence\n");
3010 let actor_clause = package
3011 .source_jtbd
3012 .clauses_by_kind(JtbdClauseKind::Actor)
3013 .next()
3014 .unwrap()
3015 .id
3016 .clone();
3017 let overlay = TruthProjectionOverlay::new(
3018 package.package_id.clone(),
3019 package.truth_version.clone(),
3020 "v1.operator-review",
3021 edited_truths.clone(),
3022 "operator clarified review evidence",
3023 vec![actor_clause.clone()],
3024 );
3025
3026 let applied = package.apply_projection_overlay(overlay.clone()).unwrap();
3027
3028 assert_eq!(package.generated_truths, original_truths);
3029 assert_eq!(applied.truths, edited_truths);
3030 assert_eq!(applied.projection_version, "v1.operator-review");
3031 assert_eq!(
3032 applied.source,
3033 TruthProjectionSource::OverlayApplied {
3034 overlay_id: overlay.overlay_id,
3035 reason: "operator clarified review evidence".to_string(),
3036 }
3037 );
3038 assert_eq!(applied.lineage.source_clause_ids, vec![actor_clause]);
3039 assert_eq!(package.base_projection().truths, original_truths);
3040 }
3041
3042 #[test]
3043 fn truth_projection_overlay_rejects_mismatches_and_unknown_clauses() {
3044 let package = decode_jtbd(vendor_input()).unwrap();
3045 let known_clause = package.source_jtbd.clause_ids().next().unwrap().clone();
3046 let valid_overlay = TruthProjectionOverlay::new(
3047 package.package_id.clone(),
3048 package.truth_version.clone(),
3049 "v1.operator-review",
3050 package.generated_truths.clone(),
3051 "operator clarified review evidence",
3052 vec![known_clause],
3053 );
3054
3055 let mut wrong_package = valid_overlay.clone();
3056 wrong_package.target_package_id = TruthPackageId::new("truth_package.other");
3057 assert!(matches!(
3058 package.apply_projection_overlay(wrong_package),
3059 Err(TruthOverlayError::PackageMismatch { .. })
3060 ));
3061
3062 let mut wrong_version = valid_overlay.clone();
3063 wrong_version.target_truth_version = "v0".to_string();
3064 assert!(matches!(
3065 package.apply_projection_overlay(wrong_version),
3066 Err(TruthOverlayError::TruthVersionMismatch { .. })
3067 ));
3068
3069 let mut unknown_clause = valid_overlay.clone();
3070 unknown_clause.source_clause_ids =
3071 vec![ClauseId::new("vendor_commitment", "actor.missing")];
3072 assert!(matches!(
3073 package.apply_projection_overlay(unknown_clause),
3074 Err(TruthOverlayError::UnknownSourceClause { .. })
3075 ));
3076
3077 let mut invalid_truth = valid_overlay;
3078 invalid_truth.edited_truths = "Truth: Broken\n\n Unknown:\n Value: bad\n".to_string();
3079 assert!(matches!(
3080 package.apply_projection_overlay(invalid_truth),
3081 Err(TruthOverlayError::TruthProjectionParse { .. })
3082 ));
3083 }
3084
3085 #[test]
3086 fn observed_stop_reason_matches_expected_set() {
3087 let expected = BTreeSet::from([
3088 ExpectedStopReason::Converged,
3089 ExpectedStopReason::CriteriaMet,
3090 ]);
3091
3092 let criteria_met = ObservedStopReason::CriteriaMet {
3093 criteria: vec!["evidence_ready".to_string()],
3094 };
3095 let exhausted = ObservedStopReason::TokenBudgetExhausted {
3096 tokens_consumed: 10_001,
3097 limit: 10_000,
3098 };
3099
3100 assert!(criteria_met.matches_expected(&expected));
3101 assert!(!exhausted.matches_expected(&expected));
3102 assert!(exhausted.is_budget_exhausted());
3103 assert_eq!(
3104 ObservedStopReason::HitlGatePending {
3105 gate_id: "gate-1".to_string(),
3106 proposal_id: "proposal-1".to_string(),
3107 summary: "approval required".to_string(),
3108 agent_id: "truth-policy-gate".to_string(),
3109 cycle: 2,
3110 }
3111 .expectation_kind(),
3112 ExpectedStopReason::HitlGatePending
3113 );
3114 }
3115
3116 #[test]
3117 fn axiom_run_report_carries_stop_reason_facts_and_integrity() {
3118 let package = decode_jtbd(vendor_input()).unwrap();
3119 let evidence_clause = package
3120 .source_jtbd
3121 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
3122 .next()
3123 .unwrap()
3124 .id
3125 .clone();
3126 let observation = AxiomRunObservation {
3127 stop_reason: ObservedStopReason::Converged,
3128 promoted_facts: vec![PromotedFactRecord {
3129 context_key: "Evidence".to_string(),
3130 fact_id: "fact.vendor_assessment".to_string(),
3131 summary: "vendor assessment present".to_string(),
3132 source_clause_ids: vec![evidence_clause.clone()],
3133 evidence_refs: vec![EvidenceRefRecord {
3134 evidence_id: "evidence.vendor_assessment".to_string(),
3135 source: "axiom_truth_package".to_string(),
3136 }],
3137 trace_link: Some(TraceLinkRecord {
3138 trace_id: "trace.vendor_commitment.1".to_string(),
3139 location: Some("fixture://vendor_commitment".to_string()),
3140 replayable: true,
3141 }),
3142 promotion_authority: None,
3143 }],
3144 integrity: RunIntegrityProof::sha256_merkle("sha256:abc123", 7, 5),
3145 replay_notes: vec!["deterministic replay profile matched".to_string()],
3146 run_stages: Vec::new(),
3147 };
3148
3149 let report =
3150 AxiomRunReport::from_observation(&package, AxiomRunVerdict::Satisfied, observation);
3151
3152 assert_eq!(report.package_id, package.package_id);
3153 assert_eq!(report.truth_version, "v1");
3154 assert_eq!(report.intent_packet_id, package.intent_packet.id);
3155 assert_eq!(report.verdict, AxiomRunVerdict::Satisfied);
3156 assert!(report.expected_stop_reason_matched());
3157 assert_eq!(report.promoted_facts.len(), 1);
3158 assert_eq!(
3159 report.promoted_facts[0].source_clause_ids,
3160 vec![evidence_clause]
3161 );
3162 assert_eq!(report.integrity.merkle_root, "sha256:abc123");
3163 assert_eq!(report.integrity.clock_time, 7);
3164 assert_eq!(report.integrity.fact_count, 5);
3165 assert_eq!(
3166 report.source_clause_ids.len(),
3167 package.source_jtbd.clauses.len()
3168 );
3169 assert_eq!(
3170 serde_json::to_value(&report).unwrap()["observed_stop_reason"]["kind"],
3171 "converged"
3172 );
3173 }
3174
3175 fn evidence_clause_id(package: &TruthPackage, key: &str) -> ClauseId {
3176 package
3177 .source_jtbd
3178 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
3179 .find(|clause| clause.key == key)
3180 .map_or_else(
3181 || panic!("missing evidence clause {key}"),
3182 |clause| clause.id.clone(),
3183 )
3184 }
3185
3186 fn promoted_fact(
3187 context_key: &str,
3188 fact_id: &str,
3189 summary: &str,
3190 source_clause_ids: Vec<ClauseId>,
3191 ) -> PromotedFactRecord {
3192 PromotedFactRecord {
3193 context_key: context_key.to_string(),
3194 fact_id: fact_id.to_string(),
3195 summary: summary.to_string(),
3196 source_clause_ids,
3197 evidence_refs: vec![EvidenceRefRecord {
3198 evidence_id: format!("evidence.{fact_id}"),
3199 source: "test_fixture".to_string(),
3200 }],
3201 trace_link: Some(TraceLinkRecord {
3202 trace_id: format!("trace.{fact_id}"),
3203 location: Some("test://verifier".to_string()),
3204 replayable: true,
3205 }),
3206 promotion_authority: None,
3207 }
3208 }
3209
3210 fn satisfying_observation(package: &TruthPackage) -> AxiomRunObservation {
3211 let promoted_facts: Vec<PromotedFactRecord> = package
3212 .source_jtbd
3213 .clauses_by_kind(JtbdClauseKind::EvidenceRequired)
3214 .map(|clause| {
3215 promoted_fact(
3216 "Evidence",
3217 &format!("fact.{}", clause.key),
3218 &format!("{} observed", clause.canonical_text),
3219 vec![clause.id.clone()],
3220 )
3221 })
3222 .collect();
3223 AxiomRunObservation {
3224 stop_reason: ObservedStopReason::Converged,
3225 promoted_facts,
3226 integrity: RunIntegrityProof::sha256_merkle("sha256:test", 1, 1),
3227 replay_notes: vec!["deterministic".to_string()],
3228 run_stages: Vec::new(),
3229 }
3230 }
3231
3232 #[test]
3233 fn verify_satisfied_when_evidence_complete_and_stop_expected() {
3234 let package = decode_jtbd(vendor_input()).unwrap();
3235 let observation = satisfying_observation(&package);
3236
3237 assert_eq!(package.verify(&observation), AxiomRunVerdict::Satisfied);
3238
3239 let report = AxiomRunReport::verify(&package, observation);
3240 assert_eq!(report.verdict, AxiomRunVerdict::Satisfied);
3241 assert!(report.expected_stop_reason_matched());
3242 }
3243
3244 #[test]
3245 fn verify_invalid_when_promoted_fact_cites_unknown_clause() {
3246 let package = decode_jtbd(vendor_input()).unwrap();
3247 let mut observation = satisfying_observation(&package);
3248 observation.promoted_facts.push(promoted_fact(
3249 "Evidence",
3250 "fact.unknown",
3251 "stray fact with no real clause",
3252 vec![ClauseId::new("vendor_commitment", "evidence.missing")],
3253 ));
3254
3255 assert_eq!(package.verify(&observation), AxiomRunVerdict::Invalid);
3256 }
3257
3258 #[test]
3259 fn verify_invalid_when_forbidden_action_text_appears_in_summary() {
3260 let package = decode_jtbd(vendor_input()).unwrap();
3261 let mut observation = satisfying_observation(&package);
3262 observation.promoted_facts.push(promoted_fact(
3263 "Diagnostic",
3264 "fact.violation",
3265 "bypassed approval detected on commitment ABC",
3266 vec![evidence_clause_id(&package, "vendor_assessment")],
3267 ));
3268
3269 assert_eq!(package.verify(&observation), AxiomRunVerdict::Invalid);
3270 }
3271
3272 #[test]
3273 fn verify_invalid_when_forbidden_action_text_appears_in_replay_note() {
3274 let package = decode_jtbd(vendor_input()).unwrap();
3275 let mut observation = satisfying_observation(&package);
3276 observation
3277 .replay_notes
3278 .push("missing audit trail surfaced post-run".to_string());
3279
3280 assert_eq!(package.verify(&observation), AxiomRunVerdict::Invalid);
3281 }
3282
3283 #[test]
3284 fn verify_exhausted_when_unexpected_budget_exhaustion() {
3285 let package = decode_jtbd(vendor_input()).unwrap();
3286 let mut observation = satisfying_observation(&package);
3287 observation.stop_reason = ObservedStopReason::TokenBudgetExhausted {
3288 tokens_consumed: 1_000_000,
3289 limit: 100_000,
3290 };
3291
3292 assert_eq!(package.verify(&observation), AxiomRunVerdict::Exhausted);
3293 }
3294
3295 #[test]
3296 fn verify_blocked_when_unexpected_hitl_gate_pending() {
3297 let package = decode_jtbd(vendor_input()).unwrap();
3298 let mut observation = satisfying_observation(&package);
3299 observation.stop_reason = ObservedStopReason::HitlGatePending {
3300 gate_id: "gate-1".to_string(),
3301 proposal_id: "proposal-1".to_string(),
3302 summary: "approval required".to_string(),
3303 agent_id: "policy-gate".to_string(),
3304 cycle: 3,
3305 };
3306
3307 assert_eq!(package.verify(&observation), AxiomRunVerdict::Blocked);
3308 }
3309
3310 #[test]
3311 fn verify_invalid_when_invariant_violated() {
3312 let package = decode_jtbd(vendor_input()).unwrap();
3313 let mut observation = satisfying_observation(&package);
3314 observation.stop_reason = ObservedStopReason::InvariantViolated {
3315 class: "structural".to_string(),
3316 name: "spend_authority".to_string(),
3317 reason: "ceiling exceeded".to_string(),
3318 };
3319
3320 assert_eq!(package.verify(&observation), AxiomRunVerdict::Invalid);
3321 }
3322
3323 #[test]
3324 fn verify_invalid_when_expected_stop_but_evidence_missing() {
3325 let package = decode_jtbd(vendor_input()).unwrap();
3326 let evidence = evidence_clause_id(&package, "vendor_assessment");
3327 let observation = AxiomRunObservation {
3329 stop_reason: ObservedStopReason::Converged,
3330 promoted_facts: vec![promoted_fact(
3331 "Evidence",
3332 "fact.vendor_assessment",
3333 "vendor assessment captured",
3334 vec![evidence],
3335 )],
3336 integrity: RunIntegrityProof::sha256_merkle("sha256:test", 1, 1),
3337 replay_notes: vec![],
3338 run_stages: Vec::new(),
3339 };
3340
3341 assert_eq!(package.verify(&observation), AxiomRunVerdict::Invalid);
3342 }
3343
3344 #[test]
3345 fn time_budget_absent_uses_epoch_sentinel_and_omits_context_field() {
3346 let package = decode_jtbd(vendor_input()).unwrap();
3347
3348 assert!(
3349 package
3350 .generated_truths
3351 .contains("Expires: 2099-01-01T00:00:00Z")
3352 );
3353 assert_eq!(package.source_jtbd.time_budget, None);
3354 assert!(
3355 package
3356 .intent_packet
3357 .context
3358 .get("time_budget_seconds")
3359 .is_none()
3360 );
3361 }
3362
3363 #[test]
3364 fn time_budget_shifts_expires_and_populates_intent_context() {
3365 let input = vendor_input().with_time_budget(TimeBudget::from_minutes(45));
3366 let package = decode_jtbd(input).unwrap();
3367
3368 assert!(
3369 package
3370 .generated_truths
3371 .contains("Expires: 2099-01-01T00:45:00Z")
3372 );
3373 assert_eq!(
3374 package.source_jtbd.time_budget,
3375 Some(TimeBudget::from_minutes(45))
3376 );
3377 assert_eq!(
3378 package.intent_packet.context["time_budget_seconds"],
3379 serde_json::json!(45 * 60)
3380 );
3381 }
3382
3383 #[test]
3384 fn time_budget_preserves_decode_determinism() {
3385 let first =
3386 decode_jtbd(vendor_input().with_time_budget(TimeBudget::from_hours(2))).unwrap();
3387 let second =
3388 decode_jtbd(vendor_input().with_time_budget(TimeBudget::from_hours(2))).unwrap();
3389
3390 assert_eq!(first.package_id, second.package_id);
3391 assert_eq!(first.intent_packet.id, second.intent_packet.id);
3392 assert_eq!(first.generated_truths, second.generated_truths);
3393 assert_eq!(
3394 serde_json::to_value(&first).unwrap(),
3395 serde_json::to_value(&second).unwrap()
3396 );
3397 }
3398
3399 #[test]
3400 fn audit_fact_lineage_succeeds_when_facts_cite_evidence_clauses() {
3401 let package = decode_jtbd(vendor_input()).unwrap();
3402 let report = AxiomRunReport::verify(&package, satisfying_observation(&package));
3403
3404 let audit = report.audit_fact_lineage(&package).unwrap();
3405 assert_eq!(audit.package_id, package.package_id);
3406 assert_eq!(audit.truth_version, package.truth_version);
3407 assert_eq!(
3408 audit.facts_audited,
3409 package.verifier_spec.required_evidence.len()
3410 );
3411 assert_eq!(audit.evidence_coverage.len(), 2);
3412 assert!(audit.failure_coverage.is_empty());
3413 }
3414
3415 #[test]
3416 fn audit_fact_lineage_rejects_unknown_clause() {
3417 let package = decode_jtbd(vendor_input()).unwrap();
3418 let mut observation = satisfying_observation(&package);
3419 observation.promoted_facts.push(promoted_fact(
3420 "Evidence",
3421 "fact.unknown",
3422 "stray fact",
3423 vec![ClauseId::new("vendor_commitment", "evidence.missing")],
3424 ));
3425 let report =
3428 AxiomRunReport::from_observation(&package, AxiomRunVerdict::Satisfied, observation);
3429
3430 match report.audit_fact_lineage(&package) {
3431 Err(FactLineageAuditError::UnknownClause { fact_id, .. }) => {
3432 assert_eq!(fact_id, "fact.unknown");
3433 }
3434 other => panic!("expected UnknownClause, got {other:?}"),
3435 }
3436 }
3437
3438 #[test]
3439 fn audit_fact_lineage_rejects_fact_that_cites_only_scope_clauses() {
3440 let package = decode_jtbd(vendor_input()).unwrap();
3441 let actor_clause = package
3442 .source_jtbd
3443 .clauses_by_kind(JtbdClauseKind::Actor)
3444 .next()
3445 .unwrap()
3446 .id
3447 .clone();
3448 let observation = AxiomRunObservation {
3449 stop_reason: ObservedStopReason::Converged,
3450 promoted_facts: vec![promoted_fact(
3451 "Diagnostic",
3452 "fact.scope-only",
3453 "actor identity confirmed",
3454 vec![actor_clause],
3455 )],
3456 integrity: RunIntegrityProof::sha256_merkle("sha256:test", 1, 1),
3457 replay_notes: vec![],
3458 run_stages: Vec::new(),
3459 };
3460 let report =
3461 AxiomRunReport::from_observation(&package, AxiomRunVerdict::Satisfied, observation);
3462
3463 match report.audit_fact_lineage(&package) {
3464 Err(FactLineageAuditError::ScopeOnlyFact { fact_id }) => {
3465 assert_eq!(fact_id, "fact.scope-only");
3466 }
3467 other => panic!("expected ScopeOnlyFact, got {other:?}"),
3468 }
3469 }
3470
3471 #[test]
3472 fn audit_fact_lineage_rejects_package_id_mismatch() {
3473 let package = decode_jtbd(vendor_input()).unwrap();
3474 let mut report = AxiomRunReport::verify(&package, satisfying_observation(&package));
3475 report.package_id = TruthPackageId::new("truth_package.other");
3476
3477 assert!(matches!(
3478 report.audit_fact_lineage(&package),
3479 Err(FactLineageAuditError::PackageMismatch { .. })
3480 ));
3481 }
3482
3483 #[test]
3484 fn audit_fact_lineage_rejects_truth_version_mismatch() {
3485 let package = decode_jtbd(vendor_input()).unwrap();
3486 let mut report = AxiomRunReport::verify(&package, satisfying_observation(&package));
3487 report.truth_version = "v0".to_string();
3488
3489 assert!(matches!(
3490 report.audit_fact_lineage(&package),
3491 Err(FactLineageAuditError::TruthVersionMismatch { .. })
3492 ));
3493 }
3494}