1mod 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)]
4148pub 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}