Skip to main content

a3s_code_core/research/
mod.rs

1//! Native research-run contracts for the A3S Code scientific workflow.
2//!
3//! This module contains bounded, digest-bound transport values. It does not
4//! resolve packages, choose scientific methods, or decide whether a finding
5//! is acceptable. A3S Use owns package and environment authority; hosts own
6//! scientific policy and human decisions.
7
8mod citation;
9mod claim;
10mod error;
11mod event;
12mod evidence;
13mod evidence_graph;
14mod protocol;
15mod provenance;
16mod reproducibility;
17mod review;
18mod review_batch;
19mod run;
20mod workflow;
21
22pub use citation::{ResearchCitationV1, RESEARCH_CITATION_SCHEMA_V1};
23pub use claim::{ResearchClaimStatusV1, ResearchClaimV1, RESEARCH_CLAIM_SCHEMA_V1};
24pub use error::ResearchContractError;
25pub use event::{ResearchEventV1, RESEARCH_EVENT_SCHEMA_V1, RESEARCH_MAX_EVENT_TYPE_BYTES};
26pub use evidence::{
27    ResearchEvidenceFactKindV1, ResearchEvidenceFactV1, RESEARCH_EVIDENCE_FACT_SCHEMA_V1,
28    RESEARCH_MAX_FACT_METADATA, RESEARCH_MAX_METADATA_VALUE_BYTES,
29};
30pub use evidence_graph::{
31    ResearchEvidenceCompletenessV1, ResearchEvidenceGraphV1, RESEARCH_EVIDENCE_GRAPH_SCHEMA_V1,
32    RESEARCH_MAX_EVIDENCE_GRAPH_CITATIONS, RESEARCH_MAX_EVIDENCE_GRAPH_CLAIMS,
33};
34pub use protocol::{
35    ResearchProtocolError, ResearchWireEnvelopeV1, ResearchWireKindDescriptorV1,
36    ResearchWireKindV1, ResearchWireTypeV1, RESEARCH_PROTOCOL_SCHEMA_V1,
37    RESEARCH_PROTOCOL_VERSION_V1, RESEARCH_WIRE_KIND_DESCRIPTORS_V1,
38};
39pub use provenance::{
40    ResearchArtifactKindV1, ResearchProvenanceReceiptV1, RESEARCH_ARTIFACT_KINDS,
41    RESEARCH_PROVENANCE_RECEIPT_SCHEMA_V1,
42};
43pub use reproducibility::{
44    ResearchReproducibilityManifestV1, RESEARCH_REPRODUCIBILITY_MANIFEST_SCHEMA_V1,
45};
46pub use review::{
47    ResearchReviewCategoryV1, ResearchReviewFindingV1, ResearchReviewLocationV1,
48    ResearchReviewSeverityV1, ResearchReviewStatusV1, RESEARCH_REVIEW_FINDING_SCHEMA_V1,
49};
50pub use review_batch::{
51    ResearchReviewBatchV1, RESEARCH_MAX_REVIEW_FINDINGS, RESEARCH_REVIEW_BATCH_SCHEMA_V1,
52};
53pub use run::{
54    ResearchReproducibilityV1, ResearchRunStatusV1, ResearchRunV1, RESEARCH_RUN_SCHEMA_V1,
55};
56pub use workflow::{
57    ResearchRerunLineageV1, ResearchWorkflowPlanV1, ResearchWorkflowStepV1,
58    RESEARCH_MAX_WORKFLOW_STEPS, RESEARCH_RERUN_LINEAGE_SCHEMA_V1,
59    RESEARCH_WORKFLOW_PLAN_SCHEMA_V1, RESEARCH_WORKFLOW_STEP_SCHEMA_V1,
60};
61
62/// Maximum JSON payload accepted by the explicit research wire helpers.
63pub use protocol::RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES;
64pub(crate) const RESEARCH_MAX_ID_BYTES: usize = 256;
65pub(crate) const RESEARCH_MAX_TEXT_BYTES: usize = 16 * 1024;
66pub(crate) const RESEARCH_MAX_DIGESTS: usize = 512;
67
68pub(crate) fn decode_json_slice<T>(bytes: &[u8]) -> Result<T, ResearchContractError>
69where
70    T: serde::de::DeserializeOwned,
71{
72    if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
73        return Err(ResearchContractError::Encoding);
74    }
75    serde_json::from_slice(bytes)
76        .map_err(|error| ResearchContractError::Serialization(error.to_string()))
77}
78
79pub(crate) fn encode_json<T: serde::Serialize + ?Sized>(
80    value: &T,
81) -> Result<Vec<u8>, ResearchContractError> {
82    let bytes = serde_json::to_vec(value)
83        .map_err(|error| ResearchContractError::Serialization(error.to_string()))?;
84    if bytes.len() > RESEARCH_PROTOCOL_MAX_MESSAGE_BYTES {
85        return Err(ResearchContractError::Encoding);
86    }
87    Ok(bytes)
88}
89
90pub(crate) fn validate_id(field: &'static str, value: &str) -> Result<(), ResearchContractError> {
91    if value.is_empty()
92        || value.len() > RESEARCH_MAX_ID_BYTES
93        || value.contains('\0')
94        || value.contains(['\r', '\n'])
95    {
96        return Err(ResearchContractError::InvalidField(field));
97    }
98    Ok(())
99}
100
101pub(crate) fn validate_text(
102    field: &'static str,
103    value: &str,
104    max_bytes: usize,
105) -> Result<(), ResearchContractError> {
106    if value.is_empty()
107        || value.len() > max_bytes
108        || value.contains('\0')
109        || value.contains(['\r', '\n'])
110    {
111        return Err(ResearchContractError::InvalidField(field));
112    }
113    Ok(())
114}
115
116pub(crate) fn validate_digest_field(
117    field: &'static str,
118    value: &str,
119) -> Result<(), ResearchContractError> {
120    crate::evaluation::validate_digest(value)
121        .map_err(|_| ResearchContractError::InvalidDigest(field))
122}
123
124pub(crate) fn digest<T: serde::Serialize>(
125    domain: &'static str,
126    value: &T,
127) -> Result<String, ResearchContractError> {
128    crate::evaluation::digest_json(domain, value)
129        .map_err(|error| ResearchContractError::Serialization(error.to_string()))
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn bounded_identifiers_reject_all_ascii_line_endings() {
138        for value in ["reviewer\n", "reviewer\r", "reviewer\r\n", "reviewer\nnext"] {
139            assert_eq!(
140                validate_id("id", value),
141                Err(ResearchContractError::InvalidField("id"))
142            );
143        }
144    }
145
146    #[test]
147    fn bounded_text_rejects_all_ascii_line_endings() {
148        for value in ["finding\n", "finding\r", "finding\r\n", "finding\nnext"] {
149            assert_eq!(
150                validate_text("text", value, 128),
151                Err(ResearchContractError::InvalidField("text"))
152            );
153        }
154    }
155}