Skip to main content

cordance_cortex/
builder.rs

1//! Receipt builder — produces a `cordance-cortex-receipt-v1-candidate` from a
2//! compiled `CordancePack`.
3//!
4//! Field mapping mirrors:
5//!   pai-axiom/PAI/Fixtures/TrustExchange/
6//!     cortex-pai-axiom-execution-receipt-v1-candidate.json
7//! with `cordance_*` prefixes and pack-derived values substituted throughout.
8//!
9//! All receipt structs in `cordance_core::receipt` are `#[non_exhaustive]`,
10//! so this module must construct values via the typed `new` constructors
11//! rather than struct-literal syntax. See `cordance_core::receipt` for the
12//! rationale (ADR 0005 / `BUILD_SPEC` §11.2).
13
14use chrono::Utc;
15use cordance_core::pack::CordancePack;
16use cordance_core::receipt::{
17    AuthorityBoundary, CortexReceiptV1Candidate, ExecutionTrust, OperatorApproval, ReceiptBody,
18    RuntimeIntegrity, SourceAnchor, SourceContext, TruthCeiling,
19};
20use cordance_core::schema;
21use tracing::debug;
22
23/// The nine forbidden uses that every cordance candidate receipt must carry.
24const FORBIDDEN_USES: &[&str] = &[
25    "claim Cortex truth",
26    "claim Cortex admission",
27    "promote Cortex memory",
28    "promote Cortex doctrine",
29    "create trusted history",
30    "claim release acceptance",
31    "authorize runtime writes",
32    "claim P6 or P9 closure",
33    "accept or promote an ADR",
34];
35
36/// Build a `CortexReceiptV1Candidate` from a compiled pack.
37///
38/// # Errors
39/// Returns `CortexError::Validation` if the assembled receipt fails structural
40/// validation.
41pub fn build_receipt(pack: &CordancePack) -> Result<CortexReceiptV1Candidate, crate::CortexError> {
42    debug!(
43        project = %pack.project.name,
44        "building cortex candidate receipt"
45    );
46
47    let context_id = if pack.source_lock.pack_id.is_empty() {
48        pack.project.name.clone()
49    } else {
50        pack.source_lock.pack_id.clone()
51    };
52
53    let source_anchors: Vec<SourceAnchor> = pack
54        .sources
55        .iter()
56        .filter(|r| !r.blocked)
57        .map(|r| SourceAnchor::new(r.id.clone(), r.path.clone(), r.sha256.clone()))
58        .collect();
59
60    let mut allowed_claim_language: Vec<String> = pack.residual_risk.clone();
61    allowed_claim_language
62        .push("cordance candidate receipt for cortex context-pack admit-cordance".into());
63    allowed_claim_language.push("candidate-only evidence".into());
64    allowed_claim_language.push("no durable promotion".into());
65    allowed_claim_language.push("no release acceptance".into());
66
67    let mut residual_risk: Vec<String> = vec![
68        "Cortex may reject or quarantine this receipt under its native parser.".into(),
69        "claim_ceiling=candidate_evidence_only".into(),
70    ];
71    // Carry through any pack-level residual risk that isn't already included.
72    for item in &pack.residual_risk {
73        if !residual_risk.contains(item) {
74            residual_risk.push(item.clone());
75        }
76    }
77
78    let source_context = SourceContext::new(
79        context_id,
80        TruthCeiling::CandidateEvidenceOnly,
81        "not_cleared_by_boundary_crossing".into(),
82    );
83
84    let execution_trust = ExecutionTrust::new(
85        "local_candidate_only".into(),
86        "deny_authority_grant".into(),
87        "not_field_level_bound_for_cortex".into(),
88        "not_field_level_bound_for_cortex".into(),
89    );
90
91    let runtime_integrity = RuntimeIntegrity::new(false, false, "not_requested".into());
92
93    let operator_approval = OperatorApproval::new(
94        true,
95        "not_supplied_for_cortex_promotion".into(),
96        "not_supplied_for_cortex_promotion".into(),
97    );
98
99    // Cortex receipts are operator-initiated attestations of a specific
100    // submission event; unlike `pack.json` (which is byte-deterministic by
101    // design — round-4 bughunt #1 removed `CordancePack.generated_at` for
102    // this exact reason), a receipt naturally captures the moment of
103    // submission. We construct the timestamp locally instead of pulling it
104    // off the pack.
105    let generated_at = Utc::now();
106    let body = ReceiptBody::new(
107        format!("cordance-candidate-{}", generated_at.format("%Y-%m-%d")),
108        generated_at,
109        "candidate_only".into(),
110        "partial_structural_evidence".into(),
111        "repo_only_no_runtime_write".into(),
112        "not_bound_for_cortex_promotion".into(),
113        format!("cordance-pack-{}", pack.project.name),
114        source_context,
115        execution_trust,
116        runtime_integrity,
117        operator_approval,
118        allowed_claim_language,
119        FORBIDDEN_USES.iter().map(|s| (*s).to_owned()).collect(),
120        source_anchors,
121        residual_risk,
122    );
123
124    let receipt = CortexReceiptV1Candidate::new(
125        schema::CORDANCE_CORTEX_RECEIPT_V1_CANDIDATE.into(),
126        1,
127        "candidate_only".into(),
128        "cortex context-pack admit-cordance".into(),
129        AuthorityBoundary::candidate_only(),
130        body,
131        vec![
132            "Cortex field-level tool_provenance consumer absent".into(),
133            "Cortex operator approval hash binding absent".into(),
134        ],
135    );
136
137    crate::validator::validate_receipt(&receipt)?;
138    Ok(receipt)
139}