Skip to main content

wenlan_types/
repair.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Approval-gated repair contracts shared by the daemon and local clients.
3
4mod frozen_v1;
5
6use crate::{
7    lint::{
8        LintCommitReceipt, LintDbSnapshotMode, LintDbSnapshotReceipt, LintDigest, LintEvidenceRef,
9        LintGateEffect, LintOpaqueId, LintOutcome, LintPageSnapshotMode, LintPageSnapshotReceipt,
10        LintProducerReceipt, LintProfile, LintReasonCode, LintSafeRootRelativePath, LintScope,
11        LintScopeKind, LintSemanticAction, LintSemanticFinding, LintSemanticProviderRoute,
12        LintSemanticReasonCode, LintSnapshotReceipts, LINT_CHECK_CATALOG_VERSION,
13        LINT_REPORT_SCHEMA_VERSION,
14    },
15    LintReport, MemoryType,
16};
17use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
18use std::{fmt, path::Component, path::Path};
19
20use frozen_v1::{
21    FrozenRepairApplyReceiptV1, FrozenRepairManifestPreBaselineV1, FrozenRepairManifestV1,
22    FrozenRepairRollbackArtifactV1, FrozenRepairVerificationReceiptV1,
23};
24
25pub const REPAIR_MANIFEST_SCHEMA_VERSION: u16 = 6;
26pub const REPAIR_ROLLBACK_FORMAT_VERSION: u16 = 2;
27pub const REPAIR_RECEIPT_SCHEMA_VERSION: u16 = 5;
28const PREVIOUS_REPAIR_MANIFEST_SCHEMA_VERSION: u16 = 5;
29const PREVIOUS_REPAIR_ROLLBACK_FORMAT_VERSION: u16 = 1;
30const PREVIOUS_REPAIR_RECEIPT_SCHEMA_VERSION: u16 = 4;
31const REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION: u16 = 4;
32pub const REPAIR_CLASSIFICATION_CHECK_ID: &str = "memories.semantic.classification";
33const PREVIOUS_LINT_REPORT_SCHEMA_VERSION: u16 = 4;
34const PREVIOUS_LINT_CHECK_CATALOG_VERSION: u16 = 2;
35const REPAIR_MEMORY_STATE_CHECK_ID: &str = "identity.memory_state_integrity";
36const REPAIR_TAG_INTEGRITY_CHECK_ID: &str = "identity.tag_integrity";
37const REPAIR_SUPERSESSION_CHECK_ID: &str = "memories.supersession_integrity";
38const REPAIR_MEMORY_ENTITY_INTEGRITY_CHECK_ID: &str = "memory_entities.integrity";
39const REPAIR_ORPHAN_LABELS_CHECK_ID: &str = "pages.links.orphan_labels";
40const REPAIR_PROJECTION_IDENTITY_CHECK_ID: &str = "pages.projection.identity";
41const REPAIR_PROJECTION_VERSION_CHECK_ID: &str = "pages.projection.version_alignment";
42const REPAIR_SOURCE_PAGE_INTEGRITY_CHECK_ID: &str = "pages.source_page_integrity";
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct StoredRepairDigestRef<'a>(&'a str);
46
47impl<'a> StoredRepairDigestRef<'a> {
48    pub const fn as_str(self) -> &'a str {
49        self.0
50    }
51}
52
53#[derive(Deserialize)]
54struct StoredManifestVersionProbe {
55    manifest_schema_version: u16,
56}
57
58#[derive(Deserialize)]
59struct StoredRollbackVersionProbe {
60    format_version: u16,
61}
62
63#[derive(Deserialize)]
64struct StoredReceiptVersionProbe {
65    receipt_schema_version: u16,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum StoredRepairManifest {
70    V1(FrozenRepairManifestV1),
71    V1PreBaseline(FrozenRepairManifestPreBaselineV1),
72    V2(Box<RepairManifest>),
73    V3(Box<RepairManifest>),
74    V4(Box<RepairManifest>),
75    V5(Box<RepairManifest>),
76    V6(Box<RepairManifest>),
77}
78
79impl StoredRepairManifest {
80    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
81        match serde_json::from_slice::<StoredManifestVersionProbe>(bytes)?.manifest_schema_version {
82            1 => match serde_json::from_slice(bytes) {
83                Ok(manifest) => Ok(Self::V1(manifest)),
84                Err(current_error) => serde_json::from_slice(bytes)
85                    .map(Self::V1PreBaseline)
86                    .map_err(|_| current_error),
87            },
88            2 => serde_json::from_slice(bytes).map(|manifest| Self::V2(Box::new(manifest))),
89            3 => serde_json::from_slice(bytes).map(|manifest| Self::V3(Box::new(manifest))),
90            4 => serde_json::from_slice(bytes).map(|manifest| Self::V4(Box::new(manifest))),
91            5 => serde_json::from_slice(bytes).map(|manifest| Self::V5(Box::new(manifest))),
92            6 => serde_json::from_slice(bytes).map(|manifest| Self::V6(Box::new(manifest))),
93            version => Err(serde_json::Error::custom(format!(
94                "unsupported repair manifest schema version {version}"
95            ))),
96        }
97    }
98
99    pub fn manifest_id(&self) -> &str {
100        match self {
101            Self::V1(manifest) => manifest.manifest_id(),
102            Self::V1PreBaseline(manifest) => manifest.manifest_id(),
103            Self::V2(manifest)
104            | Self::V3(manifest)
105            | Self::V4(manifest)
106            | Self::V5(manifest)
107            | Self::V6(manifest) => manifest.manifest_id(),
108        }
109    }
110
111    pub fn manifest_digest(&self) -> StoredRepairDigestRef<'_> {
112        match self {
113            Self::V1(manifest) => StoredRepairDigestRef(manifest.manifest_digest().as_str()),
114            Self::V1PreBaseline(manifest) => {
115                StoredRepairDigestRef(manifest.manifest_digest().as_str())
116            }
117            Self::V2(manifest)
118            | Self::V3(manifest)
119            | Self::V4(manifest)
120            | Self::V5(manifest)
121            | Self::V6(manifest) => StoredRepairDigestRef(manifest.manifest_digest().as_str()),
122        }
123    }
124
125    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
126        match self {
127            Self::V1(manifest) => manifest.canonical_unsigned_bytes(),
128            Self::V1PreBaseline(manifest) => manifest.canonical_unsigned_bytes(),
129            Self::V2(manifest)
130            | Self::V3(manifest)
131            | Self::V4(manifest)
132            | Self::V5(manifest)
133            | Self::V6(manifest) => manifest.canonical_unsigned_bytes(),
134        }
135    }
136
137    pub fn rollback_digest(&self) -> StoredRepairDigestRef<'_> {
138        match self {
139            Self::V1(manifest) => StoredRepairDigestRef(manifest.rollback().digest().as_str()),
140            Self::V1PreBaseline(manifest) => {
141                StoredRepairDigestRef(manifest.rollback().digest().as_str())
142            }
143            Self::V2(manifest)
144            | Self::V3(manifest)
145            | Self::V4(manifest)
146            | Self::V5(manifest)
147            | Self::V6(manifest) => StoredRepairDigestRef(manifest.rollback().digest().as_str()),
148        }
149    }
150
151    pub fn persisted_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
152        match self {
153            Self::V1(manifest) => serde_json::to_vec_pretty(manifest),
154            Self::V1PreBaseline(manifest) => serde_json::to_vec_pretty(manifest),
155            Self::V2(manifest)
156            | Self::V3(manifest)
157            | Self::V4(manifest)
158            | Self::V5(manifest)
159            | Self::V6(manifest) => serde_json::to_vec_pretty(manifest),
160        }
161    }
162
163    pub fn verify_and_try_into_current(
164        self,
165        verify: impl FnOnce(&[u8], StoredRepairDigestRef<'_>) -> bool,
166    ) -> Result<RepairManifest, RepairContractError> {
167        let canonical = self
168            .canonical_unsigned_bytes()
169            .map_err(|_| RepairContractError::InvalidManifest)?;
170        if !verify(&canonical, self.manifest_digest()) {
171            return Err(RepairContractError::InvalidDigest);
172        }
173        match self {
174            Self::V1(manifest) => frozen_manifest_v1_into_current(manifest),
175            Self::V1PreBaseline(manifest) => frozen_manifest_pre_baseline_v1_into_current(manifest),
176            Self::V2(manifest)
177            | Self::V3(manifest)
178            | Self::V4(manifest)
179            | Self::V5(manifest)
180            | Self::V6(manifest) => Ok(*manifest),
181        }
182    }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum RepairRollbackFileKind {
188    Missing,
189    Directory,
190    File,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
194pub struct RepairRollbackFileEntry {
195    relative_path: String,
196    kind: RepairRollbackFileKind,
197    #[serde(default, skip_serializing_if = "String::is_empty")]
198    content_hex: String,
199}
200
201#[derive(Deserialize)]
202#[serde(deny_unknown_fields)]
203struct RepairRollbackFileEntryWire {
204    relative_path: String,
205    kind: RepairRollbackFileKind,
206    #[serde(default)]
207    content_hex: String,
208}
209
210impl RepairRollbackFileEntry {
211    pub fn missing(relative_path: String) -> Result<Self, RepairContractError> {
212        Self::try_new(
213            relative_path,
214            RepairRollbackFileKind::Missing,
215            String::new(),
216        )
217    }
218
219    pub fn directory(relative_path: String) -> Result<Self, RepairContractError> {
220        Self::try_new(
221            relative_path,
222            RepairRollbackFileKind::Directory,
223            String::new(),
224        )
225    }
226
227    pub fn file(relative_path: String, content: Vec<u8>) -> Result<Self, RepairContractError> {
228        Self::try_new(
229            relative_path,
230            RepairRollbackFileKind::File,
231            encode_lower_hex(&content),
232        )
233    }
234
235    fn try_new(
236        relative_path: String,
237        kind: RepairRollbackFileKind,
238        content_hex: String,
239    ) -> Result<Self, RepairContractError> {
240        if !valid_root_relative_path(&relative_path)
241            || (kind == RepairRollbackFileKind::File
242                && (!is_lower_hex(&content_hex, content_hex.len())
243                    || !content_hex.len().is_multiple_of(2)))
244            || (kind != RepairRollbackFileKind::File && !content_hex.is_empty())
245        {
246            return Err(RepairContractError::InvalidRollbackArtifact);
247        }
248        Ok(Self {
249            relative_path,
250            kind,
251            content_hex,
252        })
253    }
254
255    pub fn relative_path(&self) -> &str {
256        &self.relative_path
257    }
258
259    pub const fn kind(&self) -> RepairRollbackFileKind {
260        self.kind
261    }
262
263    pub fn content_hex(&self) -> &str {
264        &self.content_hex
265    }
266}
267
268impl<'de> Deserialize<'de> for RepairRollbackFileEntry {
269    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
270    where
271        D: Deserializer<'de>,
272    {
273        let wire = RepairRollbackFileEntryWire::deserialize(deserializer)?;
274        Self::try_new(wire.relative_path, wire.kind, wire.content_hex).map_err(D::Error::custom)
275    }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
279#[serde(tag = "kind", rename_all = "snake_case")]
280pub enum RepairRollbackPayloadV2 {
281    SingleTable {
282        table: String,
283        source_id: String,
284        columns: Vec<String>,
285        rows: Vec<Vec<String>>,
286    },
287    RenamePageTitle {
288        page_id: String,
289        page_columns: Vec<String>,
290        before_page_row: Vec<String>,
291        projection_target_path: String,
292        projection_entries: Vec<RepairRollbackFileEntry>,
293    },
294    CompleteEntityExtraction {
295        memory_id: String,
296        memory_columns: Vec<String>,
297        before_memory_row: Vec<String>,
298        before_entity_ids: Vec<String>,
299        enrichment_status: String,
300        #[serde(skip_serializing_if = "Option::is_none")]
301        enrichment_error: Option<String>,
302        enrichment_attempts: i64,
303        enrichment_updated_at: i64,
304    },
305}
306
307#[derive(Deserialize)]
308#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
309enum RepairRollbackPayloadV2Wire {
310    SingleTable {
311        table: String,
312        source_id: String,
313        columns: Vec<String>,
314        rows: Vec<Vec<String>>,
315    },
316    RenamePageTitle {
317        page_id: String,
318        page_columns: Vec<String>,
319        before_page_row: Vec<String>,
320        projection_target_path: String,
321        projection_entries: Vec<RepairRollbackFileEntry>,
322    },
323    CompleteEntityExtraction {
324        memory_id: String,
325        memory_columns: Vec<String>,
326        before_memory_row: Vec<String>,
327        before_entity_ids: Vec<String>,
328        enrichment_status: String,
329        #[serde(default)]
330        enrichment_error: Option<String>,
331        enrichment_attempts: i64,
332        enrichment_updated_at: i64,
333    },
334}
335
336impl RepairRollbackPayloadV2 {
337    pub fn single_table(
338        table: String,
339        source_id: String,
340        columns: Vec<String>,
341        rows: Vec<Vec<String>>,
342    ) -> Result<Self, RepairContractError> {
343        if !valid_nonempty(&table)
344            || !valid_nonempty(&source_id)
345            || !valid_columns_and_rows(&columns, &rows)
346        {
347            return Err(RepairContractError::InvalidRollbackArtifact);
348        }
349        Ok(Self::SingleTable {
350            table,
351            source_id,
352            columns,
353            rows,
354        })
355    }
356
357    pub fn rename_page_title(
358        page_id: String,
359        page_columns: Vec<String>,
360        before_page_row: Vec<String>,
361        projection_target_path: String,
362        projection_entries: Vec<RepairRollbackFileEntry>,
363    ) -> Result<Self, RepairContractError> {
364        let target_path = Path::new(&projection_target_path);
365        let expected_paths = [".wenlan/state.json", projection_target_path.as_str()];
366        if !valid_nonempty(&page_id)
367            || page_columns.is_empty()
368            || page_columns.len() != before_page_row.len()
369            || !valid_unique_nonempty(&page_columns)
370            || !valid_root_relative_path(&projection_target_path)
371            || projection_target_path.starts_with('.')
372            || target_path.components().count() != 1
373            || target_path.extension().and_then(|value| value.to_str()) != Some("md")
374            || projection_entries.len() != expected_paths.len()
375            || !strictly_sorted_unique_by(&projection_entries, |entry| entry.relative_path())
376            || projection_entries
377                .iter()
378                .zip(expected_paths)
379                .any(|(entry, expected_path)| {
380                    entry.relative_path() != expected_path
381                        || entry.kind() != RepairRollbackFileKind::File
382                })
383        {
384            return Err(RepairContractError::InvalidRollbackArtifact);
385        }
386        Ok(Self::RenamePageTitle {
387            page_id,
388            page_columns,
389            before_page_row,
390            projection_target_path,
391            projection_entries,
392        })
393    }
394
395    #[allow(clippy::too_many_arguments)]
396    pub fn complete_entity_extraction(
397        memory_id: String,
398        memory_columns: Vec<String>,
399        before_memory_row: Vec<String>,
400        before_entity_ids: Vec<String>,
401        enrichment_status: String,
402        enrichment_error: Option<String>,
403        enrichment_attempts: i64,
404        enrichment_updated_at: i64,
405    ) -> Result<Self, RepairContractError> {
406        if !valid_nonempty(&memory_id)
407            || memory_columns.is_empty()
408            || memory_columns.len() != before_memory_row.len()
409            || !valid_unique_nonempty(&memory_columns)
410            || !valid_sorted_ids(&before_entity_ids)
411            || !valid_nonempty(&enrichment_status)
412            || enrichment_attempts < 0
413            || enrichment_updated_at <= 0
414        {
415            return Err(RepairContractError::InvalidRollbackArtifact);
416        }
417        Ok(Self::CompleteEntityExtraction {
418            memory_id,
419            memory_columns,
420            before_memory_row,
421            before_entity_ids,
422            enrichment_status,
423            enrichment_error,
424            enrichment_attempts,
425            enrichment_updated_at,
426        })
427    }
428}
429
430impl<'de> Deserialize<'de> for RepairRollbackPayloadV2 {
431    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
432    where
433        D: Deserializer<'de>,
434    {
435        match RepairRollbackPayloadV2Wire::deserialize(deserializer)? {
436            RepairRollbackPayloadV2Wire::SingleTable {
437                table,
438                source_id,
439                columns,
440                rows,
441            } => Self::single_table(table, source_id, columns, rows),
442            RepairRollbackPayloadV2Wire::RenamePageTitle {
443                page_id,
444                page_columns,
445                before_page_row,
446                projection_target_path,
447                projection_entries,
448            } => Self::rename_page_title(
449                page_id,
450                page_columns,
451                before_page_row,
452                projection_target_path,
453                projection_entries,
454            ),
455            RepairRollbackPayloadV2Wire::CompleteEntityExtraction {
456                memory_id,
457                memory_columns,
458                before_memory_row,
459                before_entity_ids,
460                enrichment_status,
461                enrichment_error,
462                enrichment_attempts,
463                enrichment_updated_at,
464            } => Self::complete_entity_extraction(
465                memory_id,
466                memory_columns,
467                before_memory_row,
468                before_entity_ids,
469                enrichment_status,
470                enrichment_error,
471                enrichment_attempts,
472                enrichment_updated_at,
473            ),
474        }
475        .map_err(D::Error::custom)
476    }
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
480pub struct RepairRollbackV2 {
481    format_version: u16,
482    payload: RepairRollbackPayloadV2,
483}
484
485#[derive(Deserialize)]
486#[serde(deny_unknown_fields)]
487struct RepairRollbackV2Wire {
488    format_version: u16,
489    payload: RepairRollbackPayloadV2,
490}
491
492impl RepairRollbackV2 {
493    pub fn try_new(payload: RepairRollbackPayloadV2) -> Result<Self, RepairContractError> {
494        Ok(Self {
495            format_version: REPAIR_ROLLBACK_FORMAT_VERSION,
496            payload,
497        })
498    }
499
500    pub const fn format_version(&self) -> u16 {
501        self.format_version
502    }
503
504    pub const fn payload(&self) -> &RepairRollbackPayloadV2 {
505        &self.payload
506    }
507}
508
509impl<'de> Deserialize<'de> for RepairRollbackV2 {
510    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
511    where
512        D: Deserializer<'de>,
513    {
514        let wire = RepairRollbackV2Wire::deserialize(deserializer)?;
515        if wire.format_version != REPAIR_ROLLBACK_FORMAT_VERSION {
516            return Err(D::Error::custom(
517                RepairContractError::InvalidRollbackArtifact,
518            ));
519        }
520        Self::try_new(wire.payload).map_err(D::Error::custom)
521    }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub enum StoredRepairRollbackArtifact {
526    V1(FrozenRepairRollbackArtifactV1),
527    V2(RepairRollbackV2),
528}
529
530impl StoredRepairRollbackArtifact {
531    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
532        match serde_json::from_slice::<StoredRollbackVersionProbe>(bytes)?.format_version {
533            1 => serde_json::from_slice(bytes).map(Self::V1),
534            2 => serde_json::from_slice(bytes).map(Self::V2),
535            version => Err(serde_json::Error::custom(format!(
536                "unsupported repair rollback format version {version}"
537            ))),
538        }
539    }
540
541    pub fn persisted_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
542        match self {
543            Self::V1(rollback) => serde_json::to_vec_pretty(rollback),
544            Self::V2(rollback) => serde_json::to_vec_pretty(rollback),
545        }
546    }
547
548    pub fn as_v1(&self) -> &FrozenRepairRollbackArtifactV1 {
549        match self {
550            Self::V1(rollback) => rollback,
551            Self::V2(_) => panic!("repair rollback is not frozen v1"),
552        }
553    }
554
555    pub const fn as_v2(&self) -> Option<&RepairRollbackV2> {
556        match self {
557            Self::V1(_) => None,
558            Self::V2(rollback) => Some(rollback),
559        }
560    }
561}
562
563#[derive(Debug, Clone, PartialEq, Eq)]
564pub enum StoredRepairApplyReceipt {
565    V1(FrozenRepairApplyReceiptV1),
566    V2(RepairApplyReceipt),
567    V3(RepairApplyReceipt),
568    V4(RepairApplyReceipt),
569    V5(RepairApplyReceipt),
570}
571
572impl StoredRepairApplyReceipt {
573    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
574        match serde_json::from_slice::<StoredReceiptVersionProbe>(bytes)?.receipt_schema_version {
575            1 => serde_json::from_slice(bytes).map(Self::V1),
576            2 => serde_json::from_slice(bytes).map(Self::V2),
577            3 => serde_json::from_slice(bytes).map(Self::V3),
578            4 => serde_json::from_slice(bytes).map(Self::V4),
579            5 => serde_json::from_slice(bytes).map(Self::V5),
580            version => Err(serde_json::Error::custom(format!(
581                "unsupported repair receipt schema version {version}"
582            ))),
583        }
584    }
585
586    pub fn receipt_digest(&self) -> StoredRepairDigestRef<'_> {
587        match self {
588            Self::V1(receipt) => StoredRepairDigestRef(receipt.receipt_digest().as_str()),
589            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
590                StoredRepairDigestRef(receipt.receipt_digest().as_str())
591            }
592        }
593    }
594
595    pub fn manifest_id(&self) -> &str {
596        match self {
597            Self::V1(receipt) => receipt.manifest_id(),
598            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
599                receipt.manifest_id()
600            }
601        }
602    }
603
604    pub fn manifest_digest(&self) -> StoredRepairDigestRef<'_> {
605        match self {
606            Self::V1(receipt) => StoredRepairDigestRef(receipt.manifest_digest().as_str()),
607            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
608                StoredRepairDigestRef(receipt.manifest_digest().as_str())
609            }
610        }
611    }
612
613    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
614        match self {
615            Self::V1(receipt) => receipt.canonical_unsigned_bytes(),
616            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
617                receipt.canonical_unsigned_bytes()
618            }
619        }
620    }
621
622    pub fn persisted_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
623        match self {
624            Self::V1(receipt) => serde_json::to_vec_pretty(receipt),
625            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
626                serde_json::to_vec_pretty(receipt)
627            }
628        }
629    }
630
631    pub fn verify_and_try_into_current(
632        self,
633        verify: impl FnOnce(&[u8], StoredRepairDigestRef<'_>) -> bool,
634    ) -> Result<RepairApplyReceipt, RepairContractError> {
635        let canonical = self
636            .canonical_unsigned_bytes()
637            .map_err(|_| RepairContractError::InvalidReceipt)?;
638        if !verify(&canonical, self.receipt_digest()) {
639            return Err(RepairContractError::InvalidDigest);
640        }
641        match self {
642            Self::V1(receipt) => frozen_apply_receipt_v1_into_current(receipt),
643            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
644                Ok(receipt)
645            }
646        }
647    }
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
651pub enum StoredRepairVerificationReceipt {
652    V1(FrozenRepairVerificationReceiptV1),
653    V2(RepairVerificationReceipt),
654    V3(RepairVerificationReceipt),
655    V4(RepairVerificationReceipt),
656    V5(RepairVerificationReceipt),
657}
658
659impl StoredRepairVerificationReceipt {
660    pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
661        match serde_json::from_slice::<StoredReceiptVersionProbe>(bytes)?.receipt_schema_version {
662            1 => serde_json::from_slice(bytes).map(Self::V1),
663            2 => serde_json::from_slice(bytes).map(Self::V2),
664            3 => serde_json::from_slice(bytes).map(Self::V3),
665            4 => serde_json::from_slice(bytes).map(Self::V4),
666            5 => serde_json::from_slice(bytes).map(Self::V5),
667            version => Err(serde_json::Error::custom(format!(
668                "unsupported repair receipt schema version {version}"
669            ))),
670        }
671    }
672
673    pub fn receipt_digest(&self) -> StoredRepairDigestRef<'_> {
674        match self {
675            Self::V1(receipt) => StoredRepairDigestRef(receipt.receipt_digest().as_str()),
676            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
677                StoredRepairDigestRef(receipt.receipt_digest().as_str())
678            }
679        }
680    }
681
682    pub fn manifest_id(&self) -> &str {
683        match self {
684            Self::V1(receipt) => receipt.manifest_id(),
685            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
686                receipt.manifest_id()
687            }
688        }
689    }
690
691    pub fn manifest_digest(&self) -> StoredRepairDigestRef<'_> {
692        match self {
693            Self::V1(receipt) => StoredRepairDigestRef(receipt.manifest_digest().as_str()),
694            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
695                StoredRepairDigestRef(receipt.manifest_digest().as_str())
696            }
697        }
698    }
699
700    pub fn apply_receipt_digest(&self) -> StoredRepairDigestRef<'_> {
701        match self {
702            Self::V1(receipt) => StoredRepairDigestRef(receipt.apply_receipt_digest().as_str()),
703            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
704                StoredRepairDigestRef(receipt.apply_receipt_digest().as_str())
705            }
706        }
707    }
708
709    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
710        match self {
711            Self::V1(receipt) => receipt.canonical_unsigned_bytes(),
712            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
713                receipt.canonical_unsigned_bytes()
714            }
715        }
716    }
717
718    pub fn persisted_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
719        match self {
720            Self::V1(receipt) => serde_json::to_vec_pretty(receipt),
721            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
722                serde_json::to_vec_pretty(receipt)
723            }
724        }
725    }
726
727    pub fn verify_and_try_into_current(
728        self,
729        verify: impl FnOnce(&[u8], StoredRepairDigestRef<'_>) -> bool,
730    ) -> Result<RepairVerificationReceipt, RepairContractError> {
731        let canonical = self
732            .canonical_unsigned_bytes()
733            .map_err(|_| RepairContractError::InvalidReceipt)?;
734        if !verify(&canonical, self.receipt_digest()) {
735            return Err(RepairContractError::InvalidDigest);
736        }
737        match self {
738            Self::V1(receipt) => frozen_verification_receipt_v1_into_current(receipt),
739            Self::V2(receipt) | Self::V3(receipt) | Self::V4(receipt) | Self::V5(receipt) => {
740                Ok(receipt)
741            }
742        }
743    }
744}
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq)]
747pub enum RepairContractError {
748    InvalidDigest,
749    InvalidManifestId,
750    InvalidSource,
751    InvalidTarget,
752    InvalidExpectedState,
753    UnsupportedWriter,
754    InvalidMutation,
755    InvalidAllowedEffects,
756    InvalidRollbackArtifact,
757    InvalidPostAssertions,
758    UnsupportedManifestSchema,
759    InvalidManifest,
760    InvalidPrepareRequest,
761    InvalidApplyRequest,
762    InvalidVerifyRequest,
763    InvalidReceipt,
764}
765
766impl RepairContractError {
767    const fn code(self) -> &'static str {
768        match self {
769            Self::InvalidDigest => "invalid_repair_digest",
770            Self::InvalidManifestId => "invalid_repair_manifest_id",
771            Self::InvalidSource => "invalid_repair_source",
772            Self::InvalidTarget => "invalid_repair_target",
773            Self::InvalidExpectedState => "invalid_repair_expected_state",
774            Self::UnsupportedWriter => "unsupported_repair_writer",
775            Self::InvalidMutation => "invalid_repair_mutation",
776            Self::InvalidAllowedEffects => "invalid_repair_allowed_effects",
777            Self::InvalidRollbackArtifact => "invalid_repair_rollback_artifact",
778            Self::InvalidPostAssertions => "invalid_repair_post_assertions",
779            Self::UnsupportedManifestSchema => "unsupported_repair_manifest_schema",
780            Self::InvalidManifest => "invalid_repair_manifest",
781            Self::InvalidPrepareRequest => "invalid_prepare_repair_request",
782            Self::InvalidApplyRequest => "invalid_apply_repair_request",
783            Self::InvalidVerifyRequest => "invalid_verify_repair_request",
784            Self::InvalidReceipt => "invalid_repair_receipt",
785        }
786    }
787}
788
789impl fmt::Display for RepairContractError {
790    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791        f.write_str(self.code())
792    }
793}
794
795impl std::error::Error for RepairContractError {}
796
797fn frozen_repair_digest_v1(
798    digest: frozen_v1::FrozenRepairDigestV1,
799) -> Result<RepairDigest, RepairContractError> {
800    RepairDigest::parse(&digest.0)
801}
802
803fn frozen_lint_digest_v1(
804    digest: frozen_v1::FrozenLintDigestV1,
805) -> Result<LintDigest, RepairContractError> {
806    LintDigest::from_hex(&digest.0).map_err(|_| RepairContractError::InvalidSource)
807}
808
809fn frozen_lint_opaque_id_v1(
810    id: frozen_v1::FrozenLintOpaqueIdV1,
811) -> Result<LintOpaqueId, RepairContractError> {
812    let position =
813        id.0.checked_sub(1)
814            .and_then(|value| usize::try_from(value).ok())
815            .and_then(LintOpaqueId::from_sorted_position)
816            .ok_or(RepairContractError::InvalidSource)?;
817    Ok(position)
818}
819
820fn frozen_lint_scope_v1(
821    scope: frozen_v1::FrozenLintScopeV1,
822) -> Result<LintScope, RepairContractError> {
823    match (scope.kind, scope.opaque_scope_ref) {
824        (frozen_v1::FrozenLintScopeKindV1::Global, None) => Ok(LintScope::global()),
825        (frozen_v1::FrozenLintScopeKindV1::Registered, Some(reference)) => {
826            Ok(LintScope::registered(frozen_lint_opaque_id_v1(reference)?))
827        }
828        (frozen_v1::FrozenLintScopeKindV1::Uncategorized, None) => Ok(LintScope::uncategorized()),
829        _ => Err(RepairContractError::InvalidSource),
830    }
831}
832
833fn frozen_lint_snapshots_v1(
834    snapshots: frozen_v1::FrozenLintSnapshotReceiptsV1,
835) -> Result<LintSnapshotReceipts, RepairContractError> {
836    let db = snapshots.db;
837    let db_mode = match db.mode {
838        frozen_v1::FrozenLintDbSnapshotModeV1::TransactionalReadOnly => {
839            LintDbSnapshotMode::TransactionalReadOnly
840        }
841    };
842    let pages = snapshots.pages;
843    let page_mode = match pages.mode {
844        frozen_v1::FrozenLintPageSnapshotModeV1::BestEffort => LintPageSnapshotMode::BestEffort,
845    };
846    Ok(LintSnapshotReceipts::new(
847        LintDbSnapshotReceipt::new(
848            db_mode,
849            frozen_lint_digest_v1(db.analysis_digest)?,
850            db.post_run_digest.map(frozen_lint_digest_v1).transpose()?,
851        ),
852        LintPageSnapshotReceipt::new(
853            page_mode,
854            frozen_lint_digest_v1(pages.before_scan_digest)?,
855            pages
856                .after_scan_digest
857                .map(frozen_lint_digest_v1)
858                .transpose()?,
859        ),
860    ))
861}
862
863fn frozen_lint_producer_receipt_v1(
864    receipt: frozen_v1::FrozenLintProducerReceiptV1,
865) -> Result<LintProducerReceipt, RepairContractError> {
866    let commit = receipt
867        .runtime_commit
868        .map(|commit| {
869            LintCommitReceipt::new(&commit.0).map_err(|_| RepairContractError::InvalidSource)
870        })
871        .transpose()?;
872    Ok(LintProducerReceipt::new(commit))
873}
874
875fn frozen_semantic_action_v1(action: frozen_v1::FrozenLintSemanticActionV1) -> LintSemanticAction {
876    match action {
877        frozen_v1::FrozenLintSemanticActionV1::ReclassifyMemory => {
878            LintSemanticAction::ReclassifyMemory
879        }
880        frozen_v1::FrozenLintSemanticActionV1::ReviewContradiction => {
881            LintSemanticAction::ReviewContradiction
882        }
883        frozen_v1::FrozenLintSemanticActionV1::ReviewStaleness => {
884            LintSemanticAction::ReviewStaleness
885        }
886        frozen_v1::FrozenLintSemanticActionV1::SupersedeMemory => {
887            LintSemanticAction::SupersedeMemory
888        }
889        frozen_v1::FrozenLintSemanticActionV1::AddMemoryEntityLink => {
890            LintSemanticAction::AddMemoryEntityLink
891        }
892        frozen_v1::FrozenLintSemanticActionV1::RemoveMemoryEntityLink => {
893            LintSemanticAction::RemoveMemoryEntityLink
894        }
895        frozen_v1::FrozenLintSemanticActionV1::AddEntityRelation => {
896            LintSemanticAction::AddEntityRelation
897        }
898        frozen_v1::FrozenLintSemanticActionV1::RemoveEntityRelation => {
899            LintSemanticAction::RemoveEntityRelation
900        }
901        frozen_v1::FrozenLintSemanticActionV1::ReviewPageClaim => {
902            LintSemanticAction::ReviewPageClaim
903        }
904        frozen_v1::FrozenLintSemanticActionV1::AddPageEvidence => {
905            LintSemanticAction::AddPageEvidence
906        }
907        frozen_v1::FrozenLintSemanticActionV1::RemovePageEvidence => {
908            LintSemanticAction::RemovePageEvidence
909        }
910        frozen_v1::FrozenLintSemanticActionV1::ReviewRetrieval => {
911            LintSemanticAction::ReviewRetrieval
912        }
913    }
914}
915
916fn frozen_semantic_reason_v1(
917    reason: frozen_v1::FrozenLintSemanticReasonCodeV1,
918) -> LintSemanticReasonCode {
919    match reason {
920        frozen_v1::FrozenLintSemanticReasonCodeV1::ClassificationMismatch => {
921            LintSemanticReasonCode::ClassificationMismatch
922        }
923        frozen_v1::FrozenLintSemanticReasonCodeV1::PotentialContradiction => {
924            LintSemanticReasonCode::PotentialContradiction
925        }
926        frozen_v1::FrozenLintSemanticReasonCodeV1::PotentialStaleness => {
927            LintSemanticReasonCode::PotentialStaleness
928        }
929        frozen_v1::FrozenLintSemanticReasonCodeV1::MentionWithoutLink => {
930            LintSemanticReasonCode::MentionWithoutLink
931        }
932        frozen_v1::FrozenLintSemanticReasonCodeV1::ExistingLinkMismatch => {
933            LintSemanticReasonCode::ExistingLinkMismatch
934        }
935        frozen_v1::FrozenLintSemanticReasonCodeV1::SharedContextWithoutRelation => {
936            LintSemanticReasonCode::SharedContextWithoutRelation
937        }
938        frozen_v1::FrozenLintSemanticReasonCodeV1::ExistingRelationMismatch => {
939            LintSemanticReasonCode::ExistingRelationMismatch
940        }
941        frozen_v1::FrozenLintSemanticReasonCodeV1::PotentialUnfaithfulClaim => {
942            LintSemanticReasonCode::PotentialUnfaithfulClaim
943        }
944        frozen_v1::FrozenLintSemanticReasonCodeV1::PotentialInadequateProvenance => {
945            LintSemanticReasonCode::PotentialInadequateProvenance
946        }
947        frozen_v1::FrozenLintSemanticReasonCodeV1::ClaimOverlapWithoutEvidence => {
948            LintSemanticReasonCode::ClaimOverlapWithoutEvidence
949        }
950        frozen_v1::FrozenLintSemanticReasonCodeV1::ExistingEvidenceMismatch => {
951            LintSemanticReasonCode::ExistingEvidenceMismatch
952        }
953        frozen_v1::FrozenLintSemanticReasonCodeV1::PotentialRetrievalMiss => {
954            LintSemanticReasonCode::PotentialRetrievalMiss
955        }
956        frozen_v1::FrozenLintSemanticReasonCodeV1::DanglingOwner => {
957            LintSemanticReasonCode::DanglingOwner
958        }
959        frozen_v1::FrozenLintSemanticReasonCodeV1::TemporalEvolution => {
960            LintSemanticReasonCode::TemporalEvolution
961        }
962        frozen_v1::FrozenLintSemanticReasonCodeV1::RelatedButNotEvidence => {
963            LintSemanticReasonCode::RelatedButNotEvidence
964        }
965    }
966}
967
968fn frozen_semantic_provider_v1(
969    provider: frozen_v1::FrozenLintSemanticProviderRouteV1,
970) -> LintSemanticProviderRoute {
971    match provider {
972        frozen_v1::FrozenLintSemanticProviderRouteV1::OnDevice => {
973            LintSemanticProviderRoute::OnDevice
974        }
975        frozen_v1::FrozenLintSemanticProviderRouteV1::ConfiguredExternal => {
976            LintSemanticProviderRoute::ConfiguredExternal
977        }
978        frozen_v1::FrozenLintSemanticProviderRouteV1::CallingAgent => {
979            LintSemanticProviderRoute::CallingAgent
980        }
981    }
982}
983
984fn frozen_semantic_finding_v1(
985    finding: frozen_v1::FrozenLintSemanticFindingV1,
986) -> Result<LintSemanticFinding, RepairContractError> {
987    LintSemanticFinding::try_new_with_disagreement(
988        frozen_lint_opaque_id_v1(finding.candidate_id)?,
989        frozen_semantic_action_v1(finding.proposed_action),
990        frozen_semantic_reason_v1(finding.reason_code),
991        finding.confidence_basis_points,
992        frozen_semantic_provider_v1(finding.provider_route),
993        finding
994            .evidence_ids
995            .into_iter()
996            .map(frozen_lint_digest_v1)
997            .collect::<Result<Vec<_>, _>>()?,
998        finding
999            .counterevidence_ids
1000            .into_iter()
1001            .map(frozen_lint_digest_v1)
1002            .collect::<Result<Vec<_>, _>>()?,
1003        finding.unresolved_disagreement,
1004    )
1005    .map_err(|_| RepairContractError::InvalidSource)
1006}
1007
1008fn frozen_lint_outcome_v1(outcome: frozen_v1::FrozenLintOutcomeV1) -> LintOutcome {
1009    match outcome {
1010        frozen_v1::FrozenLintOutcomeV1::Pass => LintOutcome::Pass,
1011        frozen_v1::FrozenLintOutcomeV1::Finding => LintOutcome::Finding,
1012        frozen_v1::FrozenLintOutcomeV1::NotRunPrerequisite => LintOutcome::NotRunPrerequisite,
1013        frozen_v1::FrozenLintOutcomeV1::InconsistentSnapshot => LintOutcome::InconsistentSnapshot,
1014        frozen_v1::FrozenLintOutcomeV1::FailedToRun => LintOutcome::FailedToRun,
1015    }
1016}
1017
1018fn frozen_lint_gate_effect_v1(effect: frozen_v1::FrozenLintGateEffectV1) -> LintGateEffect {
1019    match effect {
1020        frozen_v1::FrozenLintGateEffectV1::Actionable => LintGateEffect::Actionable,
1021        frozen_v1::FrozenLintGateEffectV1::Advisory => LintGateEffect::Advisory,
1022    }
1023}
1024
1025fn frozen_lint_reason_v1(reason: frozen_v1::FrozenLintReasonCodeV1) -> LintReasonCode {
1026    match reason {
1027        frozen_v1::FrozenLintReasonCodeV1::MissingArtifact => LintReasonCode::MissingArtifact,
1028        frozen_v1::FrozenLintReasonCodeV1::InvalidCatalogState => {
1029            LintReasonCode::InvalidCatalogState
1030        }
1031        frozen_v1::FrozenLintReasonCodeV1::ExpectedEmptySubstrate => {
1032            LintReasonCode::ExpectedEmptySubstrate
1033        }
1034        frozen_v1::FrozenLintReasonCodeV1::InvalidSourceConfiguration => {
1035            LintReasonCode::InvalidSourceConfiguration
1036        }
1037        frozen_v1::FrozenLintReasonCodeV1::TerminalOperationFailure => {
1038            LintReasonCode::TerminalOperationFailure
1039        }
1040        frozen_v1::FrozenLintReasonCodeV1::ExpiredRetry => LintReasonCode::ExpiredRetry,
1041        frozen_v1::FrozenLintReasonCodeV1::InvalidOperationState => {
1042            LintReasonCode::InvalidOperationState
1043        }
1044        frozen_v1::FrozenLintReasonCodeV1::DurableNoProgress => LintReasonCode::DurableNoProgress,
1045        frozen_v1::FrozenLintReasonCodeV1::SemanticProviderUnavailable => {
1046            LintReasonCode::SemanticProviderUnavailable
1047        }
1048        frozen_v1::FrozenLintReasonCodeV1::InsufficientSemanticEvidence => {
1049            LintReasonCode::InsufficientSemanticEvidence
1050        }
1051        frozen_v1::FrozenLintReasonCodeV1::SemanticExecutionFailure => {
1052            LintReasonCode::SemanticExecutionFailure
1053        }
1054        frozen_v1::FrozenLintReasonCodeV1::SemanticAgentAdjudicationRequired => {
1055            LintReasonCode::SemanticAgentAdjudicationRequired
1056        }
1057        frozen_v1::FrozenLintReasonCodeV1::SemanticAgentWorkStale => {
1058            LintReasonCode::SemanticAgentWorkStale
1059        }
1060        frozen_v1::FrozenLintReasonCodeV1::SemanticAgentSubmissionInvalid => {
1061            LintReasonCode::SemanticAgentSubmissionInvalid
1062        }
1063        frozen_v1::FrozenLintReasonCodeV1::SemanticCandidateGenerationFailure => {
1064            LintReasonCode::SemanticCandidateGenerationFailure
1065        }
1066        frozen_v1::FrozenLintReasonCodeV1::SemanticPopulationIncomplete => {
1067            LintReasonCode::SemanticPopulationIncomplete
1068        }
1069        frozen_v1::FrozenLintReasonCodeV1::SemanticDisagreementUnresolved => {
1070            LintReasonCode::SemanticDisagreementUnresolved
1071        }
1072        frozen_v1::FrozenLintReasonCodeV1::SemanticSecondJudgeRequired => {
1073            LintReasonCode::SemanticSecondJudgeRequired
1074        }
1075    }
1076}
1077
1078fn frozen_lint_safe_path_v1(
1079    path: frozen_v1::FrozenLintSafeRootRelativePathV1,
1080) -> LintSafeRootRelativePath {
1081    match path {
1082        frozen_v1::FrozenLintSafeRootRelativePathV1::PagesRoot => {
1083            LintSafeRootRelativePath::PagesRoot
1084        }
1085        frozen_v1::FrozenLintSafeRootRelativePathV1::PagesState => {
1086            LintSafeRootRelativePath::PagesState
1087        }
1088        frozen_v1::FrozenLintSafeRootRelativePathV1::PagesManifest => {
1089            LintSafeRootRelativePath::PagesManifest
1090        }
1091        frozen_v1::FrozenLintSafeRootRelativePathV1::PagesStubs => {
1092            LintSafeRootRelativePath::PagesStubs
1093        }
1094    }
1095}
1096
1097fn frozen_lint_evidence_v1(
1098    evidence: frozen_v1::FrozenLintEvidenceRefV1,
1099) -> Result<LintEvidenceRef, RepairContractError> {
1100    match evidence {
1101        frozen_v1::FrozenLintEvidenceRefV1::OpaqueId { opaque_id } => {
1102            Ok(LintEvidenceRef::OpaqueId {
1103                opaque_id: frozen_lint_opaque_id_v1(opaque_id)?,
1104            })
1105        }
1106        frozen_v1::FrozenLintEvidenceRefV1::ReasonCode { reason_code } => {
1107            Ok(LintEvidenceRef::ReasonCode {
1108                reason_code: frozen_lint_reason_v1(reason_code),
1109            })
1110        }
1111        frozen_v1::FrozenLintEvidenceRefV1::SafeRootRelativePath {
1112            safe_root_relative_path,
1113        } => Ok(LintEvidenceRef::SafeRootRelativePath {
1114            safe_root_relative_path: frozen_lint_safe_path_v1(safe_root_relative_path),
1115        }),
1116        frozen_v1::FrozenLintEvidenceRefV1::SemanticFinding { finding } => {
1117            Ok(LintEvidenceRef::SemanticFinding {
1118                finding: frozen_semantic_finding_v1(finding)?,
1119            })
1120        }
1121    }
1122}
1123
1124fn frozen_repair_lint_scope_v1(
1125    scope: frozen_v1::FrozenRepairLintScopeV1,
1126) -> Result<RepairLintScope, RepairContractError> {
1127    match scope {
1128        frozen_v1::FrozenRepairLintScopeV1::Global {} => Ok(RepairLintScope::global()),
1129        frozen_v1::FrozenRepairLintScopeV1::Registered { space } => {
1130            RepairLintScope::registered(space)
1131        }
1132        frozen_v1::FrozenRepairLintScopeV1::Uncategorized {} => {
1133            Ok(RepairLintScope::uncategorized())
1134        }
1135    }
1136}
1137
1138fn frozen_repair_scope_v1(
1139    scope: frozen_v1::FrozenRepairScopeV1,
1140) -> Result<RepairScope, RepairContractError> {
1141    match scope {
1142        frozen_v1::FrozenRepairScopeV1::Registered { space } => RepairScope::registered(space),
1143        frozen_v1::FrozenRepairScopeV1::Uncategorized {} => Ok(RepairScope::uncategorized()),
1144    }
1145}
1146
1147fn frozen_repair_target_v1(
1148    target: frozen_v1::FrozenRepairTargetV1,
1149) -> Result<RepairTarget, RepairContractError> {
1150    match target {
1151        frozen_v1::FrozenRepairTargetV1::Memory { source_id, scope } => {
1152            RepairTarget::memory(source_id, frozen_repair_scope_v1(scope)?)
1153        }
1154    }
1155}
1156
1157fn frozen_repair_source_v1(
1158    source: frozen_v1::FrozenRepairSourceV1,
1159) -> Result<RepairSource, RepairContractError> {
1160    if source.report_schema_version != frozen_v1::FROZEN_LINT_REPORT_SCHEMA_VERSION_V1
1161        || source.check_catalog_version != frozen_v1::FROZEN_LINT_CHECK_CATALOG_VERSION_V1
1162        || source.check_id != REPAIR_CLASSIFICATION_CHECK_ID
1163    {
1164        return Err(RepairContractError::InvalidSource);
1165    }
1166    RepairSource::try_new(
1167        frozen_repair_lint_scope_v1(source.lint_scope)?,
1168        frozen_lint_scope_v1(source.report_scope)?,
1169        frozen_semantic_finding_v1(source.finding)?,
1170        frozen_lint_snapshots_v1(source.general_snapshots)?,
1171        frozen_lint_snapshots_v1(source.deep_snapshots)?,
1172        frozen_lint_producer_receipt_v1(source.general_producer_receipt)?,
1173        frozen_lint_producer_receipt_v1(source.deep_producer_receipt)?,
1174        frozen_lint_digest_v1(source.agent_work_digest)?,
1175    )
1176}
1177
1178fn frozen_expected_state_v1(
1179    expected: frozen_v1::FrozenRepairExpectedStateV1,
1180) -> Result<RepairExpectedState, RepairContractError> {
1181    RepairExpectedState::try_new(
1182        expected.version,
1183        frozen_repair_digest_v1(expected.canonical_receipt)?,
1184    )
1185}
1186
1187fn frozen_writer_v1(writer: frozen_v1::FrozenRepairWriterV1) -> RepairWriter {
1188    match writer {
1189        frozen_v1::FrozenRepairWriterV1::ReclassifyMemory => RepairWriter::ReclassifyMemory,
1190    }
1191}
1192
1193fn frozen_memory_type_v1(memory_type: frozen_v1::FrozenMemoryTypeV1) -> MemoryType {
1194    match memory_type {
1195        frozen_v1::FrozenMemoryTypeV1::Identity => MemoryType::Identity,
1196        frozen_v1::FrozenMemoryTypeV1::Preference => MemoryType::Preference,
1197        frozen_v1::FrozenMemoryTypeV1::Decision => MemoryType::Decision,
1198        frozen_v1::FrozenMemoryTypeV1::Lesson => MemoryType::Lesson,
1199        frozen_v1::FrozenMemoryTypeV1::Gotcha => MemoryType::Gotcha,
1200        frozen_v1::FrozenMemoryTypeV1::Fact => MemoryType::Fact,
1201    }
1202}
1203
1204fn frozen_mutation_v1(
1205    mutation: frozen_v1::FrozenRepairMutationV1,
1206) -> Result<RepairMutation, RepairContractError> {
1207    match mutation {
1208        frozen_v1::FrozenRepairMutationV1::ReclassifyMemory {
1209            before_memory_type,
1210            after_memory_type,
1211        } => RepairMutation::from_memory_types(
1212            before_memory_type.map(frozen_memory_type_v1),
1213            frozen_memory_type_v1(after_memory_type),
1214        ),
1215    }
1216}
1217
1218fn frozen_allowed_effects_v1(
1219    effects: frozen_v1::FrozenRepairAllowedEffectsV1,
1220) -> Result<RepairAllowedEffects, RepairContractError> {
1221    if effects.fields != [frozen_v1::FrozenRepairMemoryFieldV1::MemoryType] {
1222        return Err(RepairContractError::InvalidAllowedEffects);
1223    }
1224    Ok(RepairAllowedEffects::memory_type(frozen_repair_target_v1(
1225        effects.owner,
1226    )?))
1227}
1228
1229fn frozen_rollback_reference_v1(
1230    rollback: frozen_v1::FrozenRepairRollbackReferenceV1,
1231) -> Result<RepairRollbackArtifact, RepairContractError> {
1232    if rollback.format_version != 1 {
1233        return Err(RepairContractError::InvalidRollbackArtifact);
1234    }
1235    RepairRollbackArtifact::try_new_for_format(
1236        1,
1237        rollback.relative_path,
1238        frozen_repair_digest_v1(rollback.digest)?,
1239    )
1240}
1241
1242fn frozen_check_baseline_v1(
1243    baseline: frozen_v1::FrozenRepairCheckBaselineV1,
1244) -> Result<RepairCheckBaseline, RepairContractError> {
1245    RepairCheckBaseline::try_new(
1246        baseline.check_id,
1247        frozen_lint_outcome_v1(baseline.outcome),
1248        frozen_lint_gate_effect_v1(baseline.gate_effect),
1249        baseline
1250            .evidence
1251            .into_iter()
1252            .map(frozen_lint_evidence_v1)
1253            .collect::<Result<Vec<_>, _>>()?,
1254    )
1255}
1256
1257fn frozen_post_assertions_v1(
1258    assertions: frozen_v1::FrozenRepairPostAssertionsV1,
1259) -> Result<RepairPostAssertions, RepairContractError> {
1260    if assertions.target_check_id != REPAIR_CLASSIFICATION_CHECK_ID
1261        || !assertions.require_complete_general
1262        || !assertions.require_complete_deep
1263        || !assertions.reject_new_actionable
1264        || !assertions.reject_new_incomplete
1265    {
1266        return Err(RepairContractError::InvalidPostAssertions);
1267    }
1268    RepairPostAssertions::try_new_legacy_v1(
1269        frozen_lint_digest_v1(assertions.target_evidence_id)?,
1270        assertions
1271            .general_baseline
1272            .into_iter()
1273            .map(frozen_check_baseline_v1)
1274            .collect::<Result<Vec<_>, _>>()?,
1275        assertions
1276            .deep_baseline
1277            .into_iter()
1278            .map(frozen_check_baseline_v1)
1279            .collect::<Result<Vec<_>, _>>()?,
1280        assertions.allowed_non_target_check_deltas,
1281    )
1282}
1283
1284fn frozen_post_assertions_pre_baseline_v1(
1285    assertions: frozen_v1::FrozenRepairPostAssertionsPreBaselineV1,
1286) -> Result<RepairPostAssertions, RepairContractError> {
1287    if assertions.target_check_id != REPAIR_CLASSIFICATION_CHECK_ID
1288        || !assertions.require_complete_general
1289        || !assertions.require_complete_deep
1290        || !assertions.reject_new_actionable
1291        || !assertions.reject_new_incomplete
1292        || assertions
1293            .allowed_non_target_check_deltas
1294            .iter()
1295            .any(|value| !valid_nonempty(value))
1296        || !assertions
1297            .allowed_non_target_check_deltas
1298            .windows(2)
1299            .all(|pair| pair[0] < pair[1])
1300    {
1301        return Err(RepairContractError::InvalidPostAssertions);
1302    }
1303    Ok(RepairPostAssertions {
1304        target_check_id: REPAIR_CLASSIFICATION_CHECK_ID.to_string(),
1305        target_evidence_id: frozen_lint_digest_v1(assertions.target_evidence_id)?,
1306        general_baseline: Vec::new(),
1307        deep_baseline: Vec::new(),
1308        target_record_set: None,
1309        verification_policy: RepairVerificationPolicy::LegacyWholeReports,
1310        require_complete_general: true,
1311        reject_new_actionable: true,
1312        reject_new_incomplete: true,
1313        allowed_non_target_check_deltas: assertions.allowed_non_target_check_deltas,
1314    })
1315}
1316
1317fn frozen_manifest_v1_into_current(
1318    manifest: FrozenRepairManifestV1,
1319) -> Result<RepairManifest, RepairContractError> {
1320    let frozen_v1::FrozenRepairManifestV1 {
1321        draft,
1322        manifest_digest,
1323    } = manifest;
1324    if draft.manifest_schema_version != 1 {
1325        return Err(RepairContractError::UnsupportedManifestSchema);
1326    }
1327    let draft = RepairManifestDraft::try_new(
1328        draft.manifest_id,
1329        draft.prepared_at,
1330        frozen_repair_source_v1(draft.source)?,
1331        frozen_repair_target_v1(draft.target)?,
1332        frozen_expected_state_v1(draft.expected_state)?,
1333        frozen_writer_v1(draft.writer),
1334        frozen_mutation_v1(draft.mutation)?,
1335        frozen_allowed_effects_v1(draft.allowed_effects)?,
1336        frozen_rollback_reference_v1(draft.rollback)?,
1337        frozen_post_assertions_v1(draft.post_assertions)?,
1338    )?;
1339    RepairManifest::try_new(draft, frozen_repair_digest_v1(manifest_digest)?)
1340}
1341
1342fn frozen_manifest_pre_baseline_v1_into_current(
1343    manifest: FrozenRepairManifestPreBaselineV1,
1344) -> Result<RepairManifest, RepairContractError> {
1345    let frozen_v1::FrozenRepairManifestPreBaselineV1 {
1346        draft,
1347        manifest_digest,
1348    } = manifest;
1349    if draft.manifest_schema_version != 1 {
1350        return Err(RepairContractError::UnsupportedManifestSchema);
1351    }
1352    let draft = RepairManifestDraft::try_new(
1353        draft.manifest_id,
1354        draft.prepared_at,
1355        frozen_repair_source_v1(draft.source)?,
1356        frozen_repair_target_v1(draft.target)?,
1357        frozen_expected_state_v1(draft.expected_state)?,
1358        frozen_writer_v1(draft.writer),
1359        frozen_mutation_v1(draft.mutation)?,
1360        frozen_allowed_effects_v1(draft.allowed_effects)?,
1361        frozen_rollback_reference_v1(draft.rollback)?,
1362        frozen_post_assertions_pre_baseline_v1(draft.post_assertions)?,
1363    )?;
1364    RepairManifest::try_new(draft, frozen_repair_digest_v1(manifest_digest)?)
1365}
1366
1367fn frozen_apply_receipt_v1_into_current(
1368    receipt: FrozenRepairApplyReceiptV1,
1369) -> Result<RepairApplyReceipt, RepairContractError> {
1370    let frozen_v1::FrozenRepairApplyReceiptV1 {
1371        draft,
1372        receipt_digest,
1373    } = receipt;
1374    if draft.receipt_schema_version != 1 {
1375        return Err(RepairContractError::InvalidReceipt);
1376    }
1377    let draft = RepairApplyReceiptDraft::try_new_legacy_v1(
1378        draft.manifest_id,
1379        frozen_repair_digest_v1(draft.manifest_digest)?,
1380        draft.applied_at,
1381        frozen_repair_digest_v1(draft.before_target_receipt)?,
1382        frozen_repair_digest_v1(draft.after_target_receipt)?,
1383        frozen_repair_digest_v1(draft.non_target_before)?,
1384        frozen_repair_digest_v1(draft.non_target_after)?,
1385        frozen_allowed_effects_v1(draft.actual_effects)?,
1386        frozen_writer_v1(draft.writer),
1387    )?;
1388    Ok(RepairApplyReceipt::from_draft(
1389        draft,
1390        frozen_repair_digest_v1(receipt_digest)?,
1391    ))
1392}
1393
1394fn frozen_verification_receipt_v1_into_current(
1395    receipt: FrozenRepairVerificationReceiptV1,
1396) -> Result<RepairVerificationReceipt, RepairContractError> {
1397    let frozen_v1::FrozenRepairVerificationReceiptV1 {
1398        draft,
1399        receipt_digest,
1400    } = receipt;
1401    if draft.receipt_schema_version != 1 {
1402        return Err(RepairContractError::InvalidReceipt);
1403    }
1404    let draft = RepairVerificationReceiptDraft::try_new_legacy_v1(
1405        draft.manifest_id,
1406        frozen_repair_digest_v1(draft.manifest_digest)?,
1407        frozen_repair_digest_v1(draft.apply_receipt_digest)?,
1408        draft.verified_at,
1409        frozen_lint_snapshots_v1(draft.general_snapshots)?,
1410        frozen_lint_snapshots_v1(draft.deep_snapshots)?,
1411    )?;
1412    Ok(RepairVerificationReceipt::from_draft(
1413        draft,
1414        frozen_repair_digest_v1(receipt_digest)?,
1415    ))
1416}
1417
1418fn is_lower_hex(value: &str, length: usize) -> bool {
1419    value.len() == length
1420        && value.bytes().all(|byte| {
1421            byte.is_ascii_digit() || (byte.is_ascii_lowercase() && byte.is_ascii_hexdigit())
1422        })
1423}
1424
1425fn valid_manifest_id(value: &str) -> bool {
1426    let Some(uuid) = value.strip_prefix("repair_") else {
1427        return false;
1428    };
1429    uuid.len() == 36
1430        && uuid.bytes().enumerate().all(|(index, byte)| match index {
1431            8 | 13 | 18 | 23 => byte == b'-',
1432            _ => byte.is_ascii_digit() || (byte.is_ascii_lowercase() && byte.is_ascii_hexdigit()),
1433        })
1434}
1435
1436fn valid_nonempty(value: &str) -> bool {
1437    !value.is_empty() && value.trim() == value
1438}
1439
1440fn encode_lower_hex(bytes: &[u8]) -> String {
1441    use std::fmt::Write as _;
1442
1443    let mut encoded = String::with_capacity(bytes.len() * 2);
1444    for byte in bytes {
1445        write!(&mut encoded, "{byte:02x}").expect("writing to String cannot fail");
1446    }
1447    encoded
1448}
1449
1450fn strictly_sorted_unique(values: &[String]) -> bool {
1451    !values.is_empty()
1452        && values.iter().all(|value| valid_nonempty(value))
1453        && values.windows(2).all(|pair| pair[0] < pair[1])
1454}
1455
1456fn strictly_sorted_unique_by<T>(values: &[T], key: impl Fn(&T) -> &str) -> bool {
1457    values.windows(2).all(|pair| key(&pair[0]) < key(&pair[1]))
1458}
1459
1460fn valid_sorted_ids(values: &[String]) -> bool {
1461    values.iter().all(|value| valid_nonempty(value))
1462        && values.windows(2).all(|pair| pair[0] < pair[1])
1463}
1464
1465fn valid_columns_and_rows(columns: &[String], rows: &[Vec<String>]) -> bool {
1466    valid_unique_nonempty(columns)
1467        && !rows.is_empty()
1468        && rows.iter().all(|row| row.len() == columns.len())
1469}
1470
1471fn valid_unique_nonempty(values: &[String]) -> bool {
1472    use std::collections::BTreeSet;
1473
1474    !values.is_empty()
1475        && values.iter().all(|value| valid_nonempty(value))
1476        && values.iter().collect::<BTreeSet<_>>().len() == values.len()
1477}
1478
1479fn lower_hex_nibble(byte: u8) -> Option<u8> {
1480    match byte {
1481        b'0'..=b'9' => Some(byte - b'0'),
1482        b'a'..=b'f' => Some(byte - b'a' + 10),
1483        _ => None,
1484    }
1485}
1486
1487fn valid_embedding_hex(value: &str) -> bool {
1488    const EMBEDDING_BYTES: usize = 768 * std::mem::size_of::<f32>();
1489    if !is_lower_hex(value, EMBEDDING_BYTES * 2) {
1490        return false;
1491    }
1492    value.as_bytes().chunks_exact(8).all(|chunk| {
1493        let mut bytes = [0_u8; 4];
1494        for (index, pair) in chunk.chunks_exact(2).enumerate() {
1495            let Some(high) = lower_hex_nibble(pair[0]) else {
1496                return false;
1497            };
1498            let Some(low) = lower_hex_nibble(pair[1]) else {
1499                return false;
1500            };
1501            bytes[index] = (high << 4) | low;
1502        }
1503        f32::from_le_bytes(bytes).is_finite()
1504    })
1505}
1506
1507fn valid_root_relative_path(value: &str) -> bool {
1508    let path = Path::new(value);
1509    valid_nonempty(value)
1510        && !path.is_absolute()
1511        && path
1512            .components()
1513            .all(|component| matches!(component, Component::Normal(_)))
1514}
1515
1516fn parse_memory_type(value: &str) -> Result<MemoryType, RepairContractError> {
1517    if !MemoryType::all_values().contains(&value) {
1518        return Err(RepairContractError::InvalidMutation);
1519    }
1520    value
1521        .parse()
1522        .map_err(|_| RepairContractError::InvalidMutation)
1523}
1524
1525#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1526#[serde(transparent)]
1527pub struct RepairDigest(String);
1528
1529impl RepairDigest {
1530    pub fn parse(value: &str) -> Result<Self, RepairContractError> {
1531        if is_lower_hex(value, 64) {
1532            Ok(Self(value.to_string()))
1533        } else {
1534            Err(RepairContractError::InvalidDigest)
1535        }
1536    }
1537
1538    pub fn as_str(&self) -> &str {
1539        &self.0
1540    }
1541}
1542
1543impl<'de> Deserialize<'de> for RepairDigest {
1544    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1545    where
1546        D: Deserializer<'de>,
1547    {
1548        Self::parse(&String::deserialize(deserializer)?).map_err(D::Error::custom)
1549    }
1550}
1551
1552#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1553#[serde(tag = "kind", rename_all = "snake_case")]
1554pub enum RepairLintScope {
1555    Global,
1556    Registered { space: String },
1557    Uncategorized,
1558}
1559
1560#[derive(Deserialize)]
1561#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1562enum RepairLintScopeWire {
1563    Global,
1564    Registered { space: String },
1565    Uncategorized,
1566}
1567
1568impl RepairLintScope {
1569    pub const fn global() -> Self {
1570        Self::Global
1571    }
1572
1573    pub fn registered(space: String) -> Result<Self, RepairContractError> {
1574        if valid_nonempty(&space) {
1575            Ok(Self::Registered { space })
1576        } else {
1577            Err(RepairContractError::InvalidSource)
1578        }
1579    }
1580
1581    pub const fn uncategorized() -> Self {
1582        Self::Uncategorized
1583    }
1584
1585    pub fn space(&self) -> Option<&str> {
1586        match self {
1587            Self::Registered { space } => Some(space),
1588            Self::Global | Self::Uncategorized => None,
1589        }
1590    }
1591
1592    pub fn matches_report_scope_kind(&self, report_scope: &LintScope) -> bool {
1593        matches!(
1594            (self, report_scope.kind()),
1595            (Self::Global, LintScopeKind::Global)
1596                | (Self::Registered { .. }, LintScopeKind::Registered)
1597                | (Self::Uncategorized, LintScopeKind::Uncategorized)
1598        )
1599    }
1600}
1601
1602impl<'de> Deserialize<'de> for RepairLintScope {
1603    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1604    where
1605        D: Deserializer<'de>,
1606    {
1607        match RepairLintScopeWire::deserialize(deserializer)? {
1608            RepairLintScopeWire::Global => Ok(Self::global()),
1609            RepairLintScopeWire::Registered { space } => Self::registered(space),
1610            RepairLintScopeWire::Uncategorized => Ok(Self::uncategorized()),
1611        }
1612        .map_err(D::Error::custom)
1613    }
1614}
1615
1616#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1617#[serde(tag = "kind", rename_all = "snake_case")]
1618pub enum RepairScope {
1619    Global,
1620    Registered { space: String },
1621    Uncategorized,
1622}
1623
1624#[derive(Deserialize)]
1625#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1626enum RepairScopeWire {
1627    Global,
1628    Registered { space: String },
1629    Uncategorized,
1630}
1631
1632impl RepairScope {
1633    pub const fn global() -> Self {
1634        Self::Global
1635    }
1636
1637    pub fn registered(space: String) -> Result<Self, RepairContractError> {
1638        if valid_nonempty(&space) {
1639            Ok(Self::Registered { space })
1640        } else {
1641            Err(RepairContractError::InvalidTarget)
1642        }
1643    }
1644
1645    pub const fn uncategorized() -> Self {
1646        Self::Uncategorized
1647    }
1648
1649    pub fn space(&self) -> Option<&str> {
1650        match self {
1651            Self::Registered { space } => Some(space),
1652            Self::Global | Self::Uncategorized => None,
1653        }
1654    }
1655}
1656
1657impl<'de> Deserialize<'de> for RepairScope {
1658    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1659    where
1660        D: Deserializer<'de>,
1661    {
1662        match RepairScopeWire::deserialize(deserializer)? {
1663            RepairScopeWire::Global => Ok(Self::global()),
1664            RepairScopeWire::Registered { space } => Self::registered(space),
1665            RepairScopeWire::Uncategorized => Ok(Self::uncategorized()),
1666        }
1667        .map_err(D::Error::custom)
1668    }
1669}
1670
1671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1672#[serde(rename_all = "snake_case")]
1673pub enum RepairEnrichmentStep {
1674    EntityExtract,
1675}
1676
1677#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1678#[serde(tag = "kind", rename_all = "snake_case")]
1679pub enum RepairTarget {
1680    Memory {
1681        source_id: String,
1682        scope: RepairScope,
1683    },
1684    MemoryEntityLink {
1685        memory_id: String,
1686        entity_id: String,
1687        scope: RepairScope,
1688    },
1689    MemoryEntityExtraction {
1690        memory_id: String,
1691        step: RepairEnrichmentStep,
1692        entity_ids: Vec<String>,
1693        scope: RepairScope,
1694    },
1695    Tag {
1696        source: String,
1697        source_id: String,
1698        tag: String,
1699        scope: RepairScope,
1700    },
1701    PageLink {
1702        source_page_id: String,
1703        label_key: String,
1704        scope: RepairScope,
1705    },
1706    Page {
1707        page_id: String,
1708        scope: RepairScope,
1709    },
1710    PageProjection {
1711        page_id: String,
1712        scope: RepairScope,
1713    },
1714}
1715
1716#[derive(Deserialize)]
1717#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1718enum RepairTargetWire {
1719    Memory {
1720        source_id: String,
1721        scope: RepairScope,
1722    },
1723    MemoryEntityLink {
1724        memory_id: String,
1725        entity_id: String,
1726        scope: RepairScope,
1727    },
1728    MemoryEntityExtraction {
1729        memory_id: String,
1730        step: RepairEnrichmentStep,
1731        entity_ids: Vec<String>,
1732        scope: RepairScope,
1733    },
1734    Tag {
1735        source: String,
1736        source_id: String,
1737        tag: String,
1738        scope: RepairScope,
1739    },
1740    PageLink {
1741        source_page_id: String,
1742        label_key: String,
1743        scope: RepairScope,
1744    },
1745    Page {
1746        page_id: String,
1747        scope: RepairScope,
1748    },
1749    PageProjection {
1750        page_id: String,
1751        scope: RepairScope,
1752    },
1753}
1754
1755impl RepairTarget {
1756    pub fn memory(source_id: String, scope: RepairScope) -> Result<Self, RepairContractError> {
1757        if valid_nonempty(&source_id) {
1758            Ok(Self::Memory { source_id, scope })
1759        } else {
1760            Err(RepairContractError::InvalidTarget)
1761        }
1762    }
1763
1764    pub fn memory_entity_link(
1765        memory_id: String,
1766        entity_id: String,
1767        scope: RepairScope,
1768    ) -> Result<Self, RepairContractError> {
1769        if !valid_nonempty(&memory_id) || !valid_nonempty(&entity_id) {
1770            return Err(RepairContractError::InvalidTarget);
1771        }
1772        Ok(Self::MemoryEntityLink {
1773            memory_id,
1774            entity_id,
1775            scope,
1776        })
1777    }
1778
1779    pub fn memory_entity_extraction(
1780        memory_id: String,
1781        step: RepairEnrichmentStep,
1782        entity_ids: Vec<String>,
1783        scope: RepairScope,
1784    ) -> Result<Self, RepairContractError> {
1785        if !valid_nonempty(&memory_id) || !strictly_sorted_unique(&entity_ids) {
1786            return Err(RepairContractError::InvalidTarget);
1787        }
1788        Ok(Self::MemoryEntityExtraction {
1789            memory_id,
1790            step,
1791            entity_ids,
1792            scope,
1793        })
1794    }
1795
1796    pub fn tag(
1797        source: String,
1798        source_id: String,
1799        tag: String,
1800    ) -> Result<Self, RepairContractError> {
1801        if !valid_nonempty(&source) || !valid_nonempty(&source_id) {
1802            return Err(RepairContractError::InvalidTarget);
1803        }
1804        Ok(Self::Tag {
1805            source,
1806            source_id,
1807            tag,
1808            scope: RepairScope::global(),
1809        })
1810    }
1811
1812    pub fn page_link(
1813        source_page_id: String,
1814        label_key: String,
1815        scope: RepairScope,
1816    ) -> Result<Self, RepairContractError> {
1817        if !valid_nonempty(&source_page_id) || !valid_nonempty(&label_key) {
1818            return Err(RepairContractError::InvalidTarget);
1819        }
1820        Ok(Self::PageLink {
1821            source_page_id,
1822            label_key,
1823            scope,
1824        })
1825    }
1826
1827    pub fn page_projection(
1828        page_id: String,
1829        scope: RepairScope,
1830    ) -> Result<Self, RepairContractError> {
1831        if !valid_nonempty(&page_id) {
1832            return Err(RepairContractError::InvalidTarget);
1833        }
1834        Ok(Self::PageProjection { page_id, scope })
1835    }
1836
1837    pub fn page(page_id: String, scope: RepairScope) -> Result<Self, RepairContractError> {
1838        if !valid_nonempty(&page_id) {
1839            return Err(RepairContractError::InvalidTarget);
1840        }
1841        Ok(Self::Page { page_id, scope })
1842    }
1843
1844    pub fn memory_source_id(&self) -> &str {
1845        match self {
1846            Self::Memory { source_id, .. } => source_id,
1847            Self::MemoryEntityLink { .. }
1848            | Self::MemoryEntityExtraction { .. }
1849            | Self::Tag { .. }
1850            | Self::PageLink { .. }
1851            | Self::Page { .. }
1852            | Self::PageProjection { .. } => {
1853                panic!("repair target is not a memory")
1854            }
1855        }
1856    }
1857
1858    pub fn scope(&self) -> &RepairScope {
1859        match self {
1860            Self::Memory { scope, .. }
1861            | Self::MemoryEntityLink { scope, .. }
1862            | Self::MemoryEntityExtraction { scope, .. }
1863            | Self::Tag { scope, .. }
1864            | Self::PageLink { scope, .. }
1865            | Self::Page { scope, .. }
1866            | Self::PageProjection { scope, .. } => scope,
1867        }
1868    }
1869
1870    fn review_owner_ids(&self) -> Option<Vec<String>> {
1871        match self {
1872            Self::Memory { source_id, .. } => Some(vec![source_id.clone()]),
1873            Self::PageProjection { page_id, .. } => Some(vec![page_id.clone()]),
1874            Self::MemoryEntityExtraction { memory_id, .. } => Some(vec![memory_id.clone()]),
1875            _ => None,
1876        }
1877    }
1878}
1879
1880impl<'de> Deserialize<'de> for RepairTarget {
1881    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1882    where
1883        D: Deserializer<'de>,
1884    {
1885        match RepairTargetWire::deserialize(deserializer)? {
1886            RepairTargetWire::Memory { source_id, scope } => Self::memory(source_id, scope),
1887            RepairTargetWire::MemoryEntityLink {
1888                memory_id,
1889                entity_id,
1890                scope,
1891            } => Self::memory_entity_link(memory_id, entity_id, scope),
1892            RepairTargetWire::MemoryEntityExtraction {
1893                memory_id,
1894                step,
1895                entity_ids,
1896                scope,
1897            } => Self::memory_entity_extraction(memory_id, step, entity_ids, scope),
1898            RepairTargetWire::Tag {
1899                source,
1900                source_id,
1901                tag,
1902                scope,
1903            } if scope == RepairScope::global() => Self::tag(source, source_id, tag),
1904            RepairTargetWire::Tag { .. } => Err(RepairContractError::InvalidTarget),
1905            RepairTargetWire::PageLink {
1906                source_page_id,
1907                label_key,
1908                scope,
1909            } => Self::page_link(source_page_id, label_key, scope),
1910            RepairTargetWire::Page { page_id, scope } => Self::page(page_id, scope),
1911            RepairTargetWire::PageProjection { page_id, scope } => {
1912                Self::page_projection(page_id, scope)
1913            }
1914        }
1915        .map_err(D::Error::custom)
1916    }
1917}
1918
1919#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1920pub struct RepairReviewBinding {
1921    review_id: String,
1922    occurrence_digest: RepairDigest,
1923    owner_ids: Vec<String>,
1924}
1925
1926#[derive(Deserialize)]
1927#[serde(deny_unknown_fields)]
1928struct RepairReviewBindingWire {
1929    review_id: String,
1930    occurrence_digest: RepairDigest,
1931    owner_ids: Vec<String>,
1932}
1933
1934impl RepairReviewBinding {
1935    pub fn try_new(
1936        review_id: String,
1937        occurrence_digest: RepairDigest,
1938        owner_ids: Vec<String>,
1939    ) -> Result<Self, RepairContractError> {
1940        if !valid_nonempty(&review_id) || !strictly_sorted_unique(&owner_ids) {
1941            return Err(RepairContractError::InvalidSource);
1942        }
1943        Ok(Self {
1944            review_id,
1945            occurrence_digest,
1946            owner_ids,
1947        })
1948    }
1949
1950    pub fn review_id(&self) -> &str {
1951        &self.review_id
1952    }
1953
1954    pub fn occurrence_digest(&self) -> &RepairDigest {
1955        &self.occurrence_digest
1956    }
1957
1958    pub fn owner_ids(&self) -> &[String] {
1959        &self.owner_ids
1960    }
1961}
1962
1963impl<'de> Deserialize<'de> for RepairReviewBinding {
1964    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1965    where
1966        D: Deserializer<'de>,
1967    {
1968        let wire = RepairReviewBindingWire::deserialize(deserializer)?;
1969        Self::try_new(wire.review_id, wire.occurrence_digest, wire.owner_ids)
1970            .map_err(D::Error::custom)
1971    }
1972}
1973
1974#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1975pub struct RepairSource {
1976    report_schema_version: u16,
1977    check_catalog_version: u16,
1978    lint_scope: RepairLintScope,
1979    report_scope: LintScope,
1980    check_id: String,
1981    #[serde(skip_serializing_if = "Option::is_none")]
1982    finding: Option<LintSemanticFinding>,
1983    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1984    deterministic_evidence: Vec<LintEvidenceRef>,
1985    general_snapshots: LintSnapshotReceipts,
1986    #[serde(skip_serializing_if = "Option::is_none")]
1987    deep_snapshots: Option<LintSnapshotReceipts>,
1988    general_producer_receipt: LintProducerReceipt,
1989    #[serde(skip_serializing_if = "Option::is_none")]
1990    deep_producer_receipt: Option<LintProducerReceipt>,
1991    #[serde(skip_serializing_if = "Option::is_none")]
1992    agent_work_digest: Option<LintDigest>,
1993    #[serde(skip_serializing_if = "Option::is_none")]
1994    review_binding: Option<RepairReviewBinding>,
1995}
1996
1997#[derive(Deserialize)]
1998#[serde(deny_unknown_fields)]
1999struct RepairSourceWire {
2000    report_schema_version: u16,
2001    check_catalog_version: u16,
2002    lint_scope: RepairLintScope,
2003    report_scope: LintScope,
2004    check_id: String,
2005    #[serde(default)]
2006    finding: Option<LintSemanticFinding>,
2007    #[serde(default)]
2008    deterministic_evidence: Vec<LintEvidenceRef>,
2009    general_snapshots: LintSnapshotReceipts,
2010    #[serde(default)]
2011    deep_snapshots: Option<LintSnapshotReceipts>,
2012    general_producer_receipt: LintProducerReceipt,
2013    #[serde(default)]
2014    deep_producer_receipt: Option<LintProducerReceipt>,
2015    #[serde(default)]
2016    agent_work_digest: Option<LintDigest>,
2017    #[serde(default)]
2018    review_binding: Option<RepairReviewBinding>,
2019}
2020
2021impl RepairSource {
2022    #[allow(clippy::too_many_arguments)]
2023    pub fn try_new(
2024        lint_scope: RepairLintScope,
2025        report_scope: LintScope,
2026        finding: LintSemanticFinding,
2027        general_snapshots: LintSnapshotReceipts,
2028        deep_snapshots: LintSnapshotReceipts,
2029        general_producer_receipt: LintProducerReceipt,
2030        deep_producer_receipt: LintProducerReceipt,
2031        agent_work_digest: LintDigest,
2032    ) -> Result<Self, RepairContractError> {
2033        if !lint_scope.matches_report_scope_kind(&report_scope)
2034            || finding.proposed_action() != LintSemanticAction::ReclassifyMemory
2035            || finding.unresolved_disagreement()
2036            || finding.evidence_ids().is_empty()
2037        {
2038            return Err(RepairContractError::InvalidSource);
2039        }
2040        Ok(Self {
2041            report_schema_version: LINT_REPORT_SCHEMA_VERSION,
2042            check_catalog_version: LINT_CHECK_CATALOG_VERSION,
2043            lint_scope,
2044            report_scope,
2045            check_id: REPAIR_CLASSIFICATION_CHECK_ID.to_string(),
2046            finding: Some(finding),
2047            deterministic_evidence: vec![],
2048            general_snapshots,
2049            deep_snapshots: Some(deep_snapshots),
2050            general_producer_receipt,
2051            deep_producer_receipt: Some(deep_producer_receipt),
2052            agent_work_digest: Some(agent_work_digest),
2053            review_binding: None,
2054        })
2055    }
2056
2057    #[allow(clippy::too_many_arguments)]
2058    pub fn try_new_deterministic(
2059        lint_scope: RepairLintScope,
2060        report_scope: LintScope,
2061        check_id: String,
2062        deterministic_evidence: Vec<LintEvidenceRef>,
2063        general_snapshots: LintSnapshotReceipts,
2064        deep_snapshots: LintSnapshotReceipts,
2065        general_producer_receipt: LintProducerReceipt,
2066        deep_producer_receipt: LintProducerReceipt,
2067    ) -> Result<Self, RepairContractError> {
2068        if !lint_scope.matches_report_scope_kind(&report_scope)
2069            || !valid_nonempty(&check_id)
2070            || check_id == REPAIR_CLASSIFICATION_CHECK_ID
2071        {
2072            return Err(RepairContractError::InvalidSource);
2073        }
2074        Ok(Self {
2075            report_schema_version: LINT_REPORT_SCHEMA_VERSION,
2076            check_catalog_version: LINT_CHECK_CATALOG_VERSION,
2077            lint_scope,
2078            report_scope,
2079            check_id,
2080            finding: None,
2081            deterministic_evidence,
2082            general_snapshots,
2083            deep_snapshots: Some(deep_snapshots),
2084            general_producer_receipt,
2085            deep_producer_receipt: Some(deep_producer_receipt),
2086            agent_work_digest: None,
2087            review_binding: None,
2088        })
2089    }
2090
2091    #[allow(clippy::too_many_arguments)]
2092    pub fn try_new_general_only_deterministic(
2093        lint_scope: RepairLintScope,
2094        report_scope: LintScope,
2095        check_id: String,
2096        deterministic_evidence: Vec<LintEvidenceRef>,
2097        general_snapshots: LintSnapshotReceipts,
2098        general_producer_receipt: LintProducerReceipt,
2099    ) -> Result<Self, RepairContractError> {
2100        if !lint_scope.matches_report_scope_kind(&report_scope)
2101            || !valid_nonempty(&check_id)
2102            || check_id == REPAIR_CLASSIFICATION_CHECK_ID
2103        {
2104            return Err(RepairContractError::InvalidSource);
2105        }
2106        Ok(Self {
2107            report_schema_version: LINT_REPORT_SCHEMA_VERSION,
2108            check_catalog_version: LINT_CHECK_CATALOG_VERSION,
2109            lint_scope,
2110            report_scope,
2111            check_id,
2112            finding: None,
2113            deterministic_evidence,
2114            general_snapshots,
2115            deep_snapshots: None,
2116            general_producer_receipt,
2117            deep_producer_receipt: None,
2118            agent_work_digest: None,
2119            review_binding: None,
2120        })
2121    }
2122
2123    pub fn lint_scope(&self) -> &RepairLintScope {
2124        &self.lint_scope
2125    }
2126
2127    pub fn report_scope(&self) -> &LintScope {
2128        &self.report_scope
2129    }
2130
2131    pub fn finding(&self) -> Option<&LintSemanticFinding> {
2132        self.finding.as_ref()
2133    }
2134
2135    pub fn deterministic_evidence(&self) -> &[LintEvidenceRef] {
2136        &self.deterministic_evidence
2137    }
2138
2139    pub fn check_id(&self) -> &str {
2140        &self.check_id
2141    }
2142
2143    pub fn general_snapshots(&self) -> &LintSnapshotReceipts {
2144        &self.general_snapshots
2145    }
2146
2147    pub fn deep_snapshots(&self) -> Option<&LintSnapshotReceipts> {
2148        self.deep_snapshots.as_ref()
2149    }
2150
2151    pub fn general_producer_receipt(&self) -> &LintProducerReceipt {
2152        &self.general_producer_receipt
2153    }
2154
2155    pub fn deep_producer_receipt(&self) -> Option<&LintProducerReceipt> {
2156        self.deep_producer_receipt.as_ref()
2157    }
2158
2159    pub fn agent_work_digest(&self) -> Option<&LintDigest> {
2160        self.agent_work_digest.as_ref()
2161    }
2162
2163    pub fn try_with_review_binding(
2164        mut self,
2165        review_binding: RepairReviewBinding,
2166    ) -> Result<Self, RepairContractError> {
2167        if self.review_binding.is_some() {
2168            return Err(RepairContractError::InvalidSource);
2169        }
2170        self.review_binding = Some(review_binding);
2171        Ok(self)
2172    }
2173
2174    pub const fn review_binding(&self) -> Option<&RepairReviewBinding> {
2175        self.review_binding.as_ref()
2176    }
2177
2178    pub const fn is_general_only_deterministic(&self) -> bool {
2179        self.finding.is_none()
2180            && self.agent_work_digest.is_none()
2181            && self.deep_snapshots.is_none()
2182            && self.deep_producer_receipt.is_none()
2183    }
2184}
2185
2186impl<'de> Deserialize<'de> for RepairSource {
2187    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2188    where
2189        D: Deserializer<'de>,
2190    {
2191        let wire = RepairSourceWire::deserialize(deserializer)?;
2192        let report_schema_version = wire.report_schema_version;
2193        let check_catalog_version = wire.check_catalog_version;
2194        let review_binding = wire.review_binding;
2195        if (report_schema_version != PREVIOUS_LINT_REPORT_SCHEMA_VERSION
2196            && report_schema_version != LINT_REPORT_SCHEMA_VERSION)
2197            || (check_catalog_version != PREVIOUS_LINT_CHECK_CATALOG_VERSION
2198                && check_catalog_version != LINT_CHECK_CATALOG_VERSION)
2199            || wire.deep_snapshots.is_some() != wire.deep_producer_receipt.is_some()
2200        {
2201            return Err(D::Error::custom(RepairContractError::InvalidSource));
2202        }
2203        let mut source = if wire.check_id == REPAIR_CLASSIFICATION_CHECK_ID {
2204            if !wire.deterministic_evidence.is_empty()
2205                || wire.deep_snapshots.is_none()
2206                || wire.deep_producer_receipt.is_none()
2207            {
2208                return Err(D::Error::custom(RepairContractError::InvalidSource));
2209            }
2210            Self::try_new(
2211                wire.lint_scope,
2212                wire.report_scope,
2213                wire.finding
2214                    .ok_or_else(|| D::Error::custom(RepairContractError::InvalidSource))?,
2215                wire.general_snapshots,
2216                wire.deep_snapshots
2217                    .ok_or_else(|| D::Error::custom(RepairContractError::InvalidSource))?,
2218                wire.general_producer_receipt,
2219                wire.deep_producer_receipt
2220                    .ok_or_else(|| D::Error::custom(RepairContractError::InvalidSource))?,
2221                wire.agent_work_digest
2222                    .ok_or_else(|| D::Error::custom(RepairContractError::InvalidSource))?,
2223            )
2224        } else {
2225            if wire.finding.is_some() || wire.agent_work_digest.is_some() {
2226                return Err(D::Error::custom(RepairContractError::InvalidSource));
2227            }
2228            match (wire.deep_snapshots, wire.deep_producer_receipt) {
2229                (Some(deep_snapshots), Some(deep_producer_receipt)) => Self::try_new_deterministic(
2230                    wire.lint_scope,
2231                    wire.report_scope,
2232                    wire.check_id,
2233                    wire.deterministic_evidence,
2234                    wire.general_snapshots,
2235                    deep_snapshots,
2236                    wire.general_producer_receipt,
2237                    deep_producer_receipt,
2238                ),
2239                (None, None) => Self::try_new_general_only_deterministic(
2240                    wire.lint_scope,
2241                    wire.report_scope,
2242                    wire.check_id,
2243                    wire.deterministic_evidence,
2244                    wire.general_snapshots,
2245                    wire.general_producer_receipt,
2246                ),
2247                _ => Err(RepairContractError::InvalidSource),
2248            }
2249        }
2250        .map_err(D::Error::custom)?;
2251        if let Some(review_binding) = review_binding {
2252            source = source
2253                .try_with_review_binding(review_binding)
2254                .map_err(D::Error::custom)?;
2255        }
2256        source.report_schema_version = report_schema_version;
2257        source.check_catalog_version = check_catalog_version;
2258        Ok(source)
2259    }
2260}
2261
2262#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2263pub struct RepairExpectedState {
2264    #[serde(skip_serializing_if = "Option::is_none")]
2265    version: Option<i64>,
2266    canonical_receipt: RepairDigest,
2267}
2268
2269#[derive(Deserialize)]
2270#[serde(deny_unknown_fields)]
2271struct RepairExpectedStateWire {
2272    version: Option<i64>,
2273    canonical_receipt: RepairDigest,
2274}
2275
2276impl RepairExpectedState {
2277    pub fn try_new(
2278        version: Option<i64>,
2279        canonical_receipt: RepairDigest,
2280    ) -> Result<Self, RepairContractError> {
2281        if version.is_some_and(|version| version < 0) {
2282            return Err(RepairContractError::InvalidExpectedState);
2283        }
2284        Ok(Self {
2285            version,
2286            canonical_receipt,
2287        })
2288    }
2289
2290    pub const fn version(&self) -> Option<i64> {
2291        self.version
2292    }
2293
2294    pub fn canonical_receipt(&self) -> &RepairDigest {
2295        &self.canonical_receipt
2296    }
2297}
2298
2299impl<'de> Deserialize<'de> for RepairExpectedState {
2300    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2301    where
2302        D: Deserializer<'de>,
2303    {
2304        let wire = RepairExpectedStateWire::deserialize(deserializer)?;
2305        Self::try_new(wire.version, wire.canonical_receipt).map_err(D::Error::custom)
2306    }
2307}
2308
2309#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2310#[serde(rename_all = "snake_case")]
2311pub enum RepairWriter {
2312    ReclassifyMemory,
2313    RenamePageTitle,
2314    CompleteEntityExtraction,
2315    NormalizeMemorySourceAgent,
2316    ClearMemorySupersedes,
2317    UnstageOrphanRevision,
2318    DeleteTagRow,
2319    DeleteMemoryEntityLink,
2320    BindPageLink,
2321    ArchiveEmptySourcePage,
2322    RegeneratePageProjection,
2323    QuarantineStalePageProjection,
2324}
2325
2326#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2327#[serde(tag = "kind", rename_all = "snake_case")]
2328pub enum RepairMutation {
2329    ReclassifyMemory {
2330        before_memory_type: Option<MemoryType>,
2331        after_memory_type: MemoryType,
2332    },
2333    RenamePageTitle {
2334        before_title: String,
2335        after_title: String,
2336        after_embedding_hex: String,
2337    },
2338    CompleteEntityExtraction {
2339        entity_ids: Vec<String>,
2340    },
2341    NormalizeMemorySourceAgent {
2342        before_source_agent: String,
2343    },
2344    ClearMemorySupersedes {
2345        before_supersedes: String,
2346    },
2347    UnstageOrphanRevision,
2348    DeleteTagRow {
2349        source: String,
2350        source_id: String,
2351        tag: String,
2352    },
2353    DeleteMemoryEntityLink {
2354        memory_id: String,
2355        entity_id: String,
2356    },
2357    BindPageLink {
2358        before_target_page_id: Option<String>,
2359        after_target_page_id: String,
2360    },
2361    ArchiveEmptySourcePage {
2362        before_status: String,
2363        after_status: String,
2364    },
2365    RegeneratePageProjection {
2366        database_version: i64,
2367    },
2368    QuarantineStalePageProjection {
2369        source_path: String,
2370        quarantine_path: String,
2371    },
2372}
2373
2374#[derive(Deserialize)]
2375#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
2376enum RepairMutationWire {
2377    ReclassifyMemory {
2378        before_memory_type: Option<MemoryType>,
2379        after_memory_type: MemoryType,
2380    },
2381    RenamePageTitle {
2382        before_title: String,
2383        after_title: String,
2384        after_embedding_hex: String,
2385    },
2386    CompleteEntityExtraction {
2387        entity_ids: Vec<String>,
2388    },
2389    NormalizeMemorySourceAgent {
2390        before_source_agent: String,
2391    },
2392    ClearMemorySupersedes {
2393        before_supersedes: String,
2394    },
2395    UnstageOrphanRevision,
2396    DeleteTagRow {
2397        source: String,
2398        source_id: String,
2399        tag: String,
2400    },
2401    DeleteMemoryEntityLink {
2402        memory_id: String,
2403        entity_id: String,
2404    },
2405    BindPageLink {
2406        before_target_page_id: Option<String>,
2407        after_target_page_id: String,
2408    },
2409    ArchiveEmptySourcePage {
2410        before_status: String,
2411        after_status: String,
2412    },
2413    RegeneratePageProjection {
2414        database_version: i64,
2415    },
2416    QuarantineStalePageProjection {
2417        source_path: String,
2418        quarantine_path: String,
2419    },
2420}
2421
2422impl RepairMutation {
2423    pub fn try_reclassify(
2424        before_memory_type: Option<&str>,
2425        after_memory_type: &str,
2426    ) -> Result<Self, RepairContractError> {
2427        let before_memory_type = before_memory_type.map(parse_memory_type).transpose()?;
2428        let after_memory_type = parse_memory_type(after_memory_type)?;
2429        Self::from_memory_types(before_memory_type, after_memory_type)
2430    }
2431
2432    pub fn from_memory_types(
2433        before_memory_type: Option<MemoryType>,
2434        after_memory_type: MemoryType,
2435    ) -> Result<Self, RepairContractError> {
2436        if before_memory_type.as_ref() == Some(&after_memory_type) {
2437            return Err(RepairContractError::InvalidMutation);
2438        }
2439        Ok(Self::ReclassifyMemory {
2440            before_memory_type,
2441            after_memory_type,
2442        })
2443    }
2444
2445    pub fn normalize_memory_source_agent(
2446        before_source_agent: String,
2447    ) -> Result<Self, RepairContractError> {
2448        if before_source_agent.is_empty() || !before_source_agent.trim().is_empty() {
2449            return Err(RepairContractError::InvalidMutation);
2450        }
2451        Ok(Self::NormalizeMemorySourceAgent {
2452            before_source_agent,
2453        })
2454    }
2455
2456    pub fn rename_page_title(
2457        before_title: String,
2458        after_title: String,
2459        after_embedding_hex: String,
2460    ) -> Result<Self, RepairContractError> {
2461        if !valid_nonempty(&before_title)
2462            || !valid_nonempty(&after_title)
2463            || before_title == after_title
2464            || !valid_embedding_hex(&after_embedding_hex)
2465        {
2466            return Err(RepairContractError::InvalidMutation);
2467        }
2468        Ok(Self::RenamePageTitle {
2469            before_title,
2470            after_title,
2471            after_embedding_hex,
2472        })
2473    }
2474
2475    pub fn complete_entity_extraction(
2476        entity_ids: Vec<String>,
2477    ) -> Result<Self, RepairContractError> {
2478        if !strictly_sorted_unique(&entity_ids) {
2479            return Err(RepairContractError::InvalidMutation);
2480        }
2481        Ok(Self::CompleteEntityExtraction { entity_ids })
2482    }
2483
2484    pub fn clear_memory_supersedes(before_supersedes: String) -> Result<Self, RepairContractError> {
2485        if !valid_nonempty(&before_supersedes) {
2486            return Err(RepairContractError::InvalidMutation);
2487        }
2488        Ok(Self::ClearMemorySupersedes { before_supersedes })
2489    }
2490
2491    pub const fn unstage_orphan_revision() -> Self {
2492        Self::UnstageOrphanRevision
2493    }
2494
2495    pub fn delete_tag_row(
2496        source: &str,
2497        source_id: &str,
2498        tag: &str,
2499    ) -> Result<Self, RepairContractError> {
2500        if !valid_nonempty(source) || !valid_nonempty(source_id) {
2501            return Err(RepairContractError::InvalidMutation);
2502        }
2503        Ok(Self::DeleteTagRow {
2504            source: source.to_string(),
2505            source_id: source_id.to_string(),
2506            tag: tag.to_string(),
2507        })
2508    }
2509
2510    pub fn delete_memory_entity_link(
2511        memory_id: &str,
2512        entity_id: &str,
2513    ) -> Result<Self, RepairContractError> {
2514        if !valid_nonempty(memory_id) || !valid_nonempty(entity_id) {
2515            return Err(RepairContractError::InvalidMutation);
2516        }
2517        Ok(Self::DeleteMemoryEntityLink {
2518            memory_id: memory_id.to_string(),
2519            entity_id: entity_id.to_string(),
2520        })
2521    }
2522
2523    pub fn bind_page_link(
2524        before_target_page_id: Option<String>,
2525        after_target_page_id: String,
2526    ) -> Result<Self, RepairContractError> {
2527        if before_target_page_id.is_some() || !valid_nonempty(&after_target_page_id) {
2528            return Err(RepairContractError::InvalidMutation);
2529        }
2530        Ok(Self::BindPageLink {
2531            before_target_page_id,
2532            after_target_page_id,
2533        })
2534    }
2535
2536    pub fn regenerate_page_projection(database_version: i64) -> Result<Self, RepairContractError> {
2537        if database_version < 0 {
2538            return Err(RepairContractError::InvalidMutation);
2539        }
2540        Ok(Self::RegeneratePageProjection { database_version })
2541    }
2542
2543    pub fn archive_empty_source_page() -> Self {
2544        Self::ArchiveEmptySourcePage {
2545            before_status: "active".to_string(),
2546            after_status: "archived".to_string(),
2547        }
2548    }
2549
2550    pub fn quarantine_stale_page_projection(
2551        source_path: String,
2552        quarantine_path: String,
2553    ) -> Result<Self, RepairContractError> {
2554        if !valid_root_relative_path(&source_path)
2555            || source_path.starts_with(".wenlan/")
2556            || source_path.starts_with("_sources/")
2557            || !source_path.to_ascii_lowercase().ends_with(".md")
2558            || !valid_root_relative_path(&quarantine_path)
2559            || !quarantine_path.starts_with(".wenlan/orphaned/")
2560            || !quarantine_path.to_ascii_lowercase().ends_with(".md")
2561            || source_path == quarantine_path
2562        {
2563            return Err(RepairContractError::InvalidMutation);
2564        }
2565        Ok(Self::QuarantineStalePageProjection {
2566            source_path,
2567            quarantine_path,
2568        })
2569    }
2570
2571    pub fn before_memory_type(&self) -> Option<&str> {
2572        match self {
2573            Self::ReclassifyMemory {
2574                before_memory_type, ..
2575            } => before_memory_type.as_ref().map(|value| match value {
2576                MemoryType::Identity => "identity",
2577                MemoryType::Preference => "preference",
2578                MemoryType::Decision => "decision",
2579                MemoryType::Lesson => "lesson",
2580                MemoryType::Gotcha => "gotcha",
2581                MemoryType::Fact => "fact",
2582            }),
2583            Self::NormalizeMemorySourceAgent { .. }
2584            | Self::RenamePageTitle { .. }
2585            | Self::CompleteEntityExtraction { .. }
2586            | Self::ClearMemorySupersedes { .. }
2587            | Self::UnstageOrphanRevision
2588            | Self::DeleteTagRow { .. }
2589            | Self::DeleteMemoryEntityLink { .. }
2590            | Self::BindPageLink { .. }
2591            | Self::ArchiveEmptySourcePage { .. }
2592            | Self::RegeneratePageProjection { .. }
2593            | Self::QuarantineStalePageProjection { .. } => None,
2594        }
2595    }
2596
2597    pub fn after_memory_type(&self) -> &str {
2598        match self {
2599            Self::ReclassifyMemory {
2600                after_memory_type, ..
2601            } => match after_memory_type {
2602                MemoryType::Identity => "identity",
2603                MemoryType::Preference => "preference",
2604                MemoryType::Decision => "decision",
2605                MemoryType::Lesson => "lesson",
2606                MemoryType::Gotcha => "gotcha",
2607                MemoryType::Fact => "fact",
2608            },
2609            Self::NormalizeMemorySourceAgent { .. }
2610            | Self::RenamePageTitle { .. }
2611            | Self::CompleteEntityExtraction { .. }
2612            | Self::ClearMemorySupersedes { .. }
2613            | Self::UnstageOrphanRevision
2614            | Self::DeleteTagRow { .. }
2615            | Self::DeleteMemoryEntityLink { .. }
2616            | Self::BindPageLink { .. }
2617            | Self::ArchiveEmptySourcePage { .. }
2618            | Self::RegeneratePageProjection { .. }
2619            | Self::QuarantineStalePageProjection { .. } => {
2620                panic!("repair mutation is not a memory reclassification")
2621            }
2622        }
2623    }
2624}
2625
2626impl<'de> Deserialize<'de> for RepairMutation {
2627    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2628    where
2629        D: Deserializer<'de>,
2630    {
2631        match RepairMutationWire::deserialize(deserializer)? {
2632            RepairMutationWire::ReclassifyMemory {
2633                before_memory_type,
2634                after_memory_type,
2635            } => Self::from_memory_types(before_memory_type, after_memory_type),
2636            RepairMutationWire::RenamePageTitle {
2637                before_title,
2638                after_title,
2639                after_embedding_hex,
2640            } => Self::rename_page_title(before_title, after_title, after_embedding_hex),
2641            RepairMutationWire::CompleteEntityExtraction { entity_ids } => {
2642                Self::complete_entity_extraction(entity_ids)
2643            }
2644            RepairMutationWire::NormalizeMemorySourceAgent {
2645                before_source_agent,
2646            } => Self::normalize_memory_source_agent(before_source_agent),
2647            RepairMutationWire::ClearMemorySupersedes { before_supersedes } => {
2648                Self::clear_memory_supersedes(before_supersedes)
2649            }
2650            RepairMutationWire::UnstageOrphanRevision => Ok(Self::unstage_orphan_revision()),
2651            RepairMutationWire::DeleteTagRow {
2652                source,
2653                source_id,
2654                tag,
2655            } => Self::delete_tag_row(&source, &source_id, &tag),
2656            RepairMutationWire::DeleteMemoryEntityLink {
2657                memory_id,
2658                entity_id,
2659            } => Self::delete_memory_entity_link(&memory_id, &entity_id),
2660            RepairMutationWire::BindPageLink {
2661                before_target_page_id,
2662                after_target_page_id,
2663            } => Self::bind_page_link(before_target_page_id, after_target_page_id),
2664            RepairMutationWire::ArchiveEmptySourcePage {
2665                before_status,
2666                after_status,
2667            } if before_status == "active" && after_status == "archived" => {
2668                Ok(Self::archive_empty_source_page())
2669            }
2670            RepairMutationWire::ArchiveEmptySourcePage { .. } => {
2671                Err(RepairContractError::InvalidMutation)
2672            }
2673            RepairMutationWire::RegeneratePageProjection { database_version } => {
2674                Self::regenerate_page_projection(database_version)
2675            }
2676            RepairMutationWire::QuarantineStalePageProjection {
2677                source_path,
2678                quarantine_path,
2679            } => Self::quarantine_stale_page_projection(source_path, quarantine_path),
2680        }
2681        .map_err(D::Error::custom)
2682    }
2683}
2684
2685#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2686#[serde(rename_all = "snake_case")]
2687pub enum RepairMemoryField {
2688    MemoryType,
2689    SourceAgent,
2690    Supersedes,
2691    PendingRevision,
2692    TagRow,
2693    MemoryEntityLink,
2694    MemoryEntityLinks,
2695    EnrichmentStep,
2696    TargetPageId,
2697    PageStatus,
2698    PageTitle,
2699    PageVersion,
2700    PageEmbedding,
2701    PageProjection,
2702    PageProjectionQuarantine,
2703}
2704
2705#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2706pub struct RepairAllowedEffects {
2707    owner: RepairTarget,
2708    fields: Vec<RepairMemoryField>,
2709}
2710
2711#[derive(Deserialize)]
2712#[serde(deny_unknown_fields)]
2713struct RepairAllowedEffectsWire {
2714    owner: RepairTarget,
2715    fields: Vec<RepairMemoryField>,
2716}
2717
2718impl RepairAllowedEffects {
2719    pub fn memory_type(owner: RepairTarget) -> Self {
2720        Self {
2721            owner,
2722            fields: vec![RepairMemoryField::MemoryType],
2723        }
2724    }
2725
2726    pub fn memory_source_agent(owner: RepairTarget) -> Self {
2727        Self {
2728            owner,
2729            fields: vec![RepairMemoryField::SourceAgent],
2730        }
2731    }
2732
2733    pub fn memory_supersedes(owner: RepairTarget) -> Self {
2734        Self {
2735            owner,
2736            fields: vec![RepairMemoryField::Supersedes],
2737        }
2738    }
2739
2740    pub fn memory_pending_revision(owner: RepairTarget) -> Self {
2741        Self {
2742            owner,
2743            fields: vec![RepairMemoryField::PendingRevision],
2744        }
2745    }
2746
2747    pub fn tag_row(owner: RepairTarget) -> Self {
2748        Self {
2749            owner,
2750            fields: vec![RepairMemoryField::TagRow],
2751        }
2752    }
2753
2754    pub fn memory_entity_link(owner: RepairTarget) -> Self {
2755        Self {
2756            owner,
2757            fields: vec![RepairMemoryField::MemoryEntityLink],
2758        }
2759    }
2760
2761    pub fn complete_entity_extraction(owner: RepairTarget) -> Self {
2762        Self {
2763            owner,
2764            fields: vec![
2765                RepairMemoryField::MemoryEntityLinks,
2766                RepairMemoryField::EnrichmentStep,
2767            ],
2768        }
2769    }
2770
2771    pub fn page_link_target(owner: RepairTarget) -> Self {
2772        Self {
2773            owner,
2774            fields: vec![RepairMemoryField::TargetPageId],
2775        }
2776    }
2777
2778    pub fn page_projection(owner: RepairTarget) -> Self {
2779        Self {
2780            owner,
2781            fields: vec![RepairMemoryField::PageProjection],
2782        }
2783    }
2784
2785    pub fn page_title_rename(owner: RepairTarget) -> Self {
2786        Self {
2787            owner,
2788            fields: vec![
2789                RepairMemoryField::PageTitle,
2790                RepairMemoryField::PageVersion,
2791                RepairMemoryField::PageEmbedding,
2792                RepairMemoryField::PageProjection,
2793            ],
2794        }
2795    }
2796
2797    pub fn page_status(owner: RepairTarget) -> Self {
2798        Self {
2799            owner,
2800            fields: vec![RepairMemoryField::PageStatus],
2801        }
2802    }
2803
2804    pub fn page_projection_quarantine(owner: RepairTarget) -> Self {
2805        Self {
2806            owner,
2807            fields: vec![RepairMemoryField::PageProjectionQuarantine],
2808        }
2809    }
2810
2811    pub fn owner(&self) -> &RepairTarget {
2812        &self.owner
2813    }
2814
2815    pub fn fields(&self) -> &[RepairMemoryField] {
2816        &self.fields
2817    }
2818}
2819
2820impl<'de> Deserialize<'de> for RepairAllowedEffects {
2821    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2822    where
2823        D: Deserializer<'de>,
2824    {
2825        let wire = RepairAllowedEffectsWire::deserialize(deserializer)?;
2826        let canonical = wire.fields.windows(2).all(|pair| pair[0] < pair[1]);
2827        let supported_shape = wire.fields.len() == 1
2828            || wire.fields
2829                == [
2830                    RepairMemoryField::MemoryEntityLinks,
2831                    RepairMemoryField::EnrichmentStep,
2832                ]
2833            || wire.fields
2834                == [
2835                    RepairMemoryField::PageTitle,
2836                    RepairMemoryField::PageVersion,
2837                    RepairMemoryField::PageEmbedding,
2838                    RepairMemoryField::PageProjection,
2839                ];
2840        if !canonical || !supported_shape {
2841            return Err(D::Error::custom(RepairContractError::InvalidAllowedEffects));
2842        }
2843        Ok(Self {
2844            owner: wire.owner,
2845            fields: wire.fields,
2846        })
2847    }
2848}
2849
2850#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2851pub struct RepairRollbackArtifact {
2852    format_version: u16,
2853    relative_path: String,
2854    digest: RepairDigest,
2855}
2856
2857#[derive(Deserialize)]
2858#[serde(deny_unknown_fields)]
2859struct RepairRollbackArtifactWire {
2860    format_version: u16,
2861    relative_path: String,
2862    digest: RepairDigest,
2863}
2864
2865impl RepairRollbackArtifact {
2866    pub fn try_new(
2867        relative_path: String,
2868        digest: RepairDigest,
2869    ) -> Result<Self, RepairContractError> {
2870        Self::try_new_for_format(
2871            PREVIOUS_REPAIR_ROLLBACK_FORMAT_VERSION,
2872            relative_path,
2873            digest,
2874        )
2875    }
2876
2877    pub fn try_new_v2(
2878        relative_path: String,
2879        digest: RepairDigest,
2880    ) -> Result<Self, RepairContractError> {
2881        Self::try_new_for_format(REPAIR_ROLLBACK_FORMAT_VERSION, relative_path, digest)
2882    }
2883
2884    fn try_new_for_format(
2885        format_version: u16,
2886        relative_path: String,
2887        digest: RepairDigest,
2888    ) -> Result<Self, RepairContractError> {
2889        let path = Path::new(&relative_path);
2890        if !matches!(format_version, 1 | REPAIR_ROLLBACK_FORMAT_VERSION)
2891            || !valid_nonempty(&relative_path)
2892            || path.is_absolute()
2893            || path
2894                .components()
2895                .any(|component| !matches!(component, Component::Normal(_)))
2896        {
2897            return Err(RepairContractError::InvalidRollbackArtifact);
2898        }
2899        Ok(Self {
2900            format_version,
2901            relative_path,
2902            digest,
2903        })
2904    }
2905
2906    pub const fn format_version(&self) -> u16 {
2907        self.format_version
2908    }
2909
2910    pub fn relative_path(&self) -> &str {
2911        &self.relative_path
2912    }
2913
2914    pub fn digest(&self) -> &RepairDigest {
2915        &self.digest
2916    }
2917}
2918
2919impl<'de> Deserialize<'de> for RepairRollbackArtifact {
2920    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2921    where
2922        D: Deserializer<'de>,
2923    {
2924        let wire = RepairRollbackArtifactWire::deserialize(deserializer)?;
2925        Self::try_new_for_format(wire.format_version, wire.relative_path, wire.digest)
2926            .map_err(D::Error::custom)
2927    }
2928}
2929
2930#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2931#[serde(tag = "kind", rename_all = "snake_case")]
2932pub enum RepairVerificationPolicy {
2933    LegacyWholeReports,
2934    ApplicableChecks {
2935        required_deep_check_ids: Vec<String>,
2936    },
2937    GeneralOnly,
2938}
2939
2940#[derive(Deserialize)]
2941#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
2942enum RepairVerificationPolicyWire {
2943    LegacyWholeReports,
2944    ApplicableChecks {
2945        required_deep_check_ids: Vec<String>,
2946    },
2947    GeneralOnly,
2948}
2949
2950impl RepairVerificationPolicy {
2951    fn applicable_checks(
2952        required_deep_check_ids: Vec<String>,
2953    ) -> Result<Self, RepairContractError> {
2954        if required_deep_check_ids
2955            .iter()
2956            .any(|check_id| !valid_nonempty(check_id))
2957            || !required_deep_check_ids
2958                .windows(2)
2959                .all(|pair| pair[0] < pair[1])
2960        {
2961            return Err(RepairContractError::InvalidPostAssertions);
2962        }
2963        Ok(Self::ApplicableChecks {
2964            required_deep_check_ids,
2965        })
2966    }
2967
2968    pub fn required_deep_check_ids(&self) -> Option<&[String]> {
2969        match self {
2970            Self::LegacyWholeReports | Self::GeneralOnly => None,
2971            Self::ApplicableChecks {
2972                required_deep_check_ids,
2973            } => Some(required_deep_check_ids),
2974        }
2975    }
2976
2977    pub const fn requires_whole_reports(&self) -> bool {
2978        matches!(self, Self::LegacyWholeReports)
2979    }
2980
2981    pub const fn is_general_only(&self) -> bool {
2982        matches!(self, Self::GeneralOnly)
2983    }
2984}
2985
2986impl<'de> Deserialize<'de> for RepairVerificationPolicy {
2987    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2988    where
2989        D: Deserializer<'de>,
2990    {
2991        let wire = RepairVerificationPolicyWire::deserialize(deserializer)?;
2992        match wire {
2993            RepairVerificationPolicyWire::LegacyWholeReports => Ok(Self::LegacyWholeReports),
2994            RepairVerificationPolicyWire::ApplicableChecks {
2995                required_deep_check_ids,
2996            } => Self::applicable_checks(required_deep_check_ids).map_err(D::Error::custom),
2997            RepairVerificationPolicyWire::GeneralOnly => Ok(Self::GeneralOnly),
2998        }
2999    }
3000}
3001
3002#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3003pub struct RepairPostAssertions {
3004    target_check_id: String,
3005    target_evidence_id: LintDigest,
3006    general_baseline: Vec<RepairCheckBaseline>,
3007    deep_baseline: Vec<RepairCheckBaseline>,
3008    #[serde(skip_serializing_if = "Option::is_none")]
3009    target_record_set: Option<RepairRecordSetBaseline>,
3010    verification_policy: RepairVerificationPolicy,
3011    require_complete_general: bool,
3012    reject_new_actionable: bool,
3013    reject_new_incomplete: bool,
3014    allowed_non_target_check_deltas: Vec<String>,
3015}
3016
3017#[derive(Deserialize)]
3018#[serde(deny_unknown_fields)]
3019struct RepairPostAssertionsWire {
3020    target_check_id: String,
3021    target_evidence_id: LintDigest,
3022    general_baseline: Vec<RepairCheckBaseline>,
3023    deep_baseline: Vec<RepairCheckBaseline>,
3024    #[serde(default)]
3025    target_record_set: Option<RepairRecordSetBaseline>,
3026    verification_policy: RepairVerificationPolicy,
3027    require_complete_general: bool,
3028    reject_new_actionable: bool,
3029    reject_new_incomplete: bool,
3030    allowed_non_target_check_deltas: Vec<String>,
3031}
3032
3033#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3034pub struct RepairCheckBaseline {
3035    check_id: String,
3036    outcome: crate::lint::LintOutcome,
3037    gate_effect: crate::lint::LintGateEffect,
3038    #[serde(skip_serializing_if = "Option::is_none")]
3039    affected_records: Option<u64>,
3040    evidence: Vec<crate::lint::LintEvidenceRef>,
3041}
3042
3043#[derive(Deserialize)]
3044#[serde(deny_unknown_fields)]
3045struct RepairCheckBaselineWire {
3046    check_id: String,
3047    outcome: crate::lint::LintOutcome,
3048    gate_effect: crate::lint::LintGateEffect,
3049    #[serde(default)]
3050    affected_records: Option<u64>,
3051    evidence: Vec<crate::lint::LintEvidenceRef>,
3052}
3053
3054impl RepairCheckBaseline {
3055    pub fn try_new(
3056        check_id: String,
3057        outcome: crate::lint::LintOutcome,
3058        gate_effect: crate::lint::LintGateEffect,
3059        evidence: Vec<crate::lint::LintEvidenceRef>,
3060    ) -> Result<Self, RepairContractError> {
3061        Self::try_new_with_affected_records(check_id, outcome, gate_effect, None, evidence)
3062    }
3063
3064    pub fn try_new_current(
3065        check_id: String,
3066        outcome: crate::lint::LintOutcome,
3067        gate_effect: crate::lint::LintGateEffect,
3068        affected_records: u64,
3069        evidence: Vec<crate::lint::LintEvidenceRef>,
3070    ) -> Result<Self, RepairContractError> {
3071        Self::try_new_with_affected_records(
3072            check_id,
3073            outcome,
3074            gate_effect,
3075            Some(affected_records),
3076            evidence,
3077        )
3078    }
3079
3080    fn try_new_with_affected_records(
3081        check_id: String,
3082        outcome: crate::lint::LintOutcome,
3083        gate_effect: crate::lint::LintGateEffect,
3084        affected_records: Option<u64>,
3085        evidence: Vec<crate::lint::LintEvidenceRef>,
3086    ) -> Result<Self, RepairContractError> {
3087        if !valid_nonempty(&check_id) {
3088            return Err(RepairContractError::InvalidPostAssertions);
3089        }
3090        Ok(Self {
3091            check_id,
3092            outcome,
3093            gate_effect,
3094            affected_records,
3095            evidence,
3096        })
3097    }
3098
3099    pub fn check_id(&self) -> &str {
3100        &self.check_id
3101    }
3102
3103    pub const fn outcome(&self) -> crate::lint::LintOutcome {
3104        self.outcome
3105    }
3106
3107    pub const fn gate_effect(&self) -> crate::lint::LintGateEffect {
3108        self.gate_effect
3109    }
3110
3111    pub const fn affected_records(&self) -> Option<u64> {
3112        self.affected_records
3113    }
3114
3115    pub fn evidence(&self) -> &[crate::lint::LintEvidenceRef] {
3116        &self.evidence
3117    }
3118}
3119
3120impl<'de> Deserialize<'de> for RepairCheckBaseline {
3121    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3122    where
3123        D: Deserializer<'de>,
3124    {
3125        let wire = RepairCheckBaselineWire::deserialize(deserializer)?;
3126        Self::try_new_with_affected_records(
3127            wire.check_id,
3128            wire.outcome,
3129            wire.gate_effect,
3130            wire.affected_records,
3131            wire.evidence,
3132        )
3133        .map_err(D::Error::custom)
3134    }
3135}
3136
3137#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3138pub struct RepairRecordSetBaseline {
3139    record_count: u64,
3140    digest: RepairDigest,
3141}
3142
3143#[derive(Deserialize)]
3144#[serde(deny_unknown_fields)]
3145struct RepairRecordSetBaselineWire {
3146    record_count: u64,
3147    digest: RepairDigest,
3148}
3149
3150impl RepairRecordSetBaseline {
3151    pub fn try_new(record_count: u64, digest: RepairDigest) -> Result<Self, RepairContractError> {
3152        if record_count == 0 {
3153            return Err(RepairContractError::InvalidPostAssertions);
3154        }
3155        Ok(Self {
3156            record_count,
3157            digest,
3158        })
3159    }
3160
3161    pub const fn record_count(&self) -> u64 {
3162        self.record_count
3163    }
3164
3165    pub const fn digest(&self) -> &RepairDigest {
3166        &self.digest
3167    }
3168}
3169
3170impl<'de> Deserialize<'de> for RepairRecordSetBaseline {
3171    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3172    where
3173        D: Deserializer<'de>,
3174    {
3175        let wire = RepairRecordSetBaselineWire::deserialize(deserializer)?;
3176        Self::try_new(wire.record_count, wire.digest).map_err(D::Error::custom)
3177    }
3178}
3179
3180fn valid_check_baseline(values: &[RepairCheckBaseline]) -> bool {
3181    !values.is_empty()
3182        && values
3183            .windows(2)
3184            .all(|pair| pair[0].check_id() < pair[1].check_id())
3185}
3186
3187fn manifest_baseline_schema_matches(
3188    manifest_schema_version: u16,
3189    assertions: &RepairPostAssertions,
3190) -> bool {
3191    let baselines = assertions
3192        .general_baseline()
3193        .iter()
3194        .chain(assertions.deep_baseline());
3195    match manifest_schema_version {
3196        2 => {
3197            assertions.target_record_set().is_none()
3198                && baselines
3199                    .clone()
3200                    .all(|baseline| baseline.affected_records().is_none())
3201                && !baselines
3202                    .flat_map(RepairCheckBaseline::evidence)
3203                    .any(|evidence| matches!(evidence, LintEvidenceRef::OpaqueDigest { .. }))
3204        }
3205        3 => assertions.target_record_set().is_none(),
3206        4 | 5 | REPAIR_MANIFEST_SCHEMA_VERSION
3207            if assertions.target_check_id() == REPAIR_TAG_INTEGRITY_CHECK_ID =>
3208        {
3209            assertions.target_record_set().is_some()
3210                && assertions
3211                    .general_baseline()
3212                    .iter()
3213                    .find(|baseline| baseline.check_id() == REPAIR_TAG_INTEGRITY_CHECK_ID)
3214                    .is_some_and(|baseline| baseline.affected_records().is_some())
3215        }
3216        4 | 5 | REPAIR_MANIFEST_SCHEMA_VERSION => assertions.target_record_set().is_none(),
3217        _ => false,
3218    }
3219}
3220
3221impl RepairPostAssertions {
3222    pub fn try_new(
3223        target_evidence_id: LintDigest,
3224        general_baseline: Vec<RepairCheckBaseline>,
3225        deep_baseline: Vec<RepairCheckBaseline>,
3226        allowed_non_target_check_deltas: Vec<String>,
3227    ) -> Result<Self, RepairContractError> {
3228        Self::try_new_for_check(
3229            REPAIR_CLASSIFICATION_CHECK_ID.to_string(),
3230            target_evidence_id,
3231            general_baseline,
3232            deep_baseline,
3233            vec![REPAIR_CLASSIFICATION_CHECK_ID.to_string()],
3234            allowed_non_target_check_deltas,
3235        )
3236    }
3237
3238    #[allow(clippy::too_many_arguments)]
3239    pub fn try_new_for_check(
3240        target_check_id: String,
3241        target_evidence_id: LintDigest,
3242        general_baseline: Vec<RepairCheckBaseline>,
3243        deep_baseline: Vec<RepairCheckBaseline>,
3244        required_deep_check_ids: Vec<String>,
3245        allowed_non_target_check_deltas: Vec<String>,
3246    ) -> Result<Self, RepairContractError> {
3247        if !valid_nonempty(&target_check_id)
3248            || !valid_check_baseline(&general_baseline)
3249            || !valid_check_baseline(&deep_baseline)
3250            || allowed_non_target_check_deltas
3251                .iter()
3252                .any(|value| !valid_nonempty(value))
3253            || !allowed_non_target_check_deltas
3254                .windows(2)
3255                .all(|pair| pair[0] < pair[1])
3256        {
3257            return Err(RepairContractError::InvalidPostAssertions);
3258        }
3259        Ok(Self {
3260            target_check_id,
3261            target_evidence_id,
3262            general_baseline,
3263            deep_baseline,
3264            target_record_set: None,
3265            verification_policy: RepairVerificationPolicy::applicable_checks(
3266                required_deep_check_ids,
3267            )?,
3268            require_complete_general: true,
3269            reject_new_actionable: true,
3270            reject_new_incomplete: true,
3271            allowed_non_target_check_deltas,
3272        })
3273    }
3274
3275    pub fn try_new_general_only_for_check(
3276        target_check_id: String,
3277        target_evidence_id: LintDigest,
3278        general_baseline: Vec<RepairCheckBaseline>,
3279        allowed_non_target_check_deltas: Vec<String>,
3280    ) -> Result<Self, RepairContractError> {
3281        if !valid_nonempty(&target_check_id)
3282            || !valid_check_baseline(&general_baseline)
3283            || allowed_non_target_check_deltas
3284                .iter()
3285                .any(|value| !valid_nonempty(value))
3286            || !allowed_non_target_check_deltas
3287                .windows(2)
3288                .all(|pair| pair[0] < pair[1])
3289        {
3290            return Err(RepairContractError::InvalidPostAssertions);
3291        }
3292        Ok(Self {
3293            target_check_id,
3294            target_evidence_id,
3295            general_baseline,
3296            deep_baseline: Vec::new(),
3297            target_record_set: None,
3298            verification_policy: RepairVerificationPolicy::GeneralOnly,
3299            require_complete_general: true,
3300            reject_new_actionable: true,
3301            reject_new_incomplete: true,
3302            allowed_non_target_check_deltas,
3303        })
3304    }
3305
3306    fn try_new_legacy_v1(
3307        target_evidence_id: LintDigest,
3308        general_baseline: Vec<RepairCheckBaseline>,
3309        deep_baseline: Vec<RepairCheckBaseline>,
3310        allowed_non_target_check_deltas: Vec<String>,
3311    ) -> Result<Self, RepairContractError> {
3312        let mut assertions = Self::try_new(
3313            target_evidence_id,
3314            general_baseline,
3315            deep_baseline,
3316            allowed_non_target_check_deltas,
3317        )?;
3318        assertions.verification_policy = RepairVerificationPolicy::LegacyWholeReports;
3319        Ok(assertions)
3320    }
3321
3322    pub fn target_evidence_id(&self) -> &LintDigest {
3323        &self.target_evidence_id
3324    }
3325
3326    pub fn target_check_id(&self) -> &str {
3327        &self.target_check_id
3328    }
3329
3330    pub fn try_with_target_record_set(
3331        mut self,
3332        target_record_set: RepairRecordSetBaseline,
3333    ) -> Result<Self, RepairContractError> {
3334        if self.target_check_id != REPAIR_TAG_INTEGRITY_CHECK_ID || self.target_record_set.is_some()
3335        {
3336            return Err(RepairContractError::InvalidPostAssertions);
3337        }
3338        self.target_record_set = Some(target_record_set);
3339        Ok(self)
3340    }
3341
3342    pub fn general_baseline(&self) -> &[RepairCheckBaseline] {
3343        &self.general_baseline
3344    }
3345
3346    pub fn deep_baseline(&self) -> &[RepairCheckBaseline] {
3347        &self.deep_baseline
3348    }
3349
3350    pub const fn target_record_set(&self) -> Option<&RepairRecordSetBaseline> {
3351        self.target_record_set.as_ref()
3352    }
3353
3354    pub const fn verification_policy(&self) -> &RepairVerificationPolicy {
3355        &self.verification_policy
3356    }
3357
3358    pub fn allowed_non_target_check_deltas(&self) -> &[String] {
3359        &self.allowed_non_target_check_deltas
3360    }
3361}
3362
3363impl<'de> Deserialize<'de> for RepairPostAssertions {
3364    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3365    where
3366        D: Deserializer<'de>,
3367    {
3368        let wire = RepairPostAssertionsWire::deserialize(deserializer)?;
3369        if !wire.require_complete_general
3370            || !wire.reject_new_actionable
3371            || !wire.reject_new_incomplete
3372            || wire.verification_policy.requires_whole_reports()
3373        {
3374            return Err(D::Error::custom(RepairContractError::InvalidPostAssertions));
3375        }
3376        if wire.verification_policy.is_general_only() {
3377            if !wire.deep_baseline.is_empty() {
3378                return Err(D::Error::custom(RepairContractError::InvalidPostAssertions));
3379            }
3380            let assertions = Self::try_new_general_only_for_check(
3381                wire.target_check_id,
3382                wire.target_evidence_id,
3383                wire.general_baseline,
3384                wire.allowed_non_target_check_deltas,
3385            )
3386            .map_err(D::Error::custom)?;
3387            return match wire.target_record_set {
3388                Some(target_record_set) => assertions
3389                    .try_with_target_record_set(target_record_set)
3390                    .map_err(D::Error::custom),
3391                None => Ok(assertions),
3392            };
3393        }
3394        let assertions = Self::try_new_for_check(
3395            wire.target_check_id,
3396            wire.target_evidence_id,
3397            wire.general_baseline,
3398            wire.deep_baseline,
3399            wire.verification_policy
3400                .required_deep_check_ids()
3401                .unwrap_or_default()
3402                .to_vec(),
3403            wire.allowed_non_target_check_deltas,
3404        )
3405        .map_err(D::Error::custom)?;
3406        match wire.target_record_set {
3407            Some(target_record_set) => assertions
3408                .try_with_target_record_set(target_record_set)
3409                .map_err(D::Error::custom),
3410            None => Ok(assertions),
3411        }
3412    }
3413}
3414
3415#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3416pub struct RepairManifestDraft {
3417    manifest_schema_version: u16,
3418    manifest_id: String,
3419    prepared_at: i64,
3420    source: RepairSource,
3421    target: RepairTarget,
3422    expected_state: RepairExpectedState,
3423    writer: RepairWriter,
3424    mutation: RepairMutation,
3425    allowed_effects: RepairAllowedEffects,
3426    rollback: RepairRollbackArtifact,
3427    post_assertions: RepairPostAssertions,
3428}
3429
3430impl RepairManifestDraft {
3431    #[allow(clippy::too_many_arguments)]
3432    pub fn try_new(
3433        manifest_id: String,
3434        prepared_at: i64,
3435        source: RepairSource,
3436        target: RepairTarget,
3437        expected_state: RepairExpectedState,
3438        writer: RepairWriter,
3439        mutation: RepairMutation,
3440        allowed_effects: RepairAllowedEffects,
3441        rollback: RepairRollbackArtifact,
3442        post_assertions: RepairPostAssertions,
3443    ) -> Result<Self, RepairContractError> {
3444        let manifest_schema_version = match writer {
3445            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction => {
3446                REPAIR_MANIFEST_SCHEMA_VERSION
3447            }
3448            RepairWriter::ReclassifyMemory if source.review_binding().is_some() => {
3449                REPAIR_MANIFEST_SCHEMA_VERSION
3450            }
3451            _ => PREVIOUS_REPAIR_MANIFEST_SCHEMA_VERSION,
3452        };
3453        Self::try_new_for_schema(
3454            manifest_schema_version,
3455            manifest_id,
3456            prepared_at,
3457            source,
3458            target,
3459            expected_state,
3460            writer,
3461            mutation,
3462            allowed_effects,
3463            rollback,
3464            post_assertions,
3465        )
3466    }
3467
3468    #[allow(clippy::too_many_arguments)]
3469    fn try_new_for_schema(
3470        manifest_schema_version: u16,
3471        manifest_id: String,
3472        prepared_at: i64,
3473        source: RepairSource,
3474        target: RepairTarget,
3475        expected_state: RepairExpectedState,
3476        writer: RepairWriter,
3477        mutation: RepairMutation,
3478        allowed_effects: RepairAllowedEffects,
3479        rollback: RepairRollbackArtifact,
3480        post_assertions: RepairPostAssertions,
3481    ) -> Result<Self, RepairContractError> {
3482        if !matches!(
3483            manifest_schema_version,
3484            2 | 3 | 4 | 5 | REPAIR_MANIFEST_SCHEMA_VERSION
3485        ) {
3486            return Err(RepairContractError::UnsupportedManifestSchema);
3487        }
3488        let expected_report_schema_version = if manifest_schema_version == 2 {
3489            PREVIOUS_LINT_REPORT_SCHEMA_VERSION
3490        } else {
3491            LINT_REPORT_SCHEMA_VERSION
3492        };
3493        let catalog_version_supported = source.check_catalog_version == LINT_CHECK_CATALOG_VERSION
3494            || (matches!(manifest_schema_version, 2..=4)
3495                && source.check_catalog_version == PREVIOUS_LINT_CHECK_CATALOG_VERSION);
3496        if source.report_schema_version != expected_report_schema_version
3497            || !catalog_version_supported
3498            || !manifest_baseline_schema_matches(manifest_schema_version, &post_assertions)
3499        {
3500            return Err(RepairContractError::InvalidManifest);
3501        }
3502        if manifest_schema_version == 2
3503            && (source.is_general_only_deterministic()
3504                || source
3505                    .deterministic_evidence()
3506                    .iter()
3507                    .any(|evidence| matches!(evidence, LintEvidenceRef::OpaqueDigest { .. }))
3508                || post_assertions.verification_policy().is_general_only())
3509        {
3510            return Err(RepairContractError::InvalidManifest);
3511        }
3512        if !valid_manifest_id(&manifest_id) || prepared_at <= 0 {
3513            return Err(RepairContractError::InvalidManifest);
3514        }
3515        let compatible = match (&target, writer, &mutation, allowed_effects.fields()) {
3516            (
3517                RepairTarget::Memory { .. },
3518                RepairWriter::ReclassifyMemory,
3519                RepairMutation::ReclassifyMemory { .. },
3520                [RepairMemoryField::MemoryType],
3521            )
3522            | (
3523                RepairTarget::PageProjection { .. },
3524                RepairWriter::RenamePageTitle,
3525                RepairMutation::RenamePageTitle { .. },
3526                [RepairMemoryField::PageTitle, RepairMemoryField::PageVersion, RepairMemoryField::PageEmbedding, RepairMemoryField::PageProjection],
3527            )
3528            | (
3529                RepairTarget::Memory { .. },
3530                RepairWriter::NormalizeMemorySourceAgent,
3531                RepairMutation::NormalizeMemorySourceAgent { .. },
3532                [RepairMemoryField::SourceAgent],
3533            )
3534            | (
3535                RepairTarget::Memory { .. },
3536                RepairWriter::ClearMemorySupersedes,
3537                RepairMutation::ClearMemorySupersedes { .. },
3538                [RepairMemoryField::Supersedes],
3539            )
3540            | (
3541                RepairTarget::Memory { .. },
3542                RepairWriter::UnstageOrphanRevision,
3543                RepairMutation::UnstageOrphanRevision,
3544                [RepairMemoryField::PendingRevision],
3545            )
3546            | (
3547                RepairTarget::Tag { .. },
3548                RepairWriter::DeleteTagRow,
3549                RepairMutation::DeleteTagRow { .. },
3550                [RepairMemoryField::TagRow],
3551            )
3552            | (
3553                RepairTarget::PageLink { .. },
3554                RepairWriter::BindPageLink,
3555                RepairMutation::BindPageLink { .. },
3556                [RepairMemoryField::TargetPageId],
3557            )
3558            | (
3559                RepairTarget::PageProjection { .. },
3560                RepairWriter::RegeneratePageProjection,
3561                RepairMutation::RegeneratePageProjection { .. },
3562                [RepairMemoryField::PageProjection],
3563            )
3564            | (
3565                RepairTarget::PageProjection { .. },
3566                RepairWriter::QuarantineStalePageProjection,
3567                RepairMutation::QuarantineStalePageProjection { .. },
3568                [RepairMemoryField::PageProjectionQuarantine],
3569            ) => true,
3570            (
3571                RepairTarget::Page { .. },
3572                RepairWriter::ArchiveEmptySourcePage,
3573                RepairMutation::ArchiveEmptySourcePage {
3574                    before_status,
3575                    after_status,
3576                },
3577                [RepairMemoryField::PageStatus],
3578            ) if before_status == "active" && after_status == "archived" => true,
3579            (
3580                RepairTarget::MemoryEntityLink {
3581                    memory_id,
3582                    entity_id,
3583                    ..
3584                },
3585                RepairWriter::DeleteMemoryEntityLink,
3586                RepairMutation::DeleteMemoryEntityLink {
3587                    memory_id: mutation_memory_id,
3588                    entity_id: mutation_entity_id,
3589                },
3590                [RepairMemoryField::MemoryEntityLink],
3591            ) => memory_id == mutation_memory_id && entity_id == mutation_entity_id,
3592            (
3593                RepairTarget::MemoryEntityExtraction {
3594                    step: RepairEnrichmentStep::EntityExtract,
3595                    entity_ids,
3596                    ..
3597                },
3598                RepairWriter::CompleteEntityExtraction,
3599                RepairMutation::CompleteEntityExtraction {
3600                    entity_ids: mutation_entity_ids,
3601                },
3602                [RepairMemoryField::MemoryEntityLinks, RepairMemoryField::EnrichmentStep],
3603            ) => entity_ids == mutation_entity_ids,
3604            _ => false,
3605        };
3606        if !compatible {
3607            return Err(RepairContractError::UnsupportedWriter);
3608        }
3609        let minimum_schema = match writer {
3610            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction => 6,
3611            RepairWriter::UnstageOrphanRevision
3612            | RepairWriter::DeleteMemoryEntityLink
3613            | RepairWriter::ArchiveEmptySourcePage
3614            | RepairWriter::QuarantineStalePageProjection => 5,
3615            _ => 2,
3616        };
3617        let aggregate_writer = matches!(
3618            writer,
3619            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction
3620        );
3621        let current_semantic_review_writer = writer == RepairWriter::ReclassifyMemory
3622            && manifest_schema_version == REPAIR_MANIFEST_SCHEMA_VERSION;
3623        if manifest_schema_version < minimum_schema
3624            || (aggregate_writer
3625                && (manifest_schema_version != REPAIR_MANIFEST_SCHEMA_VERSION
3626                    || rollback.format_version() != REPAIR_ROLLBACK_FORMAT_VERSION))
3627            || (current_semantic_review_writer
3628                && rollback.format_version() != PREVIOUS_REPAIR_ROLLBACK_FORMAT_VERSION)
3629            || (!aggregate_writer
3630                && !current_semantic_review_writer
3631                && (manifest_schema_version > PREVIOUS_REPAIR_MANIFEST_SCHEMA_VERSION
3632                    || rollback.format_version() != PREVIOUS_REPAIR_ROLLBACK_FORMAT_VERSION))
3633        {
3634            return Err(RepairContractError::UnsupportedWriter);
3635        }
3636        let expected_review_owner_ids = target.review_owner_ids();
3637        let review_binding_valid = match writer {
3638            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction => source
3639                .review_binding()
3640                .zip(expected_review_owner_ids.as_ref())
3641                .is_some_and(|(binding, owner_ids)| binding.owner_ids() == owner_ids),
3642            RepairWriter::ReclassifyMemory
3643                if manifest_schema_version == REPAIR_MANIFEST_SCHEMA_VERSION =>
3644            {
3645                source
3646                    .review_binding()
3647                    .zip(expected_review_owner_ids.as_ref())
3648                    .is_some_and(|(binding, owner_ids)| binding.owner_ids() == owner_ids)
3649            }
3650            _ => source.review_binding().is_none(),
3651        };
3652        if allowed_effects.owner() != &target
3653            || source.check_id() != post_assertions.target_check_id()
3654            || !repair_writer_authorized_for_source(&source, writer)
3655            || source.is_general_only_deterministic()
3656                != post_assertions.verification_policy().is_general_only()
3657            || !review_binding_valid
3658            || source.finding().is_some_and(|finding| {
3659                !finding
3660                    .evidence_ids()
3661                    .contains(post_assertions.target_evidence_id())
3662            })
3663        {
3664            return Err(RepairContractError::InvalidManifest);
3665        }
3666        Ok(Self {
3667            manifest_schema_version,
3668            manifest_id,
3669            prepared_at,
3670            source,
3671            target,
3672            expected_state,
3673            writer,
3674            mutation,
3675            allowed_effects,
3676            rollback,
3677            post_assertions,
3678        })
3679    }
3680
3681    pub fn canonical_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
3682        serde_json::to_vec(self)
3683    }
3684}
3685
3686fn repair_writer_authorized_for_source(source: &RepairSource, writer: RepairWriter) -> bool {
3687    if source.finding().is_some() {
3688        return source.check_id() == REPAIR_CLASSIFICATION_CHECK_ID
3689            && writer == RepairWriter::ReclassifyMemory;
3690    }
3691    matches!(
3692        (source.check_id(), writer),
3693        (
3694            REPAIR_MEMORY_STATE_CHECK_ID,
3695            RepairWriter::NormalizeMemorySourceAgent
3696                | RepairWriter::ClearMemorySupersedes
3697                | RepairWriter::UnstageOrphanRevision
3698        ) | (
3699            REPAIR_SUPERSESSION_CHECK_ID,
3700            RepairWriter::ClearMemorySupersedes | RepairWriter::UnstageOrphanRevision
3701        ) | (REPAIR_TAG_INTEGRITY_CHECK_ID, RepairWriter::DeleteTagRow)
3702            | (
3703                REPAIR_MEMORY_ENTITY_INTEGRITY_CHECK_ID,
3704                RepairWriter::DeleteMemoryEntityLink
3705            )
3706            | (REPAIR_ORPHAN_LABELS_CHECK_ID, RepairWriter::BindPageLink)
3707            | (
3708                REPAIR_PROJECTION_VERSION_CHECK_ID,
3709                RepairWriter::RegeneratePageProjection
3710            )
3711            | (
3712                REPAIR_PROJECTION_IDENTITY_CHECK_ID,
3713                RepairWriter::QuarantineStalePageProjection
3714            )
3715            | (
3716                REPAIR_SOURCE_PAGE_INTEGRITY_CHECK_ID,
3717                RepairWriter::ArchiveEmptySourcePage
3718            )
3719            | (
3720                "pages.duplicate_active_titles",
3721                RepairWriter::RenamePageTitle
3722            )
3723            | (
3724                "memories.enrichment_failures",
3725                RepairWriter::CompleteEntityExtraction
3726            )
3727    )
3728}
3729
3730#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3731pub struct RepairManifest {
3732    #[serde(flatten)]
3733    draft: RepairManifestDraft,
3734    manifest_digest: RepairDigest,
3735}
3736
3737#[derive(Deserialize)]
3738#[serde(deny_unknown_fields)]
3739struct RepairManifestWire {
3740    manifest_schema_version: u16,
3741    manifest_id: String,
3742    prepared_at: i64,
3743    source: RepairSource,
3744    target: RepairTarget,
3745    expected_state: RepairExpectedState,
3746    writer: RepairWriter,
3747    mutation: RepairMutation,
3748    allowed_effects: RepairAllowedEffects,
3749    rollback: RepairRollbackArtifact,
3750    post_assertions: RepairPostAssertions,
3751    manifest_digest: RepairDigest,
3752}
3753
3754impl RepairManifest {
3755    pub fn try_new(
3756        draft: RepairManifestDraft,
3757        manifest_digest: RepairDigest,
3758    ) -> Result<Self, RepairContractError> {
3759        Ok(Self {
3760            draft,
3761            manifest_digest,
3762        })
3763    }
3764
3765    pub fn manifest_id(&self) -> &str {
3766        &self.draft.manifest_id
3767    }
3768
3769    pub const fn manifest_schema_version(&self) -> u16 {
3770        self.draft.manifest_schema_version
3771    }
3772
3773    pub const fn prepared_at(&self) -> i64 {
3774        self.draft.prepared_at
3775    }
3776
3777    pub fn source(&self) -> &RepairSource {
3778        &self.draft.source
3779    }
3780
3781    pub fn target(&self) -> &RepairTarget {
3782        &self.draft.target
3783    }
3784
3785    pub fn expected_state(&self) -> &RepairExpectedState {
3786        &self.draft.expected_state
3787    }
3788
3789    pub const fn writer(&self) -> RepairWriter {
3790        self.draft.writer
3791    }
3792
3793    pub fn mutation(&self) -> &RepairMutation {
3794        &self.draft.mutation
3795    }
3796
3797    pub fn allowed_effects(&self) -> &RepairAllowedEffects {
3798        &self.draft.allowed_effects
3799    }
3800
3801    pub fn rollback(&self) -> &RepairRollbackArtifact {
3802        &self.draft.rollback
3803    }
3804
3805    pub fn post_assertions(&self) -> &RepairPostAssertions {
3806        &self.draft.post_assertions
3807    }
3808
3809    pub fn manifest_digest(&self) -> &RepairDigest {
3810        &self.manifest_digest
3811    }
3812
3813    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
3814        self.draft.canonical_bytes()
3815    }
3816}
3817
3818impl<'de> Deserialize<'de> for RepairManifest {
3819    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3820    where
3821        D: Deserializer<'de>,
3822    {
3823        let wire = RepairManifestWire::deserialize(deserializer)?;
3824        if !matches!(
3825            wire.manifest_schema_version,
3826            2 | 3 | 4 | 5 | REPAIR_MANIFEST_SCHEMA_VERSION
3827        ) {
3828            return Err(D::Error::custom(
3829                RepairContractError::UnsupportedManifestSchema,
3830            ));
3831        }
3832        let draft = RepairManifestDraft::try_new_for_schema(
3833            wire.manifest_schema_version,
3834            wire.manifest_id,
3835            wire.prepared_at,
3836            wire.source,
3837            wire.target,
3838            wire.expected_state,
3839            wire.writer,
3840            wire.mutation,
3841            wire.allowed_effects,
3842            wire.rollback,
3843            wire.post_assertions,
3844        )
3845        .map_err(D::Error::custom)?;
3846        Self::try_new(draft, wire.manifest_digest).map_err(D::Error::custom)
3847    }
3848}
3849
3850fn applicable_deep_complete(report: &LintReport) -> bool {
3851    report
3852        .checks()
3853        .iter()
3854        .find(|check| check.check_id() == REPAIR_CLASSIFICATION_CHECK_ID)
3855        .is_some_and(|check| matches!(check.outcome(), LintOutcome::Pass | LintOutcome::Finding))
3856}
3857
3858#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3859#[serde(tag = "kind", rename_all = "snake_case")]
3860pub enum RepairChoice {
3861    ReclassifyMemory {
3862        selected_finding: LintSemanticFinding,
3863        after_memory_type: MemoryType,
3864    },
3865    RenamePageTitle {
3866        review_id: String,
3867        page_id: String,
3868        before_title: String,
3869        after_title: String,
3870    },
3871    CompleteEntityExtraction {
3872        review_id: String,
3873        memory_id: String,
3874        entity_ids: Vec<String>,
3875    },
3876}
3877
3878#[derive(Deserialize)]
3879#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
3880enum RepairChoiceWire {
3881    ReclassifyMemory {
3882        selected_finding: LintSemanticFinding,
3883        after_memory_type: MemoryType,
3884    },
3885    RenamePageTitle {
3886        review_id: String,
3887        page_id: String,
3888        before_title: String,
3889        after_title: String,
3890    },
3891    CompleteEntityExtraction {
3892        review_id: String,
3893        memory_id: String,
3894        entity_ids: Vec<String>,
3895    },
3896}
3897
3898impl RepairChoice {
3899    pub fn reclassify_memory(
3900        selected_finding: LintSemanticFinding,
3901        after_memory_type: MemoryType,
3902    ) -> Result<Self, RepairContractError> {
3903        if selected_finding.proposed_action() != LintSemanticAction::ReclassifyMemory
3904            || selected_finding.unresolved_disagreement()
3905        {
3906            return Err(RepairContractError::InvalidPrepareRequest);
3907        }
3908        Ok(Self::ReclassifyMemory {
3909            selected_finding,
3910            after_memory_type,
3911        })
3912    }
3913
3914    pub fn rename_page_title(
3915        review_id: String,
3916        page_id: String,
3917        before_title: String,
3918        after_title: String,
3919    ) -> Result<Self, RepairContractError> {
3920        if !valid_nonempty(&review_id)
3921            || !valid_nonempty(&page_id)
3922            || !valid_nonempty(&before_title)
3923            || !valid_nonempty(&after_title)
3924            || before_title == after_title
3925        {
3926            return Err(RepairContractError::InvalidPrepareRequest);
3927        }
3928        Ok(Self::RenamePageTitle {
3929            review_id,
3930            page_id,
3931            before_title,
3932            after_title,
3933        })
3934    }
3935
3936    pub fn complete_entity_extraction(
3937        review_id: String,
3938        memory_id: String,
3939        entity_ids: Vec<String>,
3940    ) -> Result<Self, RepairContractError> {
3941        if !valid_nonempty(&review_id) || !valid_nonempty(&memory_id) {
3942            return Err(RepairContractError::InvalidPrepareRequest);
3943        }
3944        RepairMutation::complete_entity_extraction(entity_ids.clone())
3945            .map_err(|_| RepairContractError::InvalidPrepareRequest)?;
3946        Ok(Self::CompleteEntityExtraction {
3947            review_id,
3948            memory_id,
3949            entity_ids,
3950        })
3951    }
3952
3953    pub fn selected_finding(&self) -> Option<&LintSemanticFinding> {
3954        match self {
3955            Self::ReclassifyMemory {
3956                selected_finding, ..
3957            } => Some(selected_finding),
3958            Self::RenamePageTitle { .. } | Self::CompleteEntityExtraction { .. } => None,
3959        }
3960    }
3961
3962    pub fn after_memory_type(&self) -> Option<&MemoryType> {
3963        match self {
3964            Self::ReclassifyMemory {
3965                after_memory_type, ..
3966            } => Some(after_memory_type),
3967            Self::RenamePageTitle { .. } | Self::CompleteEntityExtraction { .. } => None,
3968        }
3969    }
3970}
3971
3972impl<'de> Deserialize<'de> for RepairChoice {
3973    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3974    where
3975        D: Deserializer<'de>,
3976    {
3977        match RepairChoiceWire::deserialize(deserializer)? {
3978            RepairChoiceWire::ReclassifyMemory {
3979                selected_finding,
3980                after_memory_type,
3981            } => Self::reclassify_memory(selected_finding, after_memory_type),
3982            RepairChoiceWire::RenamePageTitle {
3983                review_id,
3984                page_id,
3985                before_title,
3986                after_title,
3987            } => Self::rename_page_title(review_id, page_id, before_title, after_title),
3988            RepairChoiceWire::CompleteEntityExtraction {
3989                review_id,
3990                memory_id,
3991                entity_ids,
3992            } => Self::complete_entity_extraction(review_id, memory_id, entity_ids),
3993        }
3994        .map_err(D::Error::custom)
3995    }
3996}
3997
3998#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
3999pub struct PrepareRepairRequest {
4000    lint_scope: RepairLintScope,
4001    general_report: LintReport,
4002    #[serde(skip_serializing_if = "Option::is_none")]
4003    deep_report: Option<LintReport>,
4004    choice: RepairChoice,
4005}
4006
4007#[derive(Deserialize)]
4008#[serde(deny_unknown_fields)]
4009struct PrepareRepairRequestWire {
4010    lint_scope: RepairLintScope,
4011    general_report: LintReport,
4012    #[serde(default)]
4013    deep_report: Option<LintReport>,
4014    #[serde(default, deserialize_with = "deserialize_present_optional")]
4015    choice: PresentOptional<RepairChoice>,
4016    #[serde(default, deserialize_with = "deserialize_present_optional")]
4017    selected_finding: PresentOptional<LintSemanticFinding>,
4018    #[serde(default, deserialize_with = "deserialize_present_optional")]
4019    after_memory_type: PresentOptional<MemoryType>,
4020}
4021
4022struct PresentOptional<T> {
4023    present: bool,
4024    value: Option<T>,
4025}
4026
4027impl<T> Default for PresentOptional<T> {
4028    fn default() -> Self {
4029        Self {
4030            present: false,
4031            value: None,
4032        }
4033    }
4034}
4035
4036fn deserialize_present_optional<'de, D, T>(deserializer: D) -> Result<PresentOptional<T>, D::Error>
4037where
4038    D: Deserializer<'de>,
4039    T: Deserialize<'de>,
4040{
4041    Ok(PresentOptional {
4042        present: true,
4043        value: Option::<T>::deserialize(deserializer)?,
4044    })
4045}
4046
4047impl PrepareRepairRequest {
4048    pub fn try_new(
4049        lint_scope: RepairLintScope,
4050        general_report: LintReport,
4051        deep_report: LintReport,
4052        selected_finding: LintSemanticFinding,
4053        after_memory_type: MemoryType,
4054    ) -> Result<Self, RepairContractError> {
4055        let choice = RepairChoice::reclassify_memory(selected_finding, after_memory_type)?;
4056        Self::try_new_with_choice(lint_scope, general_report, Some(deep_report), choice)
4057    }
4058
4059    pub fn try_new_with_choice(
4060        lint_scope: RepairLintScope,
4061        general_report: LintReport,
4062        deep_report: Option<LintReport>,
4063        choice: RepairChoice,
4064    ) -> Result<Self, RepairContractError> {
4065        let general_valid = lint_scope.matches_report_scope_kind(general_report.scope())
4066            && general_report.profile() == LintProfile::General
4067            && general_report.complete();
4068        let deep_valid = deep_report.as_ref().is_none_or(|deep| {
4069            lint_scope.matches_report_scope_kind(deep.scope())
4070                && deep.profile() == LintProfile::Deep
4071                && general_report.scope() == deep.scope()
4072        });
4073        let choice_valid = match &choice {
4074            RepairChoice::ReclassifyMemory { .. } => deep_report
4075                .as_ref()
4076                .is_some_and(|deep| applicable_deep_complete(deep) && deep.agent_work().is_some()),
4077            RepairChoice::RenamePageTitle { .. }
4078            | RepairChoice::CompleteEntityExtraction { .. } => true,
4079        };
4080        if !general_valid || !deep_valid || !choice_valid {
4081            return Err(RepairContractError::InvalidPrepareRequest);
4082        }
4083        Ok(Self {
4084            lint_scope,
4085            general_report,
4086            deep_report,
4087            choice,
4088        })
4089    }
4090
4091    pub fn lint_scope(&self) -> &RepairLintScope {
4092        &self.lint_scope
4093    }
4094
4095    pub fn general_report(&self) -> &LintReport {
4096        &self.general_report
4097    }
4098
4099    pub fn deep_report(&self) -> Option<&LintReport> {
4100        self.deep_report.as_ref()
4101    }
4102
4103    pub fn choice(&self) -> &RepairChoice {
4104        &self.choice
4105    }
4106
4107    pub fn selected_finding(&self) -> Option<&LintSemanticFinding> {
4108        self.choice.selected_finding()
4109    }
4110
4111    pub fn after_memory_type(&self) -> Option<&MemoryType> {
4112        self.choice.after_memory_type()
4113    }
4114}
4115
4116impl<'de> Deserialize<'de> for PrepareRepairRequest {
4117    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4118    where
4119        D: Deserializer<'de>,
4120    {
4121        let wire = PrepareRepairRequestWire::deserialize(deserializer)?;
4122        let choice = match (
4123            wire.choice.present,
4124            wire.choice.value,
4125            wire.selected_finding.present,
4126            wire.selected_finding.value,
4127            wire.after_memory_type.present,
4128            wire.after_memory_type.value,
4129        ) {
4130            (true, Some(choice), false, None, false, None) => choice,
4131            (false, None, true, Some(selected_finding), true, Some(after_memory_type)) => {
4132                RepairChoice::reclassify_memory(selected_finding, after_memory_type)
4133                    .map_err(D::Error::custom)?
4134            }
4135            _ => return Err(D::Error::custom(RepairContractError::InvalidPrepareRequest)),
4136        };
4137        Self::try_new_with_choice(
4138            wire.lint_scope,
4139            wire.general_report,
4140            wire.deep_report,
4141            choice,
4142        )
4143        .map_err(D::Error::custom)
4144    }
4145}
4146
4147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4148/// Intent-binding request for cooperating agent workflows. The exact phrase is
4149/// deliberately not a local authentication or malicious-process boundary.
4150pub struct ApplyRepairRequest {
4151    manifest_id: String,
4152    approved_manifest_digest: RepairDigest,
4153    approval: String,
4154}
4155
4156#[derive(Deserialize)]
4157#[serde(deny_unknown_fields)]
4158struct ApplyRepairRequestWire {
4159    manifest_id: String,
4160    approved_manifest_digest: RepairDigest,
4161    approval: String,
4162}
4163
4164impl ApplyRepairRequest {
4165    pub fn try_new(
4166        manifest_id: String,
4167        approved_manifest_digest: RepairDigest,
4168        approval: String,
4169    ) -> Result<Self, RepairContractError> {
4170        let expected = format!(
4171            "apply repair {} {}",
4172            manifest_id,
4173            approved_manifest_digest.as_str()
4174        );
4175        if !valid_manifest_id(&manifest_id) || approval != expected {
4176            return Err(RepairContractError::InvalidApplyRequest);
4177        }
4178        Ok(Self {
4179            manifest_id,
4180            approved_manifest_digest,
4181            approval,
4182        })
4183    }
4184
4185    pub fn manifest_id(&self) -> &str {
4186        &self.manifest_id
4187    }
4188
4189    pub fn approved_manifest_digest(&self) -> &RepairDigest {
4190        &self.approved_manifest_digest
4191    }
4192
4193    pub fn approval(&self) -> &str {
4194        &self.approval
4195    }
4196}
4197
4198impl<'de> Deserialize<'de> for ApplyRepairRequest {
4199    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4200    where
4201        D: Deserializer<'de>,
4202    {
4203        let wire = ApplyRepairRequestWire::deserialize(deserializer)?;
4204        Self::try_new(
4205            wire.manifest_id,
4206            wire.approved_manifest_digest,
4207            wire.approval,
4208        )
4209        .map_err(D::Error::custom)
4210    }
4211}
4212
4213#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4214pub struct RepairApplyReceiptDraft {
4215    receipt_schema_version: u16,
4216    manifest_id: String,
4217    manifest_digest: RepairDigest,
4218    applied_at: i64,
4219    before_target_receipt: RepairDigest,
4220    after_target_receipt: RepairDigest,
4221    non_target_before: RepairDigest,
4222    non_target_after: RepairDigest,
4223    #[serde(skip_serializing_if = "Option::is_none")]
4224    post_apply_db_digest: Option<RepairDigest>,
4225    actual_effects: RepairAllowedEffects,
4226    writer: RepairWriter,
4227}
4228
4229impl RepairApplyReceiptDraft {
4230    #[allow(clippy::too_many_arguments)]
4231    pub fn try_new(
4232        manifest_id: String,
4233        manifest_digest: RepairDigest,
4234        applied_at: i64,
4235        before_target_receipt: RepairDigest,
4236        after_target_receipt: RepairDigest,
4237        non_target_before: RepairDigest,
4238        non_target_after: RepairDigest,
4239        post_apply_db_digest: RepairDigest,
4240        actual_effects: RepairAllowedEffects,
4241        writer: RepairWriter,
4242    ) -> Result<Self, RepairContractError> {
4243        let receipt_schema_version = match writer {
4244            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction => {
4245                REPAIR_RECEIPT_SCHEMA_VERSION
4246            }
4247            _ => PREVIOUS_REPAIR_RECEIPT_SCHEMA_VERSION,
4248        };
4249        Self::try_new_for_schema(
4250            receipt_schema_version,
4251            manifest_id,
4252            manifest_digest,
4253            applied_at,
4254            before_target_receipt,
4255            after_target_receipt,
4256            non_target_before,
4257            non_target_after,
4258            post_apply_db_digest,
4259            actual_effects,
4260            writer,
4261        )
4262    }
4263
4264    #[allow(clippy::too_many_arguments)]
4265    fn try_new_for_schema(
4266        receipt_schema_version: u16,
4267        manifest_id: String,
4268        manifest_digest: RepairDigest,
4269        applied_at: i64,
4270        before_target_receipt: RepairDigest,
4271        after_target_receipt: RepairDigest,
4272        non_target_before: RepairDigest,
4273        non_target_after: RepairDigest,
4274        post_apply_db_digest: RepairDigest,
4275        actual_effects: RepairAllowedEffects,
4276        writer: RepairWriter,
4277    ) -> Result<Self, RepairContractError> {
4278        if !matches!(
4279            receipt_schema_version,
4280            2 | 3 | 4 | REPAIR_RECEIPT_SCHEMA_VERSION
4281        ) {
4282            return Err(RepairContractError::InvalidReceipt);
4283        }
4284        let aggregate_writer = matches!(
4285            writer,
4286            RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction
4287        );
4288        if !valid_manifest_id(&manifest_id)
4289            || applied_at <= 0
4290            || before_target_receipt == after_target_receipt
4291            || non_target_before != non_target_after
4292            || !repair_receipt_effects_match(writer, &actual_effects)
4293            || receipt_schema_version
4294                < match writer {
4295                    RepairWriter::RenamePageTitle | RepairWriter::CompleteEntityExtraction => 5,
4296                    RepairWriter::UnstageOrphanRevision
4297                    | RepairWriter::DeleteMemoryEntityLink
4298                    | RepairWriter::ArchiveEmptySourcePage
4299                    | RepairWriter::QuarantineStalePageProjection => 4,
4300                    _ => 2,
4301                }
4302            || (aggregate_writer && receipt_schema_version != REPAIR_RECEIPT_SCHEMA_VERSION)
4303            || (!aggregate_writer
4304                && receipt_schema_version > PREVIOUS_REPAIR_RECEIPT_SCHEMA_VERSION)
4305        {
4306            return Err(RepairContractError::InvalidReceipt);
4307        }
4308        Ok(Self {
4309            receipt_schema_version,
4310            manifest_id,
4311            manifest_digest,
4312            applied_at,
4313            before_target_receipt,
4314            after_target_receipt,
4315            non_target_before,
4316            non_target_after,
4317            post_apply_db_digest: Some(post_apply_db_digest),
4318            actual_effects,
4319            writer,
4320        })
4321    }
4322
4323    #[allow(clippy::too_many_arguments)]
4324    fn try_new_legacy_v1(
4325        manifest_id: String,
4326        manifest_digest: RepairDigest,
4327        applied_at: i64,
4328        before_target_receipt: RepairDigest,
4329        after_target_receipt: RepairDigest,
4330        non_target_before: RepairDigest,
4331        non_target_after: RepairDigest,
4332        actual_effects: RepairAllowedEffects,
4333        writer: RepairWriter,
4334    ) -> Result<Self, RepairContractError> {
4335        if !valid_manifest_id(&manifest_id)
4336            || applied_at <= 0
4337            || before_target_receipt == after_target_receipt
4338            || non_target_before != non_target_after
4339            || writer != RepairWriter::ReclassifyMemory
4340        {
4341            return Err(RepairContractError::InvalidReceipt);
4342        }
4343        Ok(Self {
4344            receipt_schema_version: 1,
4345            manifest_id,
4346            manifest_digest,
4347            applied_at,
4348            before_target_receipt,
4349            after_target_receipt,
4350            non_target_before,
4351            non_target_after,
4352            post_apply_db_digest: None,
4353            actual_effects,
4354            writer,
4355        })
4356    }
4357
4358    pub fn canonical_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
4359        serde_json::to_vec(self)
4360    }
4361}
4362
4363fn repair_receipt_effects_match(
4364    writer: RepairWriter,
4365    actual_effects: &RepairAllowedEffects,
4366) -> bool {
4367    matches!(
4368        (actual_effects.owner(), writer, actual_effects.fields()),
4369        (
4370            RepairTarget::Memory { .. },
4371            RepairWriter::ReclassifyMemory,
4372            [RepairMemoryField::MemoryType]
4373        ) | (
4374            RepairTarget::Memory { .. },
4375            RepairWriter::NormalizeMemorySourceAgent,
4376            [RepairMemoryField::SourceAgent]
4377        ) | (
4378            RepairTarget::Memory { .. },
4379            RepairWriter::ClearMemorySupersedes,
4380            [RepairMemoryField::Supersedes]
4381        ) | (
4382            RepairTarget::Memory { .. },
4383            RepairWriter::UnstageOrphanRevision,
4384            [RepairMemoryField::PendingRevision]
4385        ) | (
4386            RepairTarget::Tag { .. },
4387            RepairWriter::DeleteTagRow,
4388            [RepairMemoryField::TagRow]
4389        ) | (
4390            RepairTarget::MemoryEntityLink { .. },
4391            RepairWriter::DeleteMemoryEntityLink,
4392            [RepairMemoryField::MemoryEntityLink]
4393        ) | (
4394            RepairTarget::PageLink { .. },
4395            RepairWriter::BindPageLink,
4396            [RepairMemoryField::TargetPageId]
4397        ) | (
4398            RepairTarget::PageProjection { .. },
4399            RepairWriter::RegeneratePageProjection,
4400            [RepairMemoryField::PageProjection]
4401        ) | (
4402            RepairTarget::PageProjection { .. },
4403            RepairWriter::QuarantineStalePageProjection,
4404            [RepairMemoryField::PageProjectionQuarantine]
4405        ) | (
4406            RepairTarget::Page { .. },
4407            RepairWriter::ArchiveEmptySourcePage,
4408            [RepairMemoryField::PageStatus]
4409        ) | (
4410            RepairTarget::PageProjection { .. },
4411            RepairWriter::RenamePageTitle,
4412            [
4413                RepairMemoryField::PageTitle,
4414                RepairMemoryField::PageVersion,
4415                RepairMemoryField::PageEmbedding,
4416                RepairMemoryField::PageProjection,
4417            ]
4418        ) | (
4419            RepairTarget::MemoryEntityExtraction { .. },
4420            RepairWriter::CompleteEntityExtraction,
4421            [
4422                RepairMemoryField::MemoryEntityLinks,
4423                RepairMemoryField::EnrichmentStep,
4424            ]
4425        )
4426    )
4427}
4428
4429#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4430pub struct RepairApplyReceipt {
4431    #[serde(flatten)]
4432    draft: RepairApplyReceiptDraft,
4433    receipt_digest: RepairDigest,
4434}
4435
4436#[derive(Deserialize)]
4437#[serde(deny_unknown_fields)]
4438struct RepairApplyReceiptWire {
4439    receipt_schema_version: u16,
4440    manifest_id: String,
4441    manifest_digest: RepairDigest,
4442    applied_at: i64,
4443    before_target_receipt: RepairDigest,
4444    after_target_receipt: RepairDigest,
4445    non_target_before: RepairDigest,
4446    non_target_after: RepairDigest,
4447    #[serde(default)]
4448    post_apply_db_digest: Option<RepairDigest>,
4449    actual_effects: RepairAllowedEffects,
4450    writer: RepairWriter,
4451    receipt_digest: RepairDigest,
4452}
4453
4454impl RepairApplyReceipt {
4455    #[allow(clippy::too_many_arguments)]
4456    pub fn try_new(
4457        manifest_id: String,
4458        manifest_digest: RepairDigest,
4459        applied_at: i64,
4460        before_target_receipt: RepairDigest,
4461        after_target_receipt: RepairDigest,
4462        non_target_before: RepairDigest,
4463        non_target_after: RepairDigest,
4464        post_apply_db_digest: RepairDigest,
4465        actual_effects: RepairAllowedEffects,
4466        writer: RepairWriter,
4467        receipt_digest: RepairDigest,
4468    ) -> Result<Self, RepairContractError> {
4469        let draft = RepairApplyReceiptDraft::try_new(
4470            manifest_id,
4471            manifest_digest,
4472            applied_at,
4473            before_target_receipt,
4474            after_target_receipt,
4475            non_target_before,
4476            non_target_after,
4477            post_apply_db_digest,
4478            actual_effects,
4479            writer,
4480        )?;
4481        Ok(Self::from_draft(draft, receipt_digest))
4482    }
4483
4484    pub fn from_draft(draft: RepairApplyReceiptDraft, receipt_digest: RepairDigest) -> Self {
4485        Self {
4486            draft,
4487            receipt_digest,
4488        }
4489    }
4490
4491    pub fn manifest_id(&self) -> &str {
4492        &self.draft.manifest_id
4493    }
4494
4495    pub fn manifest_digest(&self) -> &RepairDigest {
4496        &self.draft.manifest_digest
4497    }
4498
4499    pub fn receipt_digest(&self) -> &RepairDigest {
4500        &self.receipt_digest
4501    }
4502
4503    pub fn actual_effects(&self) -> &RepairAllowedEffects {
4504        &self.draft.actual_effects
4505    }
4506
4507    pub fn before_target_receipt(&self) -> &RepairDigest {
4508        &self.draft.before_target_receipt
4509    }
4510
4511    pub fn after_target_receipt(&self) -> &RepairDigest {
4512        &self.draft.after_target_receipt
4513    }
4514
4515    pub fn non_target_before(&self) -> &RepairDigest {
4516        &self.draft.non_target_before
4517    }
4518
4519    pub fn non_target_after(&self) -> &RepairDigest {
4520        &self.draft.non_target_after
4521    }
4522
4523    pub fn post_apply_db_digest(&self) -> Option<&RepairDigest> {
4524        self.draft.post_apply_db_digest.as_ref()
4525    }
4526
4527    pub const fn writer(&self) -> RepairWriter {
4528        self.draft.writer
4529    }
4530
4531    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
4532        self.draft.canonical_bytes()
4533    }
4534}
4535
4536impl<'de> Deserialize<'de> for RepairApplyReceipt {
4537    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4538    where
4539        D: Deserializer<'de>,
4540    {
4541        let wire = RepairApplyReceiptWire::deserialize(deserializer)?;
4542        let draft = match wire.receipt_schema_version {
4543            1 if wire.post_apply_db_digest.is_none() => RepairApplyReceiptDraft::try_new_legacy_v1(
4544                wire.manifest_id,
4545                wire.manifest_digest,
4546                wire.applied_at,
4547                wire.before_target_receipt,
4548                wire.after_target_receipt,
4549                wire.non_target_before,
4550                wire.non_target_after,
4551                wire.actual_effects,
4552                wire.writer,
4553            ),
4554            2 | 3 | 4 | REPAIR_RECEIPT_SCHEMA_VERSION => {
4555                RepairApplyReceiptDraft::try_new_for_schema(
4556                    wire.receipt_schema_version,
4557                    wire.manifest_id,
4558                    wire.manifest_digest,
4559                    wire.applied_at,
4560                    wire.before_target_receipt,
4561                    wire.after_target_receipt,
4562                    wire.non_target_before,
4563                    wire.non_target_after,
4564                    wire.post_apply_db_digest
4565                        .ok_or(RepairContractError::InvalidReceipt)
4566                        .map_err(D::Error::custom)?,
4567                    wire.actual_effects,
4568                    wire.writer,
4569                )
4570            }
4571            _ => Err(RepairContractError::InvalidReceipt),
4572        }
4573        .map_err(D::Error::custom)?;
4574        Ok(Self::from_draft(draft, wire.receipt_digest))
4575    }
4576}
4577
4578#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4579pub struct VerifyRepairRequest {
4580    manifest_id: String,
4581    manifest_digest: RepairDigest,
4582    apply_receipt_digest: RepairDigest,
4583    general_report: LintReport,
4584    #[serde(skip_serializing_if = "Option::is_none")]
4585    deep_report: Option<LintReport>,
4586    #[serde(skip_serializing_if = "Option::is_none")]
4587    next_apply: Option<ApplyRepairRequest>,
4588}
4589
4590#[derive(Deserialize)]
4591#[serde(deny_unknown_fields)]
4592struct VerifyRepairRequestWire {
4593    manifest_id: String,
4594    manifest_digest: RepairDigest,
4595    apply_receipt_digest: RepairDigest,
4596    general_report: LintReport,
4597    #[serde(default)]
4598    deep_report: Option<LintReport>,
4599    #[serde(default)]
4600    next_apply: Option<ApplyRepairRequest>,
4601}
4602
4603impl VerifyRepairRequest {
4604    pub fn try_new(
4605        manifest_id: String,
4606        manifest_digest: RepairDigest,
4607        apply_receipt_digest: RepairDigest,
4608        general_report: LintReport,
4609        deep_report: LintReport,
4610    ) -> Result<Self, RepairContractError> {
4611        Self::try_new_deep_backed(
4612            manifest_id,
4613            manifest_digest,
4614            apply_receipt_digest,
4615            general_report,
4616            deep_report,
4617        )
4618    }
4619
4620    pub fn try_new_deep_backed(
4621        manifest_id: String,
4622        manifest_digest: RepairDigest,
4623        apply_receipt_digest: RepairDigest,
4624        general_report: LintReport,
4625        deep_report: LintReport,
4626    ) -> Result<Self, RepairContractError> {
4627        Self::try_new_with_next_apply(
4628            manifest_id,
4629            manifest_digest,
4630            apply_receipt_digest,
4631            general_report,
4632            deep_report,
4633            None,
4634        )
4635    }
4636
4637    pub fn try_new_general_only(
4638        manifest_id: String,
4639        manifest_digest: RepairDigest,
4640        apply_receipt_digest: RepairDigest,
4641        general_report: LintReport,
4642    ) -> Result<Self, RepairContractError> {
4643        Self::try_new_general_only_with_next_apply(
4644            manifest_id,
4645            manifest_digest,
4646            apply_receipt_digest,
4647            general_report,
4648            None,
4649        )
4650    }
4651
4652    pub fn try_new_with_next_apply(
4653        manifest_id: String,
4654        manifest_digest: RepairDigest,
4655        apply_receipt_digest: RepairDigest,
4656        general_report: LintReport,
4657        deep_report: LintReport,
4658        next_apply: Option<ApplyRepairRequest>,
4659    ) -> Result<Self, RepairContractError> {
4660        Self::try_new_with_optional_deep_and_next_apply(
4661            manifest_id,
4662            manifest_digest,
4663            apply_receipt_digest,
4664            general_report,
4665            Some(deep_report),
4666            next_apply,
4667        )
4668    }
4669
4670    pub fn try_new_general_only_with_next_apply(
4671        manifest_id: String,
4672        manifest_digest: RepairDigest,
4673        apply_receipt_digest: RepairDigest,
4674        general_report: LintReport,
4675        next_apply: Option<ApplyRepairRequest>,
4676    ) -> Result<Self, RepairContractError> {
4677        Self::try_new_with_optional_deep_and_next_apply(
4678            manifest_id,
4679            manifest_digest,
4680            apply_receipt_digest,
4681            general_report,
4682            None,
4683            next_apply,
4684        )
4685    }
4686
4687    pub fn try_new_with_optional_deep_and_next_apply(
4688        manifest_id: String,
4689        manifest_digest: RepairDigest,
4690        apply_receipt_digest: RepairDigest,
4691        general_report: LintReport,
4692        deep_report: Option<LintReport>,
4693        next_apply: Option<ApplyRepairRequest>,
4694    ) -> Result<Self, RepairContractError> {
4695        if !valid_manifest_id(&manifest_id)
4696            || general_report.profile() != LintProfile::General
4697            || !general_report.complete()
4698            || deep_report.as_ref().is_some_and(|deep| {
4699                deep.profile() != LintProfile::Deep
4700                    || general_report.scope() != deep.scope()
4701                    || general_report.producer_receipt() != deep.producer_receipt()
4702            })
4703            || next_apply
4704                .as_ref()
4705                .is_some_and(|next| next.manifest_id() == manifest_id)
4706        {
4707            return Err(RepairContractError::InvalidVerifyRequest);
4708        }
4709        Ok(Self {
4710            manifest_id,
4711            manifest_digest,
4712            apply_receipt_digest,
4713            general_report,
4714            deep_report,
4715            next_apply,
4716        })
4717    }
4718
4719    pub fn manifest_id(&self) -> &str {
4720        &self.manifest_id
4721    }
4722
4723    pub fn manifest_digest(&self) -> &RepairDigest {
4724        &self.manifest_digest
4725    }
4726
4727    pub fn apply_receipt_digest(&self) -> &RepairDigest {
4728        &self.apply_receipt_digest
4729    }
4730
4731    pub fn general_report(&self) -> &LintReport {
4732        &self.general_report
4733    }
4734
4735    pub fn deep_report(&self) -> Option<&LintReport> {
4736        self.deep_report.as_ref()
4737    }
4738
4739    pub fn next_apply(&self) -> Option<&ApplyRepairRequest> {
4740        self.next_apply.as_ref()
4741    }
4742}
4743
4744impl<'de> Deserialize<'de> for VerifyRepairRequest {
4745    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4746    where
4747        D: Deserializer<'de>,
4748    {
4749        let wire = VerifyRepairRequestWire::deserialize(deserializer)?;
4750        Self::try_new_with_optional_deep_and_next_apply(
4751            wire.manifest_id,
4752            wire.manifest_digest,
4753            wire.apply_receipt_digest,
4754            wire.general_report,
4755            wire.deep_report,
4756            wire.next_apply,
4757        )
4758        .map_err(D::Error::custom)
4759    }
4760}
4761
4762#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4763pub struct RepairVerificationReceiptDraft {
4764    receipt_schema_version: u16,
4765    manifest_id: String,
4766    manifest_digest: RepairDigest,
4767    apply_receipt_digest: RepairDigest,
4768    verified_at: i64,
4769    general_snapshots: LintSnapshotReceipts,
4770    #[serde(skip_serializing_if = "Option::is_none")]
4771    deep_snapshots: Option<LintSnapshotReceipts>,
4772}
4773
4774impl RepairVerificationReceiptDraft {
4775    #[allow(clippy::too_many_arguments)]
4776    pub fn try_new(
4777        manifest_id: String,
4778        manifest_digest: RepairDigest,
4779        apply_receipt_digest: RepairDigest,
4780        verified_at: i64,
4781        general_snapshots: LintSnapshotReceipts,
4782        deep_snapshots: LintSnapshotReceipts,
4783    ) -> Result<Self, RepairContractError> {
4784        Self::try_new_for_schema(
4785            REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION,
4786            manifest_id,
4787            manifest_digest,
4788            apply_receipt_digest,
4789            verified_at,
4790            general_snapshots,
4791            Some(deep_snapshots),
4792        )
4793    }
4794
4795    pub fn try_new_general_only(
4796        manifest_id: String,
4797        manifest_digest: RepairDigest,
4798        apply_receipt_digest: RepairDigest,
4799        verified_at: i64,
4800        general_snapshots: LintSnapshotReceipts,
4801    ) -> Result<Self, RepairContractError> {
4802        Self::try_new_for_schema(
4803            REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION,
4804            manifest_id,
4805            manifest_digest,
4806            apply_receipt_digest,
4807            verified_at,
4808            general_snapshots,
4809            None,
4810        )
4811    }
4812
4813    #[allow(clippy::too_many_arguments)]
4814    fn try_new_for_schema(
4815        receipt_schema_version: u16,
4816        manifest_id: String,
4817        manifest_digest: RepairDigest,
4818        apply_receipt_digest: RepairDigest,
4819        verified_at: i64,
4820        general_snapshots: LintSnapshotReceipts,
4821        deep_snapshots: Option<LintSnapshotReceipts>,
4822    ) -> Result<Self, RepairContractError> {
4823        if !matches!(
4824            receipt_schema_version,
4825            2 | 3 | REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION
4826        ) || (receipt_schema_version == 2 && deep_snapshots.is_none())
4827        {
4828            return Err(RepairContractError::InvalidReceipt);
4829        }
4830        if !valid_manifest_id(&manifest_id) || verified_at <= 0 {
4831            return Err(RepairContractError::InvalidReceipt);
4832        }
4833        Ok(Self {
4834            receipt_schema_version,
4835            manifest_id,
4836            manifest_digest,
4837            apply_receipt_digest,
4838            verified_at,
4839            general_snapshots,
4840            deep_snapshots,
4841        })
4842    }
4843
4844    #[allow(clippy::too_many_arguments)]
4845    fn try_new_legacy_v1(
4846        manifest_id: String,
4847        manifest_digest: RepairDigest,
4848        apply_receipt_digest: RepairDigest,
4849        verified_at: i64,
4850        general_snapshots: LintSnapshotReceipts,
4851        deep_snapshots: LintSnapshotReceipts,
4852    ) -> Result<Self, RepairContractError> {
4853        if !valid_manifest_id(&manifest_id) || verified_at <= 0 {
4854            return Err(RepairContractError::InvalidReceipt);
4855        }
4856        Ok(Self {
4857            receipt_schema_version: 1,
4858            manifest_id,
4859            manifest_digest,
4860            apply_receipt_digest,
4861            verified_at,
4862            general_snapshots,
4863            deep_snapshots: Some(deep_snapshots),
4864        })
4865    }
4866
4867    pub fn canonical_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
4868        serde_json::to_vec(self)
4869    }
4870}
4871
4872#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4873pub struct RepairVerificationReceipt {
4874    #[serde(flatten)]
4875    draft: RepairVerificationReceiptDraft,
4876    receipt_digest: RepairDigest,
4877}
4878
4879#[derive(Deserialize)]
4880#[serde(deny_unknown_fields)]
4881struct RepairVerificationReceiptWire {
4882    receipt_schema_version: u16,
4883    manifest_id: String,
4884    manifest_digest: RepairDigest,
4885    apply_receipt_digest: RepairDigest,
4886    verified_at: i64,
4887    general_snapshots: LintSnapshotReceipts,
4888    #[serde(default)]
4889    deep_snapshots: Option<LintSnapshotReceipts>,
4890    receipt_digest: RepairDigest,
4891}
4892
4893impl RepairVerificationReceipt {
4894    #[allow(clippy::too_many_arguments)]
4895    pub fn try_new(
4896        manifest_id: String,
4897        manifest_digest: RepairDigest,
4898        apply_receipt_digest: RepairDigest,
4899        verified_at: i64,
4900        general_snapshots: LintSnapshotReceipts,
4901        deep_snapshots: LintSnapshotReceipts,
4902        receipt_digest: RepairDigest,
4903    ) -> Result<Self, RepairContractError> {
4904        let draft = RepairVerificationReceiptDraft::try_new(
4905            manifest_id,
4906            manifest_digest,
4907            apply_receipt_digest,
4908            verified_at,
4909            general_snapshots,
4910            deep_snapshots,
4911        )?;
4912        Ok(Self::from_draft(draft, receipt_digest))
4913    }
4914
4915    #[allow(clippy::too_many_arguments)]
4916    pub fn try_new_general_only(
4917        manifest_id: String,
4918        manifest_digest: RepairDigest,
4919        apply_receipt_digest: RepairDigest,
4920        verified_at: i64,
4921        general_snapshots: LintSnapshotReceipts,
4922        receipt_digest: RepairDigest,
4923    ) -> Result<Self, RepairContractError> {
4924        let draft = RepairVerificationReceiptDraft::try_new_general_only(
4925            manifest_id,
4926            manifest_digest,
4927            apply_receipt_digest,
4928            verified_at,
4929            general_snapshots,
4930        )?;
4931        Ok(Self::from_draft(draft, receipt_digest))
4932    }
4933
4934    pub fn from_draft(draft: RepairVerificationReceiptDraft, receipt_digest: RepairDigest) -> Self {
4935        Self {
4936            draft,
4937            receipt_digest,
4938        }
4939    }
4940
4941    pub fn receipt_digest(&self) -> &RepairDigest {
4942        &self.receipt_digest
4943    }
4944
4945    pub fn manifest_id(&self) -> &str {
4946        &self.draft.manifest_id
4947    }
4948
4949    pub fn manifest_digest(&self) -> &RepairDigest {
4950        &self.draft.manifest_digest
4951    }
4952
4953    pub fn apply_receipt_digest(&self) -> &RepairDigest {
4954        &self.draft.apply_receipt_digest
4955    }
4956
4957    pub fn general_snapshots(&self) -> &LintSnapshotReceipts {
4958        &self.draft.general_snapshots
4959    }
4960
4961    pub fn deep_snapshots(&self) -> Option<&LintSnapshotReceipts> {
4962        self.draft.deep_snapshots.as_ref()
4963    }
4964
4965    pub fn canonical_unsigned_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
4966        self.draft.canonical_bytes()
4967    }
4968}
4969
4970impl<'de> Deserialize<'de> for RepairVerificationReceipt {
4971    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4972    where
4973        D: Deserializer<'de>,
4974    {
4975        let wire = RepairVerificationReceiptWire::deserialize(deserializer)?;
4976        let draft = match wire.receipt_schema_version {
4977            1 => RepairVerificationReceiptDraft::try_new_legacy_v1(
4978                wire.manifest_id,
4979                wire.manifest_digest,
4980                wire.apply_receipt_digest,
4981                wire.verified_at,
4982                wire.general_snapshots,
4983                wire.deep_snapshots
4984                    .ok_or(RepairContractError::InvalidReceipt)
4985                    .map_err(D::Error::custom)?,
4986            ),
4987            2 => RepairVerificationReceiptDraft::try_new_for_schema(
4988                2,
4989                wire.manifest_id,
4990                wire.manifest_digest,
4991                wire.apply_receipt_digest,
4992                wire.verified_at,
4993                wire.general_snapshots,
4994                wire.deep_snapshots,
4995            ),
4996            3 => RepairVerificationReceiptDraft::try_new_for_schema(
4997                3,
4998                wire.manifest_id,
4999                wire.manifest_digest,
5000                wire.apply_receipt_digest,
5001                wire.verified_at,
5002                wire.general_snapshots,
5003                wire.deep_snapshots,
5004            ),
5005            REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION => {
5006                RepairVerificationReceiptDraft::try_new_for_schema(
5007                    REPAIR_VERIFICATION_RECEIPT_SCHEMA_VERSION,
5008                    wire.manifest_id,
5009                    wire.manifest_digest,
5010                    wire.apply_receipt_digest,
5011                    wire.verified_at,
5012                    wire.general_snapshots,
5013                    wire.deep_snapshots,
5014                )
5015            }
5016            _ => Err(RepairContractError::InvalidReceipt),
5017        }
5018        .map_err(D::Error::custom)?;
5019        Ok(Self::from_draft(draft, wire.receipt_digest))
5020    }
5021}