weavatrix-refactor-plan 0.1.1

Evidence metadata, validation profiles, and canonical fingerprints for Weavatrix refactor plans
Documentation
use crate::canonical::{CanonicalError, CanonicalSerializer};
use crate::{
    Completeness, CompletenessProof, GraphRevision, NotModified, PlanError, PlanErrorCode,
    RefactorOperation, RefactorPlan, RefactorPlanLimits, StatusCode, UncertainReference,
    WarningCode,
};
use blazingly_json::Value;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
use sha2::{Digest, Sha256};
use std::{fmt, io, io::Write as _, str::FromStr};

/// Versioned algorithm identifier included as the fingerprint domain separator.
pub const FINGERPRINT_ALGORITHM: &str = "weavatrix.refactor-plan.jcs-sha256.v1";

const DOMAIN_SEPARATOR: &[u8] = b"weavatrix.refactor-plan.jcs-sha256.v1\0";

/// A SHA-256 fingerprint over the validated JCS refactor-plan contract.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PlanFingerprint([u8; 32]);

impl PlanFingerprint {
    #[must_use]
    pub const fn as_bytes(&self) -> &[u8; 32] {
        &self.0
    }

    #[must_use]
    pub fn to_hex(self) -> String {
        let mut output = String::with_capacity(64);
        for byte in self.0 {
            use fmt::Write as _;
            write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
        }
        output
    }
}

impl fmt::Display for PlanFingerprint {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.to_hex())
    }
}

/// An invalid encoded plan fingerprint.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FingerprintParseError;

impl fmt::Display for FingerprintParseError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("fingerprint must be 64 lowercase hexadecimal characters")
    }
}

impl std::error::Error for FingerprintParseError {}

impl FromStr for PlanFingerprint {
    type Err = FingerprintParseError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.len() != 64
            || !value
                .bytes()
                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
        {
            return Err(FingerprintParseError);
        }
        let mut bytes = [0_u8; 32];
        for (index, output) in bytes.iter_mut().enumerate() {
            let start = index * 2;
            *output = u8::from_str_radix(&value[start..start + 2], 16)
                .map_err(|_| FingerprintParseError)?;
        }
        Ok(Self(bytes))
    }
}

impl Serialize for PlanFingerprint {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_hex())
    }
}

impl<'de> Deserialize<'de> for PlanFingerprint {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct FingerprintVisitor;

        impl Visitor<'_> for FingerprintVisitor {
            type Value = PlanFingerprint;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a 64-character lowercase SHA-256 fingerprint")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                value.parse().map_err(E::custom)
            }
        }

        deserializer.deserialize_str(FingerprintVisitor)
    }
}

/// Validates with default limits and returns JCS bytes excluding top-level `createdAt`.
pub fn canonical_plan_bytes(plan: &RefactorPlan) -> Result<Vec<u8>, PlanError> {
    canonical_plan_bytes_with_limits(plan, RefactorPlanLimits::default())
}

/// Validates with caller limits and returns the canonical fingerprint payload.
pub fn canonical_plan_bytes_with_limits(
    plan: &RefactorPlan,
    limits: RefactorPlanLimits,
) -> Result<Vec<u8>, PlanError> {
    crate::validation::validate_for_fingerprint(plan, limits)?;
    let mut bytes = Vec::with_capacity(256);
    write_canonical_top_level(&mut bytes, plan)?;
    Ok(bytes)
}

/// Computes a validated, domain-separated JCS fingerprint with default limits.
pub fn fingerprint_plan(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
    fingerprint_plan_with_limits(plan, RefactorPlanLimits::default())
}

/// Computes a validated, domain-separated JCS fingerprint with caller limits.
pub fn fingerprint_plan_with_limits(
    plan: &RefactorPlan,
    limits: RefactorPlanLimits,
) -> Result<PlanFingerprint, PlanError> {
    crate::validation::validate_for_fingerprint(plan, limits)?;
    fingerprint_validated(plan)
}

pub(crate) fn fingerprint_validated(plan: &RefactorPlan) -> Result<PlanFingerprint, PlanError> {
    let mut digest = Sha256::new();
    digest.update(DOMAIN_SEPARATOR);
    let mut writer = io::BufWriter::with_capacity(8 * 1024, DigestWriter(&mut digest));
    write_canonical_top_level(&mut writer, plan)?;
    writer.flush().map_err(|error| io_error(&error))?;
    drop(writer);
    Ok(PlanFingerprint(digest.finalize().into()))
}

fn write_canonical_top_level<W: io::Write>(
    writer: W,
    plan: &RefactorPlan,
) -> Result<(), PlanError> {
    let mut entries = top_level_entries(plan);
    entries
        .sort_unstable_by(|left, right| crate::canonical::compare_utf16_order(left.key, right.key));
    write_sorted_entries(writer, &entries).map_err(|error| canonical_error(&error))
}

fn write_sorted_entries<W: io::Write>(
    writer: W,
    entries: &[Entry<'_>],
) -> Result<(), CanonicalError> {
    let mut serializer = CanonicalSerializer::new(writer);
    serializer.write_punctuation(b"{")?;
    for (index, entry) in entries.iter().enumerate() {
        if index != 0 {
            serializer.write_punctuation(b",")?;
        }
        serializer.write_string(entry.key)?;
        serializer.write_punctuation(b":")?;
        serializer.write_value(&entry.value)?;
    }
    serializer.write_punctuation(b"}")
}

fn top_level_entries(plan: &RefactorPlan) -> Vec<Entry<'_>> {
    let mut entries = vec![
        Entry::new("schemaVersion", TopValue::String(&plan.schema_version)),
        Entry::new("operation", TopValue::String(&plan.operation)),
        Entry::new("operations", TopValue::Operations(&plan.operations)),
    ];
    if let Some(value) = &plan.completeness {
        entries.push(Entry::new("completeness", TopValue::Completeness(value)));
    }
    push_evidence_entries(&mut entries, plan);
    for (key, value) in &plan.evidence.extensions {
        entries.push(Entry::new(key, TopValue::Extension(value)));
    }
    entries
}

fn push_evidence_entries<'a>(entries: &mut Vec<Entry<'a>>, plan: &'a RefactorPlan) {
    let evidence = &plan.evidence;
    if !evidence.graph_revision.is_missing() {
        entries.push(Entry::new(
            "graphRevision",
            TopValue::GraphRevision(&evidence.graph_revision),
        ));
    }
    if let Some(value) = &evidence.completeness_proof {
        entries.push(Entry::new("completenessProof", TopValue::Proof(value)));
    }
    if let Some(value) = &evidence.uncertain_references {
        entries.push(Entry::new(
            "uncertainReferences",
            TopValue::Uncertain(value),
        ));
    }
    if let Some(value) = &evidence.not_modified {
        entries.push(Entry::new("notModified", TopValue::Omitted(value)));
    }
    if let Some(value) = &evidence.warnings {
        entries.push(Entry::new("warnings", TopValue::Warnings(value)));
    }
    if let Some(value) = &evidence.follow_up {
        entries.push(Entry::new("followUp", TopValue::String(value)));
    }
    if let Some(value) = &evidence.syntax_check {
        entries.push(Entry::new("syntaxCheck", TopValue::Status(value)));
    }
}

struct Entry<'a> {
    key: &'a str,
    value: TopValue<'a>,
}

impl<'a> Entry<'a> {
    const fn new(key: &'a str, value: TopValue<'a>) -> Self {
        Self { key, value }
    }
}

enum TopValue<'a> {
    String(&'a str),
    Operations(&'a [RefactorOperation]),
    Completeness(&'a Completeness),
    GraphRevision(&'a GraphRevision),
    Proof(&'a CompletenessProof),
    Uncertain(&'a [UncertainReference]),
    Omitted(&'a [NotModified]),
    Warnings(&'a [WarningCode]),
    Status(&'a StatusCode),
    Extension(&'a Value),
}

impl Serialize for TopValue<'_> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match self {
            Self::String(value) => value.serialize(serializer),
            Self::Operations(value) => value.serialize(serializer),
            Self::Completeness(value) => value.serialize(serializer),
            Self::GraphRevision(value) => value.serialize(serializer),
            Self::Proof(value) => value.serialize(serializer),
            Self::Uncertain(value) => value.serialize(serializer),
            Self::Omitted(value) => value.serialize(serializer),
            Self::Warnings(value) => value.serialize(serializer),
            Self::Status(value) => value.serialize(serializer),
            Self::Extension(value) => value.serialize(serializer),
        }
    }
}

struct DigestWriter<'a>(&'a mut Sha256);

impl io::Write for DigestWriter<'_> {
    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
        self.0.update(buffer);
        Ok(buffer.len())
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

fn canonical_error(error: &CanonicalError) -> PlanError {
    PlanError::new(
        PlanErrorCode::JsonEncoding,
        format!("could not encode canonical plan JSON: {error}"),
    )
}

fn io_error(error: &io::Error) -> PlanError {
    PlanError::new(
        PlanErrorCode::JsonEncoding,
        format!("could not write canonical plan JSON: {error}"),
    )
}