1use crate::branches::DEFAULT_BRANCH;
10use crate::model::*;
11use lex_ast::{sig_id, stage_id, Stage};
12use serde::de::DeserializeOwned;
13use serde::Serialize;
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::time::{SystemTime, UNIX_EPOCH};
17
18#[derive(Debug, thiserror::Error)]
19pub enum StoreError {
20 #[error("io error: {0}")]
21 Io(#[from] std::io::Error),
22 #[error("serialization error: {0}")]
23 Serde(#[from] serde_json::Error),
24 #[error("imports cannot be published as stages")]
25 CannotPublishImport,
26 #[error("unknown stage_id `{0}`")]
27 UnknownStage(String),
28 #[error("unknown sig_id `{0}`")]
29 UnknownSig(String),
30 #[error("invalid lifecycle transition: {0}")]
31 InvalidTransition(String),
32 #[error("unknown branch `{0}`")]
33 UnknownBranch(String),
34 #[error(transparent)]
35 Apply(#[from] lex_vcs::ApplyError),
36 #[error("type errors in published program: {} error(s)", .0.len())]
42 TypeError(Vec<lex_types::TypeError>),
43}
44
45#[derive(Debug, Clone, serde::Serialize)]
47pub struct PublishOutcome {
48 pub ops: Vec<PublishOp>,
49 pub head_op: Option<lex_vcs::OpId>,
50}
51
52#[derive(Debug, Clone, serde::Serialize)]
54pub struct PublishOp {
55 pub op_id: lex_vcs::OpId,
56 pub kind: serde_json::Value,
57}
58
59#[derive(Debug, Clone, serde::Serialize, PartialEq)]
63pub struct StageHistoryEntry {
64 pub stage_id: String,
65 pub status: StageStatus,
66 pub last_at: u64,
68 #[serde(skip_serializing_if = "Option::is_none")]
74 pub published_at: Option<u64>,
75}
76
77pub struct Store {
78 root: PathBuf,
79}
80
81impl Store {
82 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
84 let root = root.as_ref().to_path_buf();
85 fs::create_dir_all(root.join("stages"))?;
86 fs::create_dir_all(root.join("traces"))?;
87 Ok(Self { root })
88 }
89
90 pub fn root(&self) -> &Path { &self.root }
91
92 fn now() -> u64 {
93 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
94 }
95
96 fn sig_dir(&self, sig: &str) -> PathBuf { self.root.join("stages").join(sig) }
97 fn impl_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("implementations") }
98 fn tests_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("tests") }
99 fn specs_dir(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("specs") }
100 fn lifecycle_path(&self, sig: &str) -> PathBuf { self.sig_dir(sig).join("lifecycle.json") }
101
102 pub fn publish(&self, stage: &Stage) -> Result<String, StoreError> {
108 let sig = sig_id(stage).ok_or(StoreError::CannotPublishImport)?;
109 let stage_id = stage_id(stage).ok_or(StoreError::CannotPublishImport)?;
110 let name = stage_name(stage).to_string();
111
112 fs::create_dir_all(self.impl_dir(&sig))?;
113 fs::create_dir_all(self.tests_dir(&sig))?;
114 fs::create_dir_all(self.specs_dir(&sig))?;
115
116 let ast_path = self.impl_dir(&sig).join(format!("{}.ast.json", stage_id));
117 let meta_path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
118
119 if !ast_path.exists() {
120 write_canonical_json(&ast_path, stage)?;
121 }
122 if !meta_path.exists() {
123 let metadata = Metadata {
124 stage_id: stage_id.clone(),
125 sig_id: sig.clone(),
126 name,
127 published_at: Self::now(),
128 note: None,
129 };
130 write_canonical_json(&meta_path, &metadata)?;
131 }
132
133 let mut life = self.read_lifecycle(&sig).unwrap_or_else(|_| Lifecycle {
135 sig_id: sig.clone(),
136 ..Default::default()
137 });
138 if !life.transitions.iter().any(|t| t.stage_id == stage_id) {
139 life.transitions.push(Transition {
140 stage_id: stage_id.clone(),
141 from: StageStatus::Draft, to: StageStatus::Draft,
143 at: Self::now(),
144 reason: None,
145 });
146 self.write_lifecycle(&sig, &life)?;
147 }
148 Ok(stage_id)
149 }
150
151 pub fn activate(&self, stage_id: &str) -> Result<(), StoreError> {
154 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
155 let active = life.current_active().map(|s| s.to_string());
157 if let Some(prev) = active {
158 if prev != stage_id {
159 life.transitions.push(Transition {
160 stage_id: prev,
161 from: StageStatus::Active,
162 to: StageStatus::Deprecated,
163 at: Self::now(),
164 reason: Some("superseded".into()),
165 });
166 }
167 }
168 let cur = life.status_of(stage_id);
169 if cur == Some(StageStatus::Tombstone) {
170 return Err(StoreError::InvalidTransition("tombstoned cannot be activated".into()));
171 }
172 life.transitions.push(Transition {
173 stage_id: stage_id.into(),
174 from: cur.unwrap_or(StageStatus::Draft),
175 to: StageStatus::Active,
176 at: Self::now(),
177 reason: None,
178 });
179 self.write_lifecycle(&sig, &life)
180 }
181
182 pub fn deprecate(&self, stage_id: &str, reason: impl Into<String>) -> Result<(), StoreError> {
183 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
184 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
185 if cur != StageStatus::Active {
186 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Deprecated")));
187 }
188 life.transitions.push(Transition {
189 stage_id: stage_id.into(),
190 from: cur,
191 to: StageStatus::Deprecated,
192 at: Self::now(),
193 reason: Some(reason.into()),
194 });
195 self.write_lifecycle(&sig, &life)
196 }
197
198 pub fn tombstone(&self, stage_id: &str) -> Result<(), StoreError> {
199 let (sig, mut life) = self.lookup_lifecycle(stage_id)?;
200 let cur = life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))?;
201 if cur != StageStatus::Deprecated {
202 return Err(StoreError::InvalidTransition(format!("{cur:?} ⇒ Tombstone")));
203 }
204 life.transitions.push(Transition {
205 stage_id: stage_id.into(),
206 from: cur,
207 to: StageStatus::Tombstone,
208 at: Self::now(),
209 reason: None,
210 });
211 self.write_lifecycle(&sig, &life)
212 }
213
214 pub fn resolve_sig(&self, sig: &str) -> Result<Option<String>, StoreError> {
218 let life = match self.read_lifecycle(sig) {
219 Ok(l) => l,
220 Err(_) => return Ok(None),
221 };
222 Ok(life.current_active().map(|s| s.to_string()))
223 }
224
225 pub fn sig_history(&self, sig: &str) -> Result<Vec<StageHistoryEntry>, StoreError> {
232 let life = match self.read_lifecycle(sig) {
233 Ok(l) => l,
234 Err(_) => return Ok(Vec::new()),
235 };
236 let mut by_stage: indexmap::IndexMap<String, StageHistoryEntry> =
240 indexmap::IndexMap::new();
241 for t in &life.transitions {
242 let entry = by_stage.entry(t.stage_id.clone()).or_insert(StageHistoryEntry {
243 stage_id: t.stage_id.clone(),
244 status: t.to,
245 last_at: t.at,
246 published_at: None,
247 });
248 entry.status = t.to;
249 entry.last_at = t.at;
250 if t.from == StageStatus::Draft && entry.published_at.is_none() {
251 entry.published_at = Some(t.at);
252 }
253 if t.to == StageStatus::Draft && entry.published_at.is_none() {
254 entry.published_at = Some(t.at);
256 }
257 }
258 let mut out: Vec<StageHistoryEntry> = by_stage.into_values().collect();
259 out.sort_by_key(|e| std::cmp::Reverse(e.last_at));
261 Ok(out)
262 }
263
264 pub fn get_ast(&self, stage_id: &str) -> Result<Stage, StoreError> {
265 let (sig, _) = self.lookup_lifecycle(stage_id)?;
266 let path = self.impl_dir(&sig).join(format!("{}.ast.json", stage_id));
267 let bytes = fs::read(&path)?;
268 Ok(serde_json::from_slice(&bytes)?)
269 }
270
271 pub fn get_metadata(&self, stage_id: &str) -> Result<Metadata, StoreError> {
272 let (sig, _) = self.lookup_lifecycle(stage_id)?;
273 let path = self.impl_dir(&sig).join(format!("{}.metadata.json", stage_id));
274 let bytes = fs::read(&path)?;
275 Ok(serde_json::from_slice(&bytes)?)
276 }
277
278 pub fn get_status(&self, stage_id: &str) -> Result<StageStatus, StoreError> {
279 let (_sig, life) = self.lookup_lifecycle(stage_id)?;
280 life.status_of(stage_id).ok_or_else(|| StoreError::UnknownStage(stage_id.into()))
281 }
282
283 pub fn list_stages_by_name(&self, name: &str) -> Result<Vec<String>, StoreError> {
284 let mut out = Vec::new();
287 let stages_dir = self.root.join("stages");
288 if !stages_dir.exists() { return Ok(out); }
289 for entry in fs::read_dir(&stages_dir)? {
290 let entry = entry?;
291 let sig_dir = entry.path();
292 if !sig_dir.is_dir() { continue; }
293 let sig = entry.file_name().to_string_lossy().to_string();
294 let impls = self.impl_dir(&sig);
296 if !impls.exists() { continue; }
297 for f in fs::read_dir(impls)? {
298 let f = f?;
299 let p = f.path();
300 if p.extension().is_some_and(|e| e == "json")
301 && p.file_name().is_some_and(|n| n.to_string_lossy().ends_with(".metadata.json"))
302 {
303 if let Ok(bytes) = fs::read(&p) {
304 if let Ok(m) = serde_json::from_slice::<Metadata>(&bytes) {
305 if m.name == name {
306 if !out.contains(&sig) { out.push(sig.clone()); }
307 break;
308 }
309 }
310 }
311 }
312 }
313 }
314 out.sort();
315 Ok(out)
316 }
317
318 pub fn list_sigs(&self) -> Result<Vec<String>, StoreError> {
319 let stages_dir = self.root.join("stages");
320 let mut out = Vec::new();
321 if !stages_dir.exists() { return Ok(out); }
322 for entry in fs::read_dir(stages_dir)? {
323 let entry = entry?;
324 if entry.file_type()?.is_dir() {
325 out.push(entry.file_name().to_string_lossy().to_string());
326 }
327 }
328 out.sort();
329 Ok(out)
330 }
331
332 pub fn attach_test(&self, sig: &str, test: &Test) -> Result<String, StoreError> {
335 if !self.sig_dir(sig).exists() {
336 return Err(StoreError::UnknownSig(sig.into()));
337 }
338 fs::create_dir_all(self.tests_dir(sig))?;
339 let path = self.tests_dir(sig).join(format!("{}.json", test.id));
340 write_canonical_json(&path, test)?;
341 Ok(test.id.clone())
342 }
343
344 pub fn list_tests(&self, sig: &str) -> Result<Vec<Test>, StoreError> {
345 let dir = self.tests_dir(sig);
346 if !dir.exists() { return Ok(Vec::new()); }
347 let mut out = Vec::new();
348 for f in fs::read_dir(dir)? {
349 let f = f?;
350 if f.path().extension().is_some_and(|e| e == "json") {
351 let bytes = fs::read(f.path())?;
352 out.push(serde_json::from_slice(&bytes)?);
353 }
354 }
355 Ok(out)
356 }
357
358 pub fn attach_spec(&self, sig: &str, spec: &Spec) -> Result<String, StoreError> {
359 if !self.sig_dir(sig).exists() {
360 return Err(StoreError::UnknownSig(sig.into()));
361 }
362 fs::create_dir_all(self.specs_dir(sig))?;
363 let path = self.specs_dir(sig).join(format!("{}.json", spec.id));
364 write_canonical_json(&path, spec)?;
365 Ok(spec.id.clone())
366 }
367
368 pub fn list_specs(&self, sig: &str) -> Result<Vec<Spec>, StoreError> {
369 let dir = self.specs_dir(sig);
370 if !dir.exists() { return Ok(Vec::new()); }
371 let mut out = Vec::new();
372 for f in fs::read_dir(dir)? {
373 let f = f?;
374 if f.path().extension().is_some_and(|e| e == "json") {
375 let bytes = fs::read(f.path())?;
376 out.push(serde_json::from_slice(&bytes)?);
377 }
378 }
379 Ok(out)
380 }
381
382 fn trace_path(&self, run_id: &str) -> PathBuf {
385 self.root.join("traces").join(run_id).join("trace.json")
386 }
387
388 pub fn save_trace(&self, tree: &lex_trace::TraceTree) -> Result<String, StoreError> {
389 let path = self.trace_path(&tree.run_id);
390 write_canonical_json(&path, tree)?;
391 Ok(tree.run_id.clone())
392 }
393
394 pub fn load_trace(&self, run_id: &str) -> Result<lex_trace::TraceTree, StoreError> {
395 let bytes = fs::read(self.trace_path(run_id))?;
396 Ok(serde_json::from_slice(&bytes)?)
397 }
398
399 pub fn list_traces(&self) -> Result<Vec<String>, StoreError> {
400 let dir = self.root.join("traces");
401 if !dir.exists() { return Ok(Vec::new()); }
402 let mut out = Vec::new();
403 for entry in fs::read_dir(dir)? {
404 let entry = entry?;
405 if entry.file_type()?.is_dir() {
406 out.push(entry.file_name().to_string_lossy().to_string());
407 }
408 }
409 out.sort();
410 Ok(out)
411 }
412
413 fn lookup_lifecycle(&self, stage_id: &str) -> Result<(String, Lifecycle), StoreError> {
416 for sig in self.list_sigs()? {
418 if let Ok(life) = self.read_lifecycle(&sig) {
419 if life.transitions.iter().any(|t| t.stage_id == stage_id) {
420 return Ok((sig, life));
421 }
422 }
423 }
424 Err(StoreError::UnknownStage(stage_id.into()))
425 }
426
427 fn read_lifecycle(&self, sig: &str) -> Result<Lifecycle, StoreError> {
428 let path = self.lifecycle_path(sig);
429 if !path.exists() {
430 return Ok(Lifecycle { sig_id: sig.into(), transitions: Vec::new() });
431 }
432 let bytes = fs::read(&path)?;
433 Ok(serde_json::from_slice(&bytes)?)
434 }
435
436 fn write_lifecycle(&self, sig: &str, life: &Lifecycle) -> Result<(), StoreError> {
437 write_canonical_json(&self.lifecycle_path(sig), life)
438 }
439
440 pub fn publish_program(
453 &self,
454 branch: &str,
455 stages: &[lex_ast::Stage],
456 diff: &lex_vcs::DiffReport,
457 new_imports: &lex_vcs::ImportMap,
458 activate: bool,
459 ) -> Result<PublishOutcome, StoreError> {
460 use std::collections::{BTreeMap, BTreeSet};
461
462 if let Err(errors) = lex_types::check_program(stages) {
471 return Err(StoreError::TypeError(errors));
472 }
473
474 let old_head = self.branch_head(branch)?;
476 let old_name_to_sig: BTreeMap<String, String> = old_head.iter()
477 .filter_map(|(sig, stg)| {
478 self.get_metadata(stg).ok().map(|m| (m.name, sig.clone()))
479 })
480 .collect();
481 let old_effects: BTreeMap<String, BTreeSet<String>> = old_head.iter()
482 .filter_map(|(sig, stg)| {
483 let ast = self.get_ast(stg).ok()?;
484 match ast {
485 lex_ast::Stage::FnDecl(fd) => {
486 let s: BTreeSet<String> = fd.effects.iter()
487 .map(|e| e.name.clone()).collect();
488 Some((sig.clone(), s))
489 }
490 _ => None,
491 }
492 })
493 .collect();
494 let old_imports = self.derive_imports_from_oplog(branch)?;
495
496 let op_kinds = lex_vcs::diff_to_ops(lex_vcs::DiffInputs {
497 old_head: &old_head,
498 old_name_to_sig: &old_name_to_sig,
499 old_effects: &old_effects,
500 old_imports: &old_imports,
501 new_stages: stages,
502 new_imports,
503 diff,
504 }).map_err(|e| StoreError::InvalidTransition(format!("diff_to_ops: {e}")))?;
505
506 let mut ops_out: Vec<PublishOp> = Vec::new();
507 let mut last_op_id: Option<lex_vcs::OpId> = None;
508 for kind in op_kinds {
509 if let Some(stg) = stage_for_kind(&kind, stages) {
512 if !matches!(stg, lex_ast::Stage::Import(_)) {
513 self.publish(stg)?;
514 if activate {
515 if let Some(stage_id_str) = stage_id(stg) {
516 let _ = self.activate(&stage_id_str);
517 }
518 }
519 }
520 }
521 let transition = transition_for_kind(&kind);
522 let attestable = attestable_stage_ids(&transition);
523 let head_now = self.get_branch(branch)?.and_then(|b| b.head_op);
524 let op = lex_vcs::Operation::new(
525 kind.clone(),
526 head_now.into_iter().collect::<Vec<_>>(),
527 );
528 let op_id = self.apply_operation(branch, op, transition)?;
529 self.record_typecheck_passed(&attestable, &op_id)?;
530 ops_out.push(PublishOp {
531 op_id: op_id.clone(),
532 kind: serde_json::to_value(&kind)
533 .map_err(StoreError::Serde)?,
534 });
535 last_op_id = Some(op_id);
536 }
537
538 let head_op = match last_op_id {
539 Some(id) => Some(id),
540 None => self.get_branch(branch)?.and_then(|b| b.head_op),
542 };
543
544 Ok(PublishOutcome {
545 ops: ops_out,
546 head_op,
547 })
548 }
549
550 pub fn derive_imports_from_oplog(
551 &self,
552 branch: &str,
553 ) -> Result<lex_vcs::ImportMap, StoreError> {
554 use lex_vcs::OperationKind::*;
555 let log = lex_vcs::OpLog::open(self.root())?;
556 let head = match self.get_branch(branch)?.and_then(|b| b.head_op) {
557 Some(h) => h,
558 None => return Ok(Default::default()),
559 };
560 let mut out: lex_vcs::ImportMap = Default::default();
561 for r in log.walk_forward(&head, None)? {
562 match r.op.kind {
563 AddImport { in_file, module } => {
564 out.entry(in_file).or_default().insert(module);
565 }
566 RemoveImport { in_file, module } => {
567 if let Some(set) = out.get_mut(&in_file) { set.remove(&module); }
568 }
569 _ => {}
570 }
571 }
572 Ok(out)
573 }
574
575 pub fn apply_operation_checked(
628 &self,
629 branch: &str,
630 op: lex_vcs::Operation,
631 transition: lex_vcs::StageTransition,
632 candidate: &[lex_ast::Stage],
633 ) -> Result<lex_vcs::OpId, StoreError> {
634 if let Err(errors) = lex_types::check_program(candidate) {
635 return Err(StoreError::TypeError(errors));
636 }
637 let attestable = attestable_stage_ids(&transition);
638 let op_id = self.apply_operation(branch, op, transition)?;
639 self.record_typecheck_passed(&attestable, &op_id)?;
640 Ok(op_id)
641 }
642
643 pub fn attestation_log(&self) -> Result<lex_vcs::AttestationLog, StoreError> {
650 Ok(lex_vcs::AttestationLog::open(self.root())?)
651 }
652
653 fn record_typecheck_passed(
666 &self,
667 stage_ids: &[String],
668 op_id: &lex_vcs::OpId,
669 ) -> Result<(), StoreError> {
670 if stage_ids.is_empty() {
671 return Ok(());
672 }
673 let log = self.attestation_log()?;
674 for stage_id in stage_ids {
675 let attestation = lex_vcs::Attestation::new(
676 stage_id.clone(),
677 Some(op_id.clone()),
678 None,
679 lex_vcs::AttestationKind::TypeCheck,
680 lex_vcs::AttestationResult::Passed,
681 typecheck_producer(),
682 None,
683 );
684 log.put(&attestation)?;
685 }
686 Ok(())
687 }
688
689 pub fn apply_operation(
692 &self,
693 branch: &str,
694 op: lex_vcs::Operation,
695 transition: lex_vcs::StageTransition,
696 ) -> Result<lex_vcs::OpId, StoreError> {
697 if branch != DEFAULT_BRANCH && self.get_branch(branch)?.is_none() {
704 return Err(StoreError::UnknownBranch(branch.into()));
705 }
706 let log = lex_vcs::OpLog::open(self.root())?;
707 let head_op = self.get_branch(branch)?.and_then(|b| b.head_op);
708 let new_head = lex_vcs::apply(&log, head_op.as_ref(), op, transition)
709 .map_err(|e| match e {
710 lex_vcs::ApplyError::Persist(io) => StoreError::Io(io),
711 other => StoreError::Apply(other),
712 })?;
713 self.set_branch_head_op(branch, new_head.op_id.clone())?;
714 Ok(new_head.op_id)
715 }
716}
717
718fn stage_name(stage: &Stage) -> &str {
719 match stage {
720 Stage::FnDecl(fd) => &fd.name,
721 Stage::TypeDecl(td) => &td.name,
722 Stage::Import(i) => &i.alias,
723 }
724}
725
726fn stage_for_kind<'a>(
727 kind: &lex_vcs::OperationKind,
728 stages: &'a [lex_ast::Stage],
729) -> Option<&'a lex_ast::Stage> {
730 use lex_vcs::OperationKind::*;
731 let target_sig = match kind {
732 AddFunction { sig_id, .. } | ModifyBody { sig_id, .. }
733 | ChangeEffectSig { sig_id, .. } | AddType { sig_id, .. }
734 | ModifyType { sig_id, .. } => Some(sig_id.clone()),
735 RenameSymbol { to, .. } => Some(to.clone()),
736 _ => None,
737 };
738 let target_sig = target_sig?;
739 stages.iter().find(|s| sig_id(s).as_deref() == Some(target_sig.as_str()))
740}
741
742fn transition_for_kind(kind: &lex_vcs::OperationKind) -> lex_vcs::StageTransition {
743 use lex_vcs::OperationKind::*;
744 use lex_vcs::StageTransition;
745 match kind {
746 AddFunction { sig_id, stage_id, .. }
747 | AddType { sig_id, stage_id } => StageTransition::Create {
748 sig_id: sig_id.clone(), stage_id: stage_id.clone(),
749 },
750 RemoveFunction { sig_id, last_stage_id }
751 | RemoveType { sig_id, last_stage_id } => StageTransition::Remove {
752 sig_id: sig_id.clone(), last: last_stage_id.clone(),
753 },
754 ModifyBody { sig_id, from_stage_id, to_stage_id }
755 | ChangeEffectSig { sig_id, from_stage_id, to_stage_id, .. }
756 | ModifyType { sig_id, from_stage_id, to_stage_id } => StageTransition::Replace {
757 sig_id: sig_id.clone(),
758 from: from_stage_id.clone(),
759 to: to_stage_id.clone(),
760 },
761 RenameSymbol { from, to, body_stage_id } => StageTransition::Rename {
762 from: from.clone(), to: to.clone(),
763 body_stage_id: body_stage_id.clone(),
764 },
765 AddImport { .. } | RemoveImport { .. } => StageTransition::ImportOnly,
766 Merge { .. } => StageTransition::Merge { entries: Default::default() },
767 }
768}
769
770fn typecheck_producer() -> lex_vcs::ProducerDescriptor {
775 lex_vcs::ProducerDescriptor {
776 tool: "lex-store".into(),
777 version: env!("CARGO_PKG_VERSION").into(),
778 model: None,
779 }
780}
781
782fn attestable_stage_ids(transition: &lex_vcs::StageTransition) -> Vec<String> {
789 use lex_vcs::StageTransition::*;
790 match transition {
791 Create { stage_id, .. } => vec![stage_id.clone()],
792 Replace { to, .. } => vec![to.clone()],
793 Rename { body_stage_id, .. } => vec![body_stage_id.clone()],
794 Merge { entries } => entries
795 .values()
796 .filter_map(|opt| opt.clone())
797 .collect(),
798 Remove { .. } | ImportOnly => Vec::new(),
799 }
800}
801
802fn write_canonical_json<T: Serialize>(path: &Path, value: &T) -> Result<(), StoreError> {
803 let v = serde_json::to_value(value)?;
804 let s = lex_ast::canon_json::to_canonical_string(&v);
805 if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; }
806 fs::write(path, s)?;
807 Ok(())
808}
809
810#[allow(dead_code)]
811fn read_json<T: DeserializeOwned>(path: &Path) -> Result<T, StoreError> {
812 let bytes = fs::read(path)?;
813 Ok(serde_json::from_slice(&bytes)?)
814}