weavatrix-refactor-plan 0.1.0

Evidence metadata, validation profiles, and canonical fingerprints for Weavatrix refactor plans
Documentation
use crate::metadata::{JsonBudget, JsonLimits};
use crate::{PlanError, PlanErrorCode, RefactorOperation, RefactorPlan, RefactorPlanLimits};
use blazingly_json::Value;
use std::collections::BTreeMap;

const PLAN_FIELDS: [&str; 12] = [
    "schemaVersion",
    "operation",
    "operations",
    "completeness",
    "createdAt",
    "graphRevision",
    "completenessProof",
    "uncertainReferences",
    "notModified",
    "warnings",
    "followUp",
    "syntaxCheck",
];

pub(crate) fn validate_extensions(
    plan: &RefactorPlan,
    limits: RefactorPlanLimits,
) -> Result<(), PlanError> {
    let mut budget = JsonBudget::new(JsonLimits {
        bytes: limits.max_extension_bytes,
        nodes: limits.max_extension_nodes,
        depth: limits.max_extension_depth,
        key_bytes: limits.max_path_bytes,
    });
    visit(&mut budget, &plan.evidence.extensions, &PLAN_FIELDS, || {
        "plan".to_owned()
    })?;
    visit_evidence(&mut budget, plan)?;
    for (index, operation) in plan.operations.iter().enumerate() {
        visit_operation(&mut budget, operation, index)?;
    }
    Ok(())
}

fn visit_evidence(budget: &mut JsonBudget, plan: &RefactorPlan) -> Result<(), PlanError> {
    if let Some(proof) = &plan.evidence.completeness_proof {
        visit(budget, &proof.extensions, &["scope", "planner"], || {
            "completenessProof".to_owned()
        })?;
        visit(
            budget,
            &proof.scope.extensions,
            &["kind", "value", "roots", "languages"],
            || "completenessProof.scope".to_owned(),
        )?;
        visit(
            budget,
            &proof.planner.extensions,
            &["name", "version", "backend", "backendVersion"],
            || "completenessProof.planner".to_owned(),
        )?;
    }
    for (index, reference) in plan
        .evidence
        .uncertain_references
        .iter()
        .flatten()
        .enumerate()
    {
        visit(
            budget,
            &reference.extensions,
            &[
                "path", "file", "line", "subject", "kind", "reason", "excerpt",
            ],
            || format!("uncertainReferences[{index}]"),
        )?;
        if let Some(subject) = &reference.subject {
            visit_subject(budget, &subject.extensions, || {
                format!("uncertainReferences[{index}]")
            })?;
        }
    }
    visit_not_modified(budget, plan)
}

fn visit_not_modified(budget: &mut JsonBudget, plan: &RefactorPlan) -> Result<(), PlanError> {
    for (index, entry) in plan.evidence.not_modified.iter().flatten().enumerate() {
        visit(
            budget,
            &entry.extensions,
            &["path", "file", "subject", "operationIndex", "reason"],
            || format!("notModified[{index}]"),
        )?;
        if let Some(subject) = &entry.subject {
            visit_subject(budget, &subject.extensions, || {
                format!("notModified[{index}]")
            })?;
        }
    }
    Ok(())
}

fn visit_subject<F: Fn() -> String>(
    budget: &mut JsonBudget,
    extensions: &BTreeMap<String, Value>,
    parent: F,
) -> Result<(), PlanError> {
    visit(budget, extensions, &["kind", "value"], || {
        format!("{}.subject", parent())
    })
}

fn visit_operation(
    budget: &mut JsonBudget,
    operation: &RefactorOperation,
    index: usize,
) -> Result<(), PlanError> {
    match operation {
        RefactorOperation::Modify(file) => visit(
            budget,
            &file.extensions,
            &["path", "sha256", "edits"],
            || "modify".to_owned(),
        )
        .and_then(|()| visit_edits(budget, &file.edits, index)),
        RefactorOperation::Create(file) => visit(
            budget,
            &file.extensions,
            &["path", "contents", "permissions"],
            || "create".to_owned(),
        ),
        RefactorOperation::Delete(file) => visit(
            budget,
            &file.extensions,
            &["path", "expectedSha256"],
            || "delete".to_owned(),
        ),
        RefactorOperation::Rename(file) => visit(
            budget,
            &file.extensions,
            &["from", "to", "expectedSourceSha256", "edits"],
            || "rename".to_owned(),
        )
        .and_then(|()| visit_edits(budget, &file.edits, index)),
    }
    .map_err(|error| error.at_operation(index))
}

fn visit_edits(
    budget: &mut JsonBudget,
    edits: &[crate::TextEdit],
    operation_index: usize,
) -> Result<(), PlanError> {
    const FIELDS: [&str; 7] = [
        "startLine",
        "startChar",
        "endLine",
        "endChar",
        "before",
        "after",
        "provenance",
    ];
    for (index, edit) in edits.iter().enumerate() {
        visit(budget, &edit.extensions, &FIELDS, || {
            format!("operations[{operation_index}].edits[{index}]")
        })?;
    }
    Ok(())
}

/// Validates one extension map; `field` renders the diagnostic location and
/// only runs when the map is actually rejected.
fn visit<F: FnOnce() -> String>(
    budget: &mut JsonBudget,
    extensions: &BTreeMap<String, Value>,
    reserved: &[&str],
    field: F,
) -> Result<(), PlanError> {
    if extensions.is_empty() {
        return Ok(());
    }
    if let Some(key) = extensions
        .keys()
        .find(|key| reserved.contains(&key.as_str()))
    {
        return Err(PlanError::new(
            PlanErrorCode::ExtensionConflict,
            format!("extension field {key:?} is reserved"),
        )
        .at_field(format!("{}.{key}", field())));
    }
    budget.visit_map(extensions)
}