a3s-code-core 8.5.4

A3S Code Core - Embeddable AI agent library with tool execution
Documentation
//! Native research-run contracts for the A3S Code scientific workflow.
//!
//! This module contains bounded, digest-bound transport values. It does not
//! resolve packages, choose scientific methods, or decide whether a finding
//! is acceptable. A3S Use owns package and environment authority; hosts own
//! scientific policy and human decisions.

mod citation;
mod claim;
mod error;
mod event;
mod evidence;
mod evidence_graph;
mod protocol;
mod provenance;
mod reproducibility;
mod review;
mod review_batch;
mod run;
mod workflow;

pub use citation::{ResearchCitationV1, RESEARCH_CITATION_SCHEMA_V1};
pub use claim::{ResearchClaimStatusV1, ResearchClaimV1, RESEARCH_CLAIM_SCHEMA_V1};
pub use error::ResearchContractError;
pub use event::{ResearchEventV1, RESEARCH_EVENT_SCHEMA_V1, RESEARCH_MAX_EVENT_TYPE_BYTES};
pub use evidence::{
    ResearchEvidenceFactKindV1, ResearchEvidenceFactV1, RESEARCH_EVIDENCE_FACT_SCHEMA_V1,
    RESEARCH_MAX_FACT_METADATA, RESEARCH_MAX_METADATA_VALUE_BYTES,
};
pub use evidence_graph::{
    ResearchEvidenceCompletenessV1, ResearchEvidenceGraphV1, RESEARCH_EVIDENCE_GRAPH_SCHEMA_V1,
    RESEARCH_MAX_EVIDENCE_GRAPH_CITATIONS, RESEARCH_MAX_EVIDENCE_GRAPH_CLAIMS,
};
pub use protocol::{
    ResearchProtocolError, ResearchWireEnvelopeV1, ResearchWireKindDescriptorV1,
    ResearchWireKindV1, ResearchWireTypeV1, RESEARCH_PROTOCOL_SCHEMA_V1,
    RESEARCH_PROTOCOL_VERSION_V1, RESEARCH_WIRE_KIND_DESCRIPTORS_V1,
};
pub use provenance::{
    ResearchArtifactKindV1, ResearchProvenanceReceiptV1, RESEARCH_ARTIFACT_KINDS,
    RESEARCH_PROVENANCE_RECEIPT_SCHEMA_V1,
};
pub use reproducibility::{
    ResearchReproducibilityManifestV1, RESEARCH_REPRODUCIBILITY_MANIFEST_SCHEMA_V1,
};
pub use review::{
    ResearchReviewCategoryV1, ResearchReviewFindingV1, ResearchReviewLocationV1,
    ResearchReviewSeverityV1, ResearchReviewStatusV1, RESEARCH_REVIEW_FINDING_SCHEMA_V1,
};
pub use review_batch::{
    ResearchReviewBatchV1, RESEARCH_MAX_REVIEW_FINDINGS, RESEARCH_REVIEW_BATCH_SCHEMA_V1,
};
pub use run::{
    ResearchReproducibilityV1, ResearchRunStatusV1, ResearchRunV1, RESEARCH_RUN_SCHEMA_V1,
};
pub use workflow::{
    ResearchRerunLineageV1, ResearchWorkflowPlanV1, ResearchWorkflowStepV1,
    RESEARCH_MAX_WORKFLOW_STEPS, RESEARCH_RERUN_LINEAGE_SCHEMA_V1,
    RESEARCH_WORKFLOW_PLAN_SCHEMA_V1, RESEARCH_WORKFLOW_STEP_SCHEMA_V1,
};

/// Maximum JSON payload accepted by the explicit research wire helpers.
pub use protocol::RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES;
pub(crate) const RESEARCH_MAX_ID_BYTES: usize = 256;
pub(crate) const RESEARCH_MAX_TEXT_BYTES: usize = 16 * 1024;
pub(crate) const RESEARCH_MAX_DIGESTS: usize = 512;

pub(crate) fn decode_json_slice<T>(bytes: &[u8]) -> Result<T, ResearchContractError>
where
    T: serde::de::DeserializeOwned,
{
    if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
        return Err(ResearchContractError::Encoding);
    }
    serde_json::from_slice(bytes)
        .map_err(|error| ResearchContractError::Serialization(error.to_string()))
}

pub(crate) fn encode_json<T: serde::Serialize + ?Sized>(
    value: &T,
) -> Result<Vec<u8>, ResearchContractError> {
    let bytes = serde_json::to_vec(value)
        .map_err(|error| ResearchContractError::Serialization(error.to_string()))?;
    if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
        return Err(ResearchContractError::Encoding);
    }
    Ok(bytes)
}

pub(crate) fn validate_id(field: &'static str, value: &str) -> Result<(), ResearchContractError> {
    if value.is_empty()
        || value.len() > RESEARCH_MAX_ID_BYTES
        || value.contains('\0')
        || value.contains(['\r', '\n'])
    {
        return Err(ResearchContractError::InvalidField(field));
    }
    Ok(())
}

pub(crate) fn validate_text(
    field: &'static str,
    value: &str,
    max_bytes: usize,
) -> Result<(), ResearchContractError> {
    if value.is_empty()
        || value.len() > max_bytes
        || value.contains('\0')
        || value.contains(['\r', '\n'])
    {
        return Err(ResearchContractError::InvalidField(field));
    }
    Ok(())
}

pub(crate) fn validate_digest_field(
    field: &'static str,
    value: &str,
) -> Result<(), ResearchContractError> {
    crate::content_digest::validate_digest(value)
        .map_err(|_| ResearchContractError::InvalidDigest(field))
}

pub(crate) fn digest<T: serde::Serialize>(
    domain: &'static str,
    value: &T,
) -> Result<String, ResearchContractError> {
    crate::content_digest::digest_json(domain, value)
        .map_err(|error| ResearchContractError::Serialization(error.to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn bounded_identifiers_reject_all_ascii_line_endings() {
        for value in ["reviewer\n", "reviewer\r", "reviewer\r\n", "reviewer\nnext"] {
            assert_eq!(
                validate_id("id", value),
                Err(ResearchContractError::InvalidField("id"))
            );
        }
    }

    #[test]
    fn bounded_text_rejects_all_ascii_line_endings() {
        for value in ["finding\n", "finding\r", "finding\r\n", "finding\nnext"] {
            assert_eq!(
                validate_text("text", value, 128),
                Err(ResearchContractError::InvalidField("text"))
            );
        }
    }
}