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,
};
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::evaluation::validate_digest(value)
.map_err(|_| ResearchContractError::InvalidDigest(field))
}
pub(crate) fn digest<T: serde::Serialize>(
domain: &'static str,
value: &T,
) -> Result<String, ResearchContractError> {
crate::evaluation::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"))
);
}
}
}