cordance-cortex 0.1.1

Cordance Cortex adapter. Emits candidate receipts; never writes to the cortex repo or runtime.
Documentation
//! Receipt builder — produces a `cordance-cortex-receipt-v1-candidate` from a
//! compiled `CordancePack`.
//!
//! Field mapping mirrors:
//!   pai-axiom/PAI/Fixtures/TrustExchange/
//!     cortex-pai-axiom-execution-receipt-v1-candidate.json
//! with `cordance_*` prefixes and pack-derived values substituted throughout.
//!
//! All receipt structs in `cordance_core::receipt` are `#[non_exhaustive]`,
//! so this module must construct values via the typed `new` constructors
//! rather than struct-literal syntax. See `cordance_core::receipt` for the
//! rationale (ADR 0005 / `BUILD_SPEC` §11.2).

use chrono::Utc;
use cordance_core::pack::CordancePack;
use cordance_core::receipt::{
    AuthorityBoundary, CortexReceiptV1Candidate, ExecutionTrust, OperatorApproval, ReceiptBody,
    RuntimeIntegrity, SourceAnchor, SourceContext, TruthCeiling,
};
use cordance_core::schema;
use tracing::debug;

/// The nine forbidden uses that every cordance candidate receipt must carry.
const FORBIDDEN_USES: &[&str] = &[
    "claim Cortex truth",
    "claim Cortex admission",
    "promote Cortex memory",
    "promote Cortex doctrine",
    "create trusted history",
    "claim release acceptance",
    "authorize runtime writes",
    "claim P6 or P9 closure",
    "accept or promote an ADR",
];

/// Build a `CortexReceiptV1Candidate` from a compiled pack.
///
/// # Errors
/// Returns `CortexError::Validation` if the assembled receipt fails structural
/// validation.
pub fn build_receipt(pack: &CordancePack) -> Result<CortexReceiptV1Candidate, crate::CortexError> {
    debug!(
        project = %pack.project.name,
        "building cortex candidate receipt"
    );

    let context_id = if pack.source_lock.pack_id.is_empty() {
        pack.project.name.clone()
    } else {
        pack.source_lock.pack_id.clone()
    };

    let source_anchors: Vec<SourceAnchor> = pack
        .sources
        .iter()
        .filter(|r| !r.blocked)
        .map(|r| SourceAnchor::new(r.id.clone(), r.path.clone(), r.sha256.clone()))
        .collect();

    let mut allowed_claim_language: Vec<String> = pack.residual_risk.clone();
    allowed_claim_language
        .push("cordance candidate receipt for cortex context-pack admit-cordance".into());
    allowed_claim_language.push("candidate-only evidence".into());
    allowed_claim_language.push("no durable promotion".into());
    allowed_claim_language.push("no release acceptance".into());

    let mut residual_risk: Vec<String> = vec![
        "Cortex may reject or quarantine this receipt under its native parser.".into(),
        "claim_ceiling=candidate_evidence_only".into(),
    ];
    // Carry through any pack-level residual risk that isn't already included.
    for item in &pack.residual_risk {
        if !residual_risk.contains(item) {
            residual_risk.push(item.clone());
        }
    }

    let source_context = SourceContext::new(
        context_id,
        TruthCeiling::CandidateEvidenceOnly,
        "not_cleared_by_boundary_crossing".into(),
    );

    let execution_trust = ExecutionTrust::new(
        "local_candidate_only".into(),
        "deny_authority_grant".into(),
        "not_field_level_bound_for_cortex".into(),
        "not_field_level_bound_for_cortex".into(),
    );

    let runtime_integrity = RuntimeIntegrity::new(false, false, "not_requested".into());

    let operator_approval = OperatorApproval::new(
        true,
        "not_supplied_for_cortex_promotion".into(),
        "not_supplied_for_cortex_promotion".into(),
    );

    // Cortex receipts are operator-initiated attestations of a specific
    // submission event; unlike `pack.json` (which is byte-deterministic by
    // design — round-4 bughunt #1 removed `CordancePack.generated_at` for
    // this exact reason), a receipt naturally captures the moment of
    // submission. We construct the timestamp locally instead of pulling it
    // off the pack.
    let generated_at = Utc::now();
    let body = ReceiptBody::new(
        format!("cordance-candidate-{}", generated_at.format("%Y-%m-%d")),
        generated_at,
        "candidate_only".into(),
        "partial_structural_evidence".into(),
        "repo_only_no_runtime_write".into(),
        "not_bound_for_cortex_promotion".into(),
        format!("cordance-pack-{}", pack.project.name),
        source_context,
        execution_trust,
        runtime_integrity,
        operator_approval,
        allowed_claim_language,
        FORBIDDEN_USES.iter().map(|s| (*s).to_owned()).collect(),
        source_anchors,
        residual_risk,
    );

    let receipt = CortexReceiptV1Candidate::new(
        schema::CORDANCE_CORTEX_RECEIPT_V1_CANDIDATE.into(),
        1,
        "candidate_only".into(),
        "cortex context-pack admit-cordance".into(),
        AuthorityBoundary::candidate_only(),
        body,
        vec![
            "Cortex field-level tool_provenance consumer absent".into(),
            "Cortex operator approval hash binding absent".into(),
        ],
    );

    crate::validator::validate_receipt(&receipt)?;
    Ok(receipt)
}