Skip to main content

weavatrix_refactor_plan/
operation.rs

1use crate::{Completeness, FileEdit, PlanError, PlanEvidence, PlanFingerprint, TextEdit};
2use blazingly_json::Value;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6/// Stable wire schema owned by this crate.
7pub const REFACTOR_PLAN_SCHEMA: &str = "weavatrix.refactor-plan.v1";
8
9/// A bounded, versioned collection of logical refactor operations and evidence.
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct RefactorPlan {
13    pub schema_version: String,
14    pub operation: String,
15    /// One simultaneous transition set; array order is identity, not execution order.
16    pub operations: Vec<RefactorOperation>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub completeness: Option<Completeness>,
19    #[serde(flatten)]
20    pub evidence: PlanEvidence,
21}
22
23impl RefactorPlan {
24    #[must_use]
25    pub fn new(operation: impl Into<String>, operations: Vec<RefactorOperation>) -> Self {
26        Self {
27            schema_version: REFACTOR_PLAN_SCHEMA.to_owned(),
28            operation: operation.into(),
29            operations,
30            completeness: None,
31            evidence: PlanEvidence::default(),
32        }
33    }
34
35    pub fn validate(&self) -> Result<crate::ValidatedConsumerPlan<'_>, PlanError> {
36        crate::validate_consumer_plan(self, crate::RefactorPlanLimits::default())
37    }
38
39    pub fn validate_with(
40        &self,
41        limits: crate::RefactorPlanLimits,
42    ) -> Result<crate::ValidatedConsumerPlan<'_>, PlanError> {
43        crate::validate_consumer_plan(self, limits)
44    }
45
46    pub fn fingerprint(&self) -> Result<PlanFingerprint, PlanError> {
47        crate::fingerprint_plan(self)
48    }
49
50    /// Converts a legacy edit envelope without losing text edits or extensions.
51    ///
52    /// # This requires a capturing decode
53    ///
54    /// The edit envelope declares only `schemaVersion`, `operation`, `files`,
55    /// and `completeness`, so every annotation this crate understands —
56    /// `createdAt`, `graphRevision`, `completenessProof`,
57    /// `uncertainReferences`, `notModified`, `warnings`, `followUp`,
58    /// `syntaxCheck` — arrives as an undeclared member and lives in
59    /// `crate::EditPlan::extensions`.
60    ///
61    /// A plan decoded through [`crate::weavatrix_edit::DeclaredEditPlan`] has
62    /// empty extension maps at every level. This conversion then returns `Ok`
63    /// with [`PlanEvidence::default()`](crate::PlanEvidence), reporting neither
64    /// an error nor a warning, the extension budget in
65    /// [`validate_with`](Self::validate_with) has nothing left to weigh, and the
66    /// [`fingerprint`](Self::fingerprint) differs from the one the same wire
67    /// document yields after a capturing decode. Decode through
68    /// [`crate::EditPlan`] whenever the evidence matters.
69    pub fn from_text_edit_plan(plan: crate::EditPlan) -> Result<Self, PlanError> {
70        crate::conversion::from_text_edit_plan(plan)
71    }
72
73    /// Converts to a legacy edit envelope if every operation is a text modify.
74    pub fn try_into_text_edit_plan(self) -> Result<crate::EditPlan, PlanError> {
75        crate::conversion::into_text_edit_plan(self)
76    }
77}
78
79/// One logical operation in a refactor plan.
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
82#[serde(deny_unknown_fields)]
83pub enum RefactorOperation {
84    Modify(FileEdit),
85    Create(CreateFile),
86    Delete(DeleteFile),
87    Rename(RenameFile),
88}
89
90/// Exact UTF-8 contents to create at a path that must be absent.
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct CreateFile {
94    pub path: String,
95    pub contents: String,
96    #[serde(default, skip_serializing_if = "CreatePermissions::is_default")]
97    pub permissions: CreatePermissions,
98    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
99    pub extensions: BTreeMap<String, Value>,
100}
101
102impl CreateFile {
103    #[must_use]
104    pub fn new(path: impl Into<String>, contents: impl Into<String>) -> Self {
105        Self {
106            path: path.into(),
107            contents: contents.into(),
108            permissions: CreatePermissions::default(),
109            extensions: BTreeMap::new(),
110        }
111    }
112
113    #[must_use]
114    pub const fn with_executable(mut self, executable: bool) -> Self {
115        self.permissions.executable = executable;
116        self
117    }
118}
119
120/// Deterministic portable permission policy for a newly created source file.
121#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123#[serde(deny_unknown_fields)]
124pub struct CreatePermissions {
125    pub executable: bool,
126}
127
128impl CreatePermissions {
129    #[must_use]
130    pub const fn is_default(&self) -> bool {
131        !self.executable
132    }
133
134    #[must_use]
135    pub const fn readonly(self) -> bool {
136        false
137    }
138
139    #[must_use]
140    pub const fn unix_mode(self) -> u32 {
141        if self.executable { 0o755 } else { 0o644 }
142    }
143}
144
145/// Delete an existing file only when its complete contents match.
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct DeleteFile {
149    pub path: String,
150    pub expected_sha256: String,
151    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
152    pub extensions: BTreeMap<String, Value>,
153}
154
155impl DeleteFile {
156    #[must_use]
157    pub fn new(path: impl Into<String>, expected_sha256: impl Into<String>) -> Self {
158        Self {
159            path: path.into(),
160            expected_sha256: expected_sha256.into(),
161            extensions: BTreeMap::new(),
162        }
163    }
164}
165
166/// Move one exact source to an absent destination.
167///
168/// `edits` use v1 UTF-16 coordinates against the original `from` contents
169/// guarded by `expected_source_sha256`, before the move.
170#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct RenameFile {
173    pub from: String,
174    pub to: String,
175    pub expected_source_sha256: String,
176    #[serde(default, skip_serializing_if = "Vec::is_empty")]
177    pub edits: Vec<TextEdit>,
178    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
179    pub extensions: BTreeMap<String, Value>,
180}
181
182impl RenameFile {
183    #[must_use]
184    pub fn new(
185        from: impl Into<String>,
186        to: impl Into<String>,
187        expected_source_sha256: impl Into<String>,
188    ) -> Self {
189        Self {
190            from: from.into(),
191            to: to.into(),
192            expected_source_sha256: expected_source_sha256.into(),
193            edits: Vec::new(),
194            extensions: BTreeMap::new(),
195        }
196    }
197
198    #[must_use]
199    pub fn with_edits(mut self, edits: Vec<TextEdit>) -> Self {
200        self.edits = edits;
201        self
202    }
203}